我与LiteDB的工作

目录

继承

文本搜索

索引

结论


在本文中,您将看到一个桌面应用程序,该应用程序创建许多对象并在其中搜索文本。

最近,我正在为我的程序寻找存储系统。这是一个桌面应用程序,可创建许多对象并在其中搜索文本。所以我想:我为什么不尝试一些新的东西。我可以使用某种文档数据库代替SQL数据库。但我不想有一个单独的服务器,我希望这个数据库与一个简单的文件一起工作。在互联网上搜索这种数据库的.NET应用程序很快将我带到了LiteDB。在这里,我想分享我对这个数据库的经验。

继承

我的程序工作原理如下。我想存储这样的对象:

internal class Item
{
    public string Title { get; set; }

    public string Description { get; set; }

    public List<Field> Fields { get; set; } = new List<Field>();
}

但是Field类是abstract。它有许多后代:

internal abstract class Field
{
}

internal sealed class TextField : Field
{
    public string Text { get; set; }
}

internal sealed class PasswordField : Field
{
    public string Password { get; set; }
}

internal sealed class DescriptionField : Field
{
    public string Description { get; set; }
}

...

在使用SQL数据库时,我必须配置Field类的各种后代的存储。我认为使用LiteDB,我将不得不编写自己的BSON序列化机制,LiteDB提供了这样的机会。但我感到惊喜。我什么都不需要。已经实现了各种类型的序列化和反序列化。您只需创建必要的对象:

var items = new Item[]
{
    new Item
    {
        Title = "item1",
        Description = "description1",
        Fields =
        {
            new TextField
            {
                Text = "text1"
            },
            new PasswordField
            {
                Password = "123"
            }
        }
    },
    new Item
    {
        Title = "item2",
        Description = "description2",
        Fields =
        {
            new TextField
            {
                Text = "text2"
            },
            new DescriptionField
            {
                Description = "description2"
            }
        }
    }
};

...并将它们插入到数据库中:

using (var db = new LiteDatabase(connectionString))
{
    var collection = db.GetCollection<Item>();

    collection.InsertBulk(items);
}

就这样。LiteDB具有LiteDB.Studio实用程序,允许您查看数据库的内容。让我们看看我们的对象是如何存储的:

{
  "_id": {"$oid": "62bf12ce12a00b0f966e9afa"},
  "Title": "item1",
  "Description": "description1",
  "Fields":
  [
    {
      "_type": "LiteDBSearching.TextField, LiteDBSearching",
      "Text": "text1"
    },
    {
      "_type": "LiteDBSearching.PasswordField, LiteDBSearching",
      "Password": "123"
    }
  ]
}

看起来每个对象都有一个_type属性,允许从数据库正确反序列化。

好吧,我们已经保存了我们的对象。让我们继续阅读。

文本搜索

正如我之前所说,我需要搜索Item对象,其中TitleDescription属性及其字段的属性(Fields属性)包含一些文本。

TitleDescription属性内部搜索并不复杂。文档非常清楚:

var items = collection.Query()
    .Where(i => i.Title.Contains("1") || i.Description.Contains("1"))
    .ToArray();

但是按字段搜索存在问题。你看,abstractField不包含任何属性。这就是为什么我不能参考他们。幸运的是,LiteDB允许您使用字符串查询语法:

var items = collection.Query()
    .Where("$.Title LIKE '%1%' OR $.Description LIKE '%1%'")
    .ToArray();

那么,我们如何使用此语法在字段中搜索呢?该文档给出了一个提示,即查询应如下所示:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR $.Fields[@.Text] 
LIKE '%1%' OR $.Fields[@.Description] LIKE '%1%' OR $.Fields[@.Password] LIKE '%1%'

但这会导致一个错误:

Left expression `$.Fields[@.Text]` returns more than one result. 
Try use ANY or ALL before operant.

是的,使用ANY函数解决了这个问题:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR ANY($.Fields[@.Text LIKE '%1%']) 
OR ANY($.Fields[@.Description LIKE '%1%']) OR ANY($.Fields[@.Password LIKE '%1%'])

但我想对这个表达方式发表几点评论。首先,我们似乎可以使用这样的表达式:

ANY($.Fields[@.Text LIKE '%1%'])

但事实并非如此。如果尝试使用此表达式查询元素,将收到以下错误:

Expression 'ANY($.Fields[@.Text LIKE "%1%"])' are not supported as predicate expression.

