.Net Core 使用MongoDb、及关系介绍

一、前言


简单介绍Asp.Net Core应用连接MongoDB数据库,并进行简单的CRUD和一对多关系的建立。

需要用到的东西:.NET Core 3.0+、MongoDB、Robot3T、MongoDB.Driver。

二、创建简单CRUD


使用MongoDB创建简单CRUD的WebApi,直接看微软的官方教程。很详细!
https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/first-mongo-app?view=aspnetcore-3.1&tabs=visual-studio

看完不太懂,也能看MongoDB.Driver的文档,也有教程 (**警告)。
https://mongodb.github.io/mongo-csharp-driver/2.11/getting_started/quick_tour/

三、上面的注意点


1. MongoClient

MongoClient实际代表数据库的一个连接池,就算使用多线程,也只需一个实例。(可以注册为单例服务,如一个单例类的属性)

2. MongoDB的查询结果调用ToList<T>小心字段对不上出错。可以尝试在Model类上加[BsonIgnoreExtraElements]特性。

四、介绍MongoDB集合间的关系与引用


1. 嵌入式关系

可以把A的文档嵌入到B的文档中。优点是比较容易的获取和维护数据。缺点是数据量不断变大,会影响读写性能,而且会出现数据冗余。

一次查询即可带出
db.users.findOne({"name":"Tom Benzamin"},{"address":1})

参考资料:https://www.runoob.com/mongodb/mongodb-relationships.html

2. 引用式关系

引用式关系是设计数据库时经常用到的方法,这种方法把A文档和B文档分开,通过引用文档的 id 字段来建立关系。
优点是像关系数据库的外键,省空间。缺点是查询要查两遍,或者要使用$lookup来关联集合,查的慢。

  • 手动引用(Manual References)
    A文档中有个字段直接存B文档的id。示例:
{
   "_id":ObjectId("52ffc33cd85242f436000001"),
   "contact": "987654321",
   "dob": "01-01-1991",
   "name": "Tom Benzamin",
   "address_ids": [
      ObjectId("52ffc4a5d85242602e000000"),
      ObjectId("52ffc4a5d85242602e000001")
   ]
}

//查询语句:
var result = db.users.find({"name":"Tom Benzamin"},{"address_ids":1})
var addresses = db.address.find({"_id":{"$in":result[0]["address_ids"]}})
  • DBRefs

{ $ref : <value>, $id : <value>, $db : <value> }

$ref:集合名称;$id:引用的id;$db:数据库名称,可选参数。

可以看到DBRef的结构比Manual References的复杂,占用的空间大,但是功能也强大。如果要跨数据库连接,都得需要使用DBRef。
MongoDB提供了函数来解析DBRef,不用像Manual References需要自己手动写两次查询。
DBRef就是在两个Collection之间定义的一个关联关系,比如,把CollectionB "_id"列的值存在CollectionA的一个列中,然后通过CollectionA这个列中所存的值在CollectionB中找到相应的记录。
示例:

{
   "_id":ObjectId("53402597d852426020000002"),
   "address": {
         "$ref": "address_home",
         "$id": ObjectId("534009e4d852427820000002"),
         "$db": "runoob"},
   "name": "Tom Benzamin"
}

//查询语句:
var user = db.users.findOne({"name":"Tom Benzamin"});
var dbRef = user.address;
db[dbRef.$ref].findOne({"_id":(dbRef.$id)});

//或者另一种DBRef用法
{
   "_id":ObjectId("53402597d852426020000002"),
   "address":new DBRef('address_home',ObjectId("534009e4d852427820000002"))
   "name": "Tom Benzamin"
}

//查询语句,用DBRef的fetch方法
var user = db.users.findOne({"name":"Tom Benzamin"});
user.address.fetch();

参考资料:https://www.runoob.com/mongodb/mongodb-database-references.html
https://www.cnblogs.com/duanxz/p/10838910.html

  • 使用Aggregate的$lookup来查询手动引用
    使用$lookup来连接集合,查询对应的引用,也不错,类似SQL连表。
    示例:
