目录
在本文中,您将看到一个桌面应用程序,该应用程序创建许多对象并在其中搜索文本。
最近,我正在为我的程序寻找存储系统。这是一个桌面应用程序,可创建许多对象并在其中搜索文本。所以我想:“我为什么不尝试一些新的东西。我可以使用某种文档数据库代替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对象,其中Title和Description属性及其字段的属性(Fields属性)包含一些文本。
在Title和Description属性内部搜索并不复杂。文档非常清楚:
var items = collection.Query()
.Where(i => i.Title.Contains("1") || i.Description.Contains("1"))
.ToArray();
但是按字段搜索存在问题。你看,abstract类Field不包含任何属性。这就是为什么我不能参考他们。幸运的是,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谓词中的1和0。我不知道他们为什么以这种方式实现它。
其次,我对短语感到困惑尝试在操作之前使用 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