很奇怪,不是吗?事实证明,你应该这样写:

ANY($.Fields[@.Text LIKE '%1%']) = true

我立即想起SQL Server谓词中的10。我不知道他们为什么以这种方式实现它。

其次,我对短语感到困惑尝试在操作之前使用 ANY ALL。对我来说,这与函数调用不对应。事实证明,LiteDB支持以下语法:

$.Fields[*].Text ANY LIKE '%1%'

遗憾的是,文档中没有对此进行描述。我在Github上的LiteDB测试源代码中遇到了这个问题。此语法作为谓词工作正常,无需true和做任何比较。

最后,我们可以重写您的查询表达式,如下所示:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR ($.Fields[*].Text ANY LIKE '%1%') 
OR ($.Fields[*].Description ANY LIKE '%1%') OR ($.Fields[*].Password ANY LIKE '%1%')

这里还有几件事困扰着我。首先,对于每个新字段类型,如果我使用新的属性名称,我将不得不重写此表达式。我们能做些什么吗?嗯,我们可以。

LiteDB支持BsonField属性,该属性指定存储此属性的数据库字段的名称。它的用法如下:

internal sealed class TextField : Field
{
    [BsonField("TextField")]
    public string Text { get; set; }
}

internal sealed class PasswordField : Field
{
    [BsonField("TextField")]
    public string Password { get; set; }
}

internal sealed class DescriptionField : Field
{
    [BsonField("TextField")]
    public string Description { get; set; }
}

现在我们可以为任何Field对象编写一个查询表达式:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR $.Fields[*].TextField ANY LIKE '%1%'

当我添加Field类的新后代时,我可以简单地用[BsonField("TextField")]属性标记它的属性。然后,我就不需要更改查询的表达式。

不幸的是,这种方法并不能完全解决我们所有的问题。事实是,Field的后代具有任意数量的属性,我需要在其中搜索文本。这意味着我可能无法将它们全部保存在现有的数据库字段中。

这就是为什么我仍然会使用以下形式的表达:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR ($.Fields[*].Text ANY LIKE '%1%') 
OR ($.Fields[*].Description ANY LIKE '%1%') OR ($.Fields[*].Password ANY LIKE '%1%')

我们还有另一个问题。我在表达式中多次使用搜索字符串%1%。还有一种SQL注入攻击(尽管我不确定我是否可以在这里使用SQL这个词)。简而言之,我谈论的是在查询中使用参数。LiteDB API允许我们使用它们:

但是我们到底应该怎么做呢?不幸的是,文档再次让我失望。我必须转到LiteDB测试的源代码,并在那里查看我应该如何使用参数:

var items = collection.Query()
    .Where("$.Title LIKE @0 OR $.Description LIKE @0 OR ($.Fields[*].Text ANY LIKE @0) 
OR ($.Fields[*].Description ANY LIKE @0) OR ($.Fields[*].Password ANY LIKE @0)", "%1%")
    .ToArray();

好了,搜索就完成了。但是它有多快呢?

索引

LiteDB支持索引。当然,我的应用程序不会存储大量数据,因此它并不重要。但是,尽快使用索引和执行查询会很棒。

首先,我们需要了解此查询是否使用某种索引。为此,LiteDB具有EXPLAIN命令。在LiteDB.Studio中,我以这种方式执行查询:

EXPLAIN
SELECT $ FROM Item
WHERE $.Title LIKE '%1%'
    OR $.Description LIKE '%1%'
    OR ($.Fields[*].Text ANY LIKE '%1%')
    OR ($.Fields[*].Description ANY LIKE '%1%')
    OR ($.Fields[*].Password ANY LIKE '%1%')

结果包含有关所用索引的信息:

"index":
  {
    "name": "_id",
    "expr": "$._id",
    "order": 1,
    "mode": "FULL INDEX SCAN(_id)",
    "cost": 100
  },

如您所见,我们现在必须浏览所有数据。我想取得更好的结果。

该文档明确指出,可以基于数组类型属性创建索引。在这种情况下,我可以搜索此数组中的任何元素。例如,我可以创建一个索引来搜索字段的Text属性:

collection.EnsureIndex("TextIndex", "$.Fields[*].Text");

现在我们可以在查询中使用此索引:

var items = collection.Query()
    .Where("$.Fields[*].Text ANY LIKE @0", "%1%")
    .ToArray();

LiteDB.Studio中的EXPLAIN命令显示此查询确实使用我们创建的索引:

"index":
  {
    "name": "TextIndex",
    "expr": "MAP($.Fields[*]=>@.Text)",
    "order": 1,
    "mode": "FULL INDEX SCAN(TextIndex LIKE \"%1%\")",
    "cost": 100
  },

但是我们如何将所有属性组合在一个索引中呢?在这里,我们可以使用CONCAT命令。它将多个值组合到一个数组中。创建完整索引的方式如下:

collection.EnsureIndex("ItemsIndex", @"CONCAT($.Title,
            CONCAT($.Description,
                CONCAT($.Fields[*].Text,
                    CONCAT($.Fields[*].Password,
                            $.Fields[*].Description
                    )
                )
            )
        )");

要使用它,我们必须重写查询的表达式:

var items = collection.Query()
    .Where(
        @"CONCAT($.Title,
            CONCAT($.Description,
                CONCAT($.Fields[*].Text,
                    CONCAT($.Fields[*].Password,
                            $.Fields[*].Description
                    )
                )
            )
        ) ANY LIKE @0",
        "%1%")
    .ToArray();

