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可以更方便读写,而且也方便使用索引。

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

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

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

获取一个弱类型collection:

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

使用弱类型collection进行CRUD:

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:

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

CRUD示例:

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:

[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:

var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);
IgnoreExtraElementsConvention的意思是在反序列化文档时,忽略映射类中未定义的字段。

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

_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

正确设置方法:

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

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);
        }
    }
}

运行结果:

-- 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