db.product.aggregate([
      {
        $lookup:
          {
           from: "orders",  //需要关联的集合【orders】
           localField: "_id",   //【orders】的matching key
           foreignField: "pid", //【product】集合需要关联的键
           as: "inventory_docs" //连接到的外键数据的集合
          }
    }
 ])

了解更多:https://www.cnblogs.com/xuliuzai/p/10055535.html

五、.Net Core访问MongoDB的例子代码


1. 相关模型类

例子有两个模型,Product(产品),Manufacturer(制造商)

    [BsonIgnoreExtraElements]   //忽略其余字段
    public class Manufacturer
    {
        ///// <summary>
        ///// 主键,mongodb默认
        ///// </summary>
        //public string _id { get; set; }
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        [JsonProperty("_id")]
        public string Id { get; set; }
        /// <summary>
        /// 名称
        /// </summary>
        [BsonElement("name")]   //测试例子,name为mongodb的集合的字段
        [JsonProperty("name")]
        public string FullName { get; set; }
    }
[BsonIgnoreExtraElements]    //忽略其余字段
    public class Product
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        [JsonProperty("_id")]
        public string Id { get; set; }
        [BsonElement("name")]
        public string Name { get; set; }
        /// <summary>
        /// 图片地址
        /// </summary>
        [BsonElement("image")]
        public string Image { get; set; }
        /// <summary>
        /// 价格
        /// </summary>
        [BsonElement("price")]
        public double? Price { get; set; }
        /// <summary>
        /// 描述
        /// </summary>
        [BsonElement("description")]
        public string Description { get; set; }
        /// <summary>
        /// 制造商id
        /// </summary>
        [BsonElement("manufacturer")]
        [BsonRepresentation(BsonType.ObjectId)]
        public string ManufacturerId { get; set; }
        /// <summary>
        /// 制造商模型
        /// </summary>
        [BsonIgnoreIfNull]
        public Manufacturer Manufacturer { get; set; }
    }

2. CRUD的简单例子

列出一个制造商Service的CRUD代码

public class ManufacturerService
{
    //生产商对应集合
    private readonly IMongoCollection<Manufacturer> _manufacturerCol;
    public ManufacturerService(IMongoCollection<Manufacturer> collection)
    {
        //外部传入用到的集合
        _manufacturerCol = collection ?? throw new ArgumentNullException(nameof(collection));
    }

    /// <summary>
    /// 查询所有
    /// </summary>
    public List<Manufacturer> Get() => _manufacturerCol.Find(m => true).ToList();

    /// <summary>
    /// 查询单个
    /// </summary>
    public Manufacturer Get(string id) => _manufacturerCol.Find(m => m.Id == id).FirstOrDefault();

    public Manufacturer Create(Manufacturer manufacturer)
    {
        _manufacturerCol.InsertOne(manufacturer);
        return manufacturer;
    }

    public void Update(string id, Manufacturer manufacturer)
    {
        _manufacturerCol.ReplaceOne(m => m.Id == id, manufacturer);
    }

    public void Remove(Manufacturer manufacturer)
    {
        _manufacturerCol.DeleteOne(m => m.Id == manufacturer.Id);
    }

    public void Remove(string id)
    {
        _manufacturerCol.DeleteOne(m => m.Id == id);
    }
}

3. 使用DBRefs的文档,如何关联

MongoDB.Driver这个库有个MongoDBRef的类,表示DBRefs这种结构。以前是有API直接使用MongoDBRef,查出对应文档。据我所知,这种操作好像已经过时,不太清楚。详情看官方文档。

 MongoDBRef r1 = new MongoDBRef("testdb", "orders",new ObjectId("5e194c1b2dfe031505deaf67"));

4. 使用手动引用的文档,如何关联

  • 手动查两遍(第一次查询A,再通过引用字段,查出B)
    自己封装了一个第二遍查关联集合的方法。
 //调用自定义封装方法
