MongoDB强类型Collection用法示例

.NET中使用MongoDB非常简单,一般来说可以直接使用BsonDocument,也可以使用定义好的数据类型对文档进行CRUD操作。本文通过实例对比一下两种方式的优劣,通常,通过强类型Collection对文档进行操作更为便捷。

BsonDocument CRUD

MongoDB C# Driver官方文档 是直接用BsonDocument进行操作的,猜想官方文档可能是想突出体现MongoDB是schema-less的,可以不定义schema而读写文档。但noSQL不代表schema-less,而是not only SQL。在同一个collection中还是推荐使用统一的data model,因为统一的schema可以更方便读写,而且也方便使用索引。

下文将使用的示例文档定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class DemoEntry
{
    public string Uuid { get; set; }
    public string Value { get; set; }

    public override string ToString()
    {
        return Uuid + ": " + Value;
    }
}

获取一个弱类型collection:

1
IMongoCollection<BsonDocument> _bsonCollection = database.GetCollection<BsonDocument>(CollectionName);

使用弱类型collection进行CRUD:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var entry = new DemoEntry
{
    Uuid = uuid,
    Value = "1"
};

Console.WriteLine("-- BsonDocument CRUD ---");
var filter = Builders<BsonDocument>.Filter.Eq("Uuid", uuid);
// Create
Console.WriteLine("[Create] " + entry);
await _bsonCollection.ReplaceOneAsync(filter, entry.ToBsonDocument(), new ReplaceOptions { IsUpsert = true });

// Read
var doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
var retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
Console.WriteLine("[Read] " + retrievedEntry);

// Update
await _bsonCollection.UpdateOneAsync(filter, Builders<BsonDocument>.Update.Set("Value", "2"));
doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
Console.WriteLine("[Update] " + retrievedEntry);

// Delete
var t = await _bsonCollection.DeleteOneAsync(filter);
Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);

注意在弱类型collection的读写中,我们需要在写操作之前手动将class转换成bsondocument (entry.ToBsonDocument()),同时在读操作之后手动将bsondocument解析成class (BsonSerializer.Deserialize<DemoEntry>(doc))。此外,我们在构建filter时需要手动输入field名称的字符串,很容易出错,而且是运行时错误。

强类型CRUD示例

获取一个强类型collection:

1
IMongoCollection<DemoEntry> _stronglyTypedCollection = database.GetCollection<DemoEntry>(CollectionName);

CRUD示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public async Task StronglyTypedCrudAsync(string uuid)
{
	var entry = new DemoEntry
	{
		Uuid = uuid,
		Value = "1"
	};

	Console.WriteLine("-- Strongly typed CRUD ---");
	// Create
	Console.WriteLine("[Create] " + entry);
	await _stronglyTypedCollection.ReplaceOneAsync(x => x.Uuid == uuid, entry, new ReplaceOptions {IsUpsert = true});

	// Read
	var retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
	Console.WriteLine("[Read] " + retrievedEntry);

	// Update
	await _stronglyTypedCollection.UpdateOneAsync(x => x.Uuid == uuid, Builders<DemoEntry>.Update.Set(x => x.Value, "2"));
	retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
	Console.WriteLine("[Update] " + retrievedEntry);

	// Delete
	var t = await _stronglyTypedCollection.DeleteOneAsync(x => x.Uuid == uuid);
	Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
}

是否要使用强类型collection?

推荐使用强类型collection。 优点非常明显:

  • 不易出错:使用lambda表达式指定filter,在编译时就确定了field name
  • 代码简捷:不需要手动在class和bsondocument之间转换。
  • LINQ语法支持:这一点在使用aggregation的时候极为便捷,一行代码就能处理十行的一个aggregation pipeline。

IgnoreExtraElements Convention

大部分MongoDB新用户想必都会遇到如下错误:

Element ‘_id’ does not match any field or property of class

_id是什么 ?

In MongoDB, each document stored in a collection requires a unique _id field that acts as a primary key. If an inserted document omits the _id field, the MongoDB driver automatically generates an ObjectId for the _id field.

但在代码中我们往往不想显式定义这么个字段,因为它对开发者来说没有太多意义。可以通过两种方法解决这个问题。

[BsonIgnoreExtraElements] Annotation

在类定义中加上[BsonIgnoreExtraElements] annotation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[BsonIgnoreExtraElements]
public class DemoEntry
{
    public string Uuid { get; set; }
    public string Value { get; set; }

    public override string ToString()
    {
        return Uuid + ": " + Value;
    }
}

此方法的缺点在于要引入MongoDB驱动包的依赖。如果这个类定义在一个接口库中,其他用户在使用此接口库时也会依赖MongoDB nuget包。而在系统设计中,底层存储逻辑对上层用户应该是透明的。

设置IgnoreExtraElementsConvention

