65.9K
CodeProject 正在变化。 阅读更多。
Home

noDB

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (9投票s)

2020年1月3日

CPOL

4分钟阅读

viewsIcon

16823

downloadIcon

424

使用对象列表、LINQ和类似SQL的字符串查询,代替数据库,并使用fastJSON序列化器

引言

我已经在生产环境中使用RaptorDB很长时间了,最近我想,如果我可以在一个项目中不用数据库引擎,但仍然拥有相同的功能会怎么样。这个想法来自RealNews,它根本不使用数据库,即使有大量的 feed 条目也能很好地工作。

通过我将在下面解释的内容,我能够将此功能添加到我的应用程序中,并通过配置设置,在RaptorDB和动态LINQ to objects之间无缝切换,排序和分页功能对用户保持不变。

显然,这并非适用于所有情况的解决方案,并且仅当您的数据大小有限且可以放入内存时才有意义。

一些用途包括

  • 在应用程序中缓存数据,以实现快速的无往返访问
  • 小型/中型数据量的零安装应用程序
  • 在Web API后面定义用户查询功能

需要什么

假设你有一个对象列表

var list = new List<SalesInvoice>();

在代码中,您已经可以使用 LINQ 做很多事情,例如

var results = list.FindAll(x => x.Serial<100);

这在编译时效果很好,但是如果过滤器是一个动态字符串,您从浏览器中获取,并且您的代码位于 API 后面的服务器上怎么办?

输入 System.LINQ.Dynamic,它是 Microsoft 的一段代码,允许您在 Where() 函数中使用字符串。

有了这个,现在你可以这样做

var result = list.Where("name.Contains(\"Peter\") and serial<100");

这太棒了,但我真的喜欢编写类似 SQL 的查询,而不是 C# 特定的查询

var result = list.Where("name = \"Peter\" and serial<100");

因此,需要对 System.LINQ.Dynamic 进行修改,这并不适合胆小的人。

工作原理

现在,System.LINQ.Dynamic 是一个复杂的东西,不容易理解,但是通过调试,我能够找到进行相等性检查的位置,并插入所需的代码。

该代码检查等号左侧的 string 类型,然后构建一个 Expression 来处理右侧的 Contains() 检查。

此外,为了允许搜索 Guid 类型,它首先使用 ToString(),然后使用 Contains(),因此您可以搜索类似字符串的 Guid

Contains() 的问题

首先,我使用了 Contains(),但很快发现它是区分大小写的,这不是用户所期望的,所以我不得不编写一个不区分大小写的版本,使用 IndexOf(),本质上是 Expression 语法中的以下 C# 代码

var bool = str.IndexOf("val", StringComparison.OrdinalIgnoreCase) >= 0 ; // yes/no "val" in str

代码

下面是在 System.LINQ.DynamicDynamic.cs 中添加和更改的代码。我注释掉的 throw 异常在某种原因下发生了,但是跳过它就可以正常工作。

// changed
void CheckAndPromoteOperands(Type signatures, string opName, 
                             ref Expression left, ref Expression right, int errorPos)
{
    Expression[] args = new Expression[] { left, right };
    MethodBase method;
    if (FindMethod(signatures, "F", false, args, out method) != 1)
        ;//  throw IncompatibleOperandsError(opName, left, right, errorPos); // MG
    left = args[0];
    right = args[1];
}
// added
Expression Contains(Expression left, Expression right)
{
    // if left string -> generate contains method
    Expression[] args = new Expression[] { 
             right, Expression.Constant(StringComparison.OrdinalIgnoreCase) };
    if (_indexof == null)
    {
        // using indexof() instead of contains() which is case sensitive
        FindMethod(typeof(string), "IndexOf", false, args, out MethodBase mb);
        _indexof = mb;
    }
    Expression ex = Expression.Call(left, (MethodInfo)_indexof, args);
    return Expression.GreaterThanOrEqual(ex, Expression.Constant(0));
}
// added
Expression NotContains(Expression left, Expression right)
{
    // if left string -> generate method
    Expression[] args = new Expression[] { right, Expression.Constant
                                           (StringComparison.OrdinalIgnoreCase) };
    if (_indexof == null)
    {
        // using indexof() instead of contains() which is case sensitive
        FindMethod(typeof(string), "IndexOf", false, args, out MethodBase mb);
        _indexof = mb;
    }
    Expression ex = Expression.Call(left, (MethodInfo)_indexof, args);
    return Expression.LessThan(ex, Expression.Constant(0));
}
// changed
Expression GenerateEqual(Expression left, Expression right)
{
    // MG
    if(left.Type == typeof(Guid))
    {
        FindMethod(typeof(Guid), "ToString", false, new Expression[] { }, out MethodBase ts);
        Expression tse = Expression.Call(left, (MethodInfo)ts, null);

        return Contains(tse, right);
    }

    if (left.Type == typeof(string))
        return Contains(left, right);
    else
        return Expression.Equal(left, right);
}
// changed
Expression GenerateNotEqual(Expression left, Expression right)
{   // MG
    if (left.Type == typeof(Guid))
    {
        FindMethod(typeof(Guid), "ToString", false, new Expression[] { }, out MethodBase ts);
        Expression tse = Expression.Call(left, (MethodInfo)ts, null);

        return NotContains(tse, right);
    }

    if (left.Type == typeof(string))
        return NotContains(left, right);
    else
        return Expression.NotEqual(left, right);
}