await MongoDbCusHelper.IncludeList(list, _manufacturerCol, "ManufacturerId", "Manufacturer", "Id");

//定义
public static async Task<IEnumerable<TSource>> IncludeList<TSource, TDest>(
    IEnumerable<TSource> sourcesList, IMongoCollection<TDest> destCol, string srcFieldName, string newDocFieldName, string destFieldName = "_id")
{
    if (sourcesList == null)
    {
        throw new ArgumentNullException(nameof(sourcesList));
    }

    if (destCol is null)
    {
        throw new ArgumentNullException(nameof(destCol));
    }

    if (string.IsNullOrWhiteSpace(newDocFieldName))
        newDocFieldName = destCol.CollectionNamespace.CollectionName;

    //判断TSource, TDest是否存在对应字段
    PropertyInfo srcPi = typeof(TSource).GetProperty(srcFieldName);
    if (srcPi == null)
    {
        throw new ArgumentException($"{nameof(TSource)}没有{srcFieldName}这个属性");
    }
    PropertyInfo destPi = typeof(TDest).GetProperty(destFieldName);
    if (destPi == null)
    {
        throw new ArgumentException($"{nameof(TDest)}没有{destFieldName}这个属性");
    }
    PropertyInfo srcDestPi = typeof(TSource).GetProperty(newDocFieldName);
    if (srcDestPi == null)
    {
        throw new ArgumentException($"{nameof(TSource)}没有{newDocFieldName}这个属性");
    }
    //查关联数据
    var srcFieldNames = sourcesList.Select(src => srcPi.GetValue(src)).Distinct();
    var filter = Builders<TDest>.Filter.In(destFieldName, srcFieldNames);
    var destList = await destCol.Find(filter).ToListAsync();
    //只包含一个文档
    foreach (var srcItem in sourcesList)
    {
        var tmpDestItem = destList.FirstOrDefault
            (m => destPi?.GetValue(m)?.ToString() == srcPi?.GetValue(srcItem)?.ToString());
        srcDestPi.SetValue(srcItem, tmpDestItem);
    }
    return sourcesList;
}
  • 通过lookup来关联数据,只需查一遍
//通过Aggregate方法查
public Product GetFluentLookup(string id)
{
    //使用Fluent Aggregate Query
    var tmpresults = _productCol.Aggregate()
        .Match(p => p.Id == id)
        .Lookup("manufacturers", "manufacturer"
        , "_id", "Manufacturer")
        //使用Unwind将数组变为文档
        .Unwind("Manufacturer").FirstOrDefault();
    Product products = null;
    products = BsonSerializer.Deserialize<Product>(tmpresults);
    return products;
}
//通过Linq形式的Aggregate查
public Product GetByLinq(string id)
{
    var results = from p in _productCol.AsQueryable()
                  where p.Id == id
                  join m in _manufacturerCol.AsQueryable()
                  on p.ManufacturerId equals m.Id into joined
                  //再from in 关联结果,就会报错,不知为何
                  //from sub_m in joined.DefaultIfEmpty()       
                  //select new { Product = p, sub_m };
                  select new { p, joined };

    List<Product> products = new List<Product>();
    foreach (var p in results)
    {
        Product tmpP = p.p;
        tmpP.Manufacturer = p.joined.FirstOrDefault();
        products.Add(tmpP);
    }
    return products.FirstOrDefault();
}

了解更多,可查阅https://mongodb.github.io/mongo-csharp-driver/2.11/reference/driver/crud/linq/#lookup

六、小结


大概写了下,记录.Net Core如何使用MongoDB数据库,主要是一对多、一对一关联相比SQL麻烦。好像MongoDB不擅长复杂的关系,复杂的多表连接,不过其查询性能比关系数据库好很多。这里只是少部分内容,还有很多要学。

posted @ 2020-11-14 22:12  SuperMutant  阅读(837)  评论(0)    收藏  举报