另一种方案是在获取collection之前设置IgnoreExtraElementsConvention:

1
2
var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

IgnoreExtraElementsConvention的意思是在反序列化文档时,忽略映射类中未定义的字段。

对强类型collection的用法,设置IgnoreExtraElementsConvention和获取collection的顺序非常重要

1
2
3
4
_beingCollection = database.GetCollection<BsonDocument>(AvatarFrameworkMongoConfig.BeingCollectionName);

var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

上面就是错误的顺序,下面错误依然会发生:

Element ‘_id’ does not match any field or property of class

正确设置方法:

1
2
3
4
var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

_beingCollection = database.GetCollection<BsonDocument>(AvatarFrameworkMongoConfig.BeingCollectionName);

而对于弱类型collection,这个设置顺序似乎是无所谓的。猜想应该是在创建强类型collection时会使用当前的ConventionRegistry设置BsonClassMap

Full Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
using System;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Driver;

namespace MongoDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var demo = new StronglyTypedDemo();
            demo.BsonCrudAsync("abc").Wait();
            Console.WriteLine("-----------------------------------");
            demo.StronglyTypedCrudAsync("abc").Wait();

            Console.ReadLine();
        }
    }

    public class DemoEntry
    {
        public string Uuid { get; set; }
        public string Value { get; set; }

        public override string ToString()
        {
            return Uuid + ": " + Value;
        }
    }

    public class StronglyTypedDemo
    {
        private const string DatabaseName = "";
        private const string CollectionName = "Test";
        private const string ConnectionString = "";

        private readonly MongoClient MongoClient;
        private readonly IMongoCollection<BsonDocument> _bsonCollection;
        private readonly IMongoCollection<DemoEntry> _stronglyTypedCollection;

        public StronglyTypedDemo()
        {
            var clientSettings = MongoClientSettings.FromConnectionString(ConnectionString);
            clientSettings.ConnectTimeout = TimeSpan.FromSeconds(5);
            clientSettings.ServerSelectionTimeout = TimeSpan.FromSeconds(5);
            clientSettings.AllowInsecureTls = true;

            MongoClient = new MongoClient(clientSettings);
            var database = MongoClient.GetDatabase(DatabaseName);

            var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
            ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);

            _bsonCollection = database.GetCollection<BsonDocument>(CollectionName);
            _stronglyTypedCollection = database.GetCollection<DemoEntry>(CollectionName);
        }

        public async Task StronglyTypedCrudAsync(string uuid)
        {
            var entry = new DemoEntry
            {
                Uuid = uuid,
                Value = "1"
            };

            Console.WriteLine("-- Strongly typed CRUD ---");
            // Create
            Console.WriteLine("[Create] " + entry);
            await _stronglyTypedCollection.ReplaceOneAsync(x => x.Uuid == uuid, entry, new ReplaceOptions {IsUpsert = true});

            // Read
            var retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
            Console.WriteLine("[Read] " + retrievedEntry);

            // Update
            await _stronglyTypedCollection.UpdateOneAsync(x => x.Uuid == uuid, Builders<DemoEntry>.Update.Set(x => x.Value, "2"));
            retrievedEntry = await _stronglyTypedCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync();
            Console.WriteLine("[Update] " + retrievedEntry);

            // Delete
            var t = await _stronglyTypedCollection.DeleteOneAsync(x => x.Uuid == uuid);
            Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
        }

        public async Task BsonCrudAsync(string uuid)
        {
            var entry = new DemoEntry
            {
                Uuid = uuid,
                Value = "1"
            };

            Console.WriteLine("-- BsonDocument CRUD ---");
            var filter = Builders<BsonDocument>.Filter.Eq("Uuid", uuid);
            // Create
            Console.WriteLine("[Create] " + entry);
            await _bsonCollection.ReplaceOneAsync(filter, entry.ToBsonDocument(), new ReplaceOptions { IsUpsert = true });

            // Read
            var doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
            var retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
            Console.WriteLine("[Read] " + retrievedEntry);

            // Update
            await _bsonCollection.UpdateOneAsync(filter, Builders<BsonDocument>.Update.Set("Value", "2"));
            doc = await _bsonCollection.Find(filter).FirstOrDefaultAsync();
            retrievedEntry = BsonSerializer.Deserialize<DemoEntry>(doc);
            Console.WriteLine("[Update] " + retrievedEntry);

            // Delete
            var t = await _bsonCollection.DeleteOneAsync(filter);
            Console.WriteLine("[Delete] deleted count: " + t.DeletedCount);
        }
    }
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- BsonDocument CRUD ---
[Create] abc: 1
[Read] abc: 1
[Update] abc: 2
[Delete] deleted count: 1
-----------------------------------
-- Strongly typed CRUD ---
[Create] abc: 1
[Read] abc: 1
[Update] abc: 2
[Delete] deleted count: 1