酷炫功能

LINQ 允许您分页数据

list.Where("serial<100").Skip(100).Take(10);

它还允许您对数据进行排序

list.Where("serial<100").Orderby("name"); // ascending
list.Where("serial<100").Orderby("name desc"); // descending

如您所见,属性名称不区分大小写,这使得使用它很愉快。

此外,您可以使用

  • 类似 SQL 的 and or
  • c# 风格 && ||
  • 不等于 != <>
  • 括号
  • 带有点的属性中的属性
(name = "peter" && address = "hill") or (serial<100 and date.year=2000)

name != "peter" and address <> "hill"

示例应用程序

为了展示这些功能,我创建了一个示例应用程序,它使用 Faker.dll 生成 100,000 个 SalesInvoice 对象,并允许您查询并在网格中显示结果。

您可以从菜单中选择示例查询,并通过文本框进行调整和更改。要运行查询,只需在键入时按 Enter 键。

其他可能的功能

  • RaptorDB 中,可全文搜索的列被分解为单词,因此您可以这样做
    name = "alice  bob" means (name contains "alice" and name contains "bob")
    name = "alice +bob" means (name contains "alice" or  name contains "bob")
    name = "alice -bob" means (name contains "alice" and name does not contain "bob")
  • 您还可以使用任何其他序列化程序,例如更快的加载和保存速度的 fastBinaryJSON,或者如果您的对象结构非常简单,则可以使用 protobuf 甚至 fastCSV
    • 在一个包含 13,586 个项目的列表上,我进行了测试,fastCSV 花费了 68 毫秒来加载,而 fastJSON 则花费了 230 毫秒。
  • 处理在多用户环境中添加和删除项目,即在访问时锁定列表。

附录 - 更新 1

我遇到的一个问题是,如果您在查询的属性中存在 null 值,那么 LINQ 就会崩溃,为了克服这个问题,我更改了 Contains()NotContains() 函数来检查这一点

Expression NotContains(Expression left, Expression right)
{
    // if left string -> generate method
    // FIX : if right has spaces -> generate multiple "and indexof()"
    Expression[] args = new Expression[] { right, Expression.Constant(StringComparison.OrdinalIgnoreCase) };
    if (_indexof == null)
    {
        // using indexof() instead of contains() which is case sensitive
        FindMethod(typeof(string), "IndexOf", false, args, out MethodBase mb);
        _indexof = mb;
    }
    // null checking left value
    Expression nn = Expression.Equal(Expression.Constant(null), left);
    Expression ex = Expression.Call(left, (MethodInfo)_indexof, args);
    return Expression.OrElse(nn, Expression.LessThan(ex, Expression.Constant(0)));
}

// MG
Expression Contains(Expression left, Expression right)
{
    // if left string -> generate method
    // FIX : if right has spaces -> generate multiple "and indexof()"
    Expression[] args = new Expression[] { right, Expression.Constant(StringComparison.OrdinalIgnoreCase) };
    if (_indexof == null)
    {
        // using indexof() instead of contains() which is case sensitive
        FindMethod(typeof(string), "IndexOf", false, args, out MethodBase mb);
        _indexof = mb;
    }
    // null checking left value
    Expression nn = Expression.NotEqual(Expression.Constant(null), left);
    Expression ex = Expression.Call(left, (MethodInfo)_indexof, args);
    return Expression.AndAlso(nn, Expression.GreaterThanOrEqual(ex, Expression.Constant(0)));
}

以前的版本

历史

  • 初始版本 : 2020 年 1 月 3 日
  • 更新 1 : 2020 年 1 月 11 日
    • 空值检查
© . All rights reserved.