现在我们的搜索真正使用了索引:

"index":
  {
    "name": "ItemsIndex",
    "expr": "CONCAT($.Title,CONCAT($.Description,CONCAT(MAP($.Fields[*]=>@.Text),
             CONCAT(MAP($.Fields[*]=>@.Password),MAP($.Fields[*]=>@.Description)))))",
    "order": 1,
    "mode": "FULL INDEX SCAN(ItemsIndex LIKE \"%3%\")",
    "cost": 100
  },

不幸的是,LIKE运算符仍然导致FULL INDEX SCAN。我们只能希望该指数能带来一些优势。但是等等。为什么我们只希望当我们能够衡量它时?毕竟,我们有BenchmarkDotNet

我编写了以下代码进行性能测试:

[SimpleJob(RuntimeMoniker.Net60)]
public class LiteDBSearchComparison
{
    private LiteDatabase _database;
    private ILiteCollection<Item> _collection;

    [GlobalSetup]
    public void Setup()
    {
        if (File.Exists("compare.dat"))
            File.Delete("compare.dat");

        _database = new LiteDatabase("Filename=compare.dat");

        _collection = _database.GetCollection<Item>();

        _collection.EnsureIndex("ItemIndex", @"CONCAT($.Title,
            CONCAT($.Description,
                CONCAT($.Fields[*].Text,
                    CONCAT($.Fields[*].Password,
                            $.Fields[*].Description
                    )
                )
            )
        )");

        for (int i = 0; i < 100; i++)
        {
            var item = new Item
            {
                Title = "t",
                Description = "d",
                Fields =
                {
                    new TextField { Text = "te" },
                    new PasswordField { Password = "p" },
                    new DescriptionField { Description = "de" }
                }
            };

            _collection.Insert(item);
        }
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        _database.Dispose();
    }

    [Benchmark(Baseline = true)]
    public void WithoutIndex()
    {
        _ = _collection.Query()
            .Where("$.Title LIKE @0 OR $.Description LIKE @0 OR 
            ($.Fields[*].Text ANY LIKE @0) OR ($.Fields[*].Description 
             ANY LIKE @0) OR ($.Fields[*].Password ANY LIKE @0)",
                "%1%")
            .ToArray();
    }

    [Benchmark]
    public void WithIndex()
    {
        _ = _collection.Query()
            .Where(@"CONCAT($.Title,
                        CONCAT($.Description,
                            CONCAT($.Fields[*].Text,
                                CONCAT($.Fields[*].Password,
                                        $.Fields[*].Description
                                )
                            )
                        )
                    ) ANY LIKE @0",
                "%1%")
            .ToArray();
    }
}

以下是结果:

方法

Mean

Error

StdDev

Ratio

WithoutIndex

752.7us

14.7us

21.56us

1.00

WithIndex

277.5us

4.30us

4.02us

0.37

如您所见,该指数确实提供了显着的性能优势。

结论

这就是我想说的。总的来说,我对LiteDB印象很好。我已准备好将其用作小型项目的文档存储。不幸的是,在我看来,文档不是最好的水平。

https://www.codeproject.com/Articles/5337270/My-Work-with-LiteDB

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值