用過 mongodb 吧, 這三個大坑踩過嗎?

一:背景

1. 講故事

前段時間有位朋友在微信羣問,在向 mongodb 中插入的時間爲啥取出來的時候少了 8 個小時,8 在時間處理上是一個很是敏感的數字,又吉利又是一個普適的話題,後來我想一想初次使用 mongodb 的朋友必定還會遇到各類新坑,好比說: 插入的數據取不出來,看不爽的 ObjectID,時區不對等等,這篇就和你們一塊兒聊一聊。算法

二: 1號坑 插進去的數據取不出來

1. 案例展現

這個問題是使用強類型操做 mongodb 你必定會遇到的問題,案例代碼以下:mongodb

class Program
    {
        static void Main(string[] args)
        {
            var client = new MongoClient("mongodb://192.168.1.128:27017");
            var database = client.GetDatabase("school");
            var table = database.GetCollection<Student>("student");

            table.InsertOne(new Student() { StudentName = "hxc", Created = DateTime.Now });

            var query = table.AsQueryable().ToList();

        }
    }

    public class Student
    {
        public string StudentName { get; set; }

        public DateTime Created { get; set; }
    }

我去,這麼簡單的一個操做還報錯,要初學到放棄嗎? 挺急的,在線等!express

2. 堆棧中深挖源碼

做爲一個碼農還得有鑽研代碼的能力,從錯誤信息中看說有一個 _id 不匹配 student 中的任何一個字段,而後把所有堆棧找出來。json

System.FormatException
  HResult=0x80131537
  Message=Element '_id' does not match any field or property of class Newtonsoft.Test.Student.
  Source=MongoDB.Driver
  StackTrace:
   at MongoDB.Driver.Linq.MongoQueryProviderImpl`1.Execute(Expression expression)
   at MongoDB.Driver.Linq.MongoQueryableImpl`2.GetEnumerator()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Newtonsoft.Test.Program.Main(String[] args) in E:\crm\JsonNet\Newtonsoft.Test\Program.cs:line 32

接下來就用 dnspy 去定位一下 MongoQueryProviderImpl.Execute 到底乾的啥,截圖以下:微信

我去,這代碼硬核哈,用了 LambdaExpression 表達式樹,咱們知道表達式樹用於將一個領域的查詢結構轉換爲另外一個領域的查詢結構,但要尋找如何構建這個方法體就比較耗時間了,接下來仍是用 dnspy 去調試看看有沒有更深層次的堆棧。ide

這個堆棧信息就很是清楚了,原來是在 MongoDB.Bson.Serialization.BsonClassMapSerializer.DeserializeClass 方法中出了問題,接下來找到問題代碼,簡化以下:性能

public TClass DeserializeClass(BsonDeserializationContext context)
{
    while (reader.ReadBsonType() != BsonType.EndOfDocument)
    {
        TrieNameDecoder<int> trieNameDecoder = new TrieNameDecoder<int>(elementTrie);
        string text = reader.ReadName(trieNameDecoder);
        if (trieNameDecoder.Found)
        {
            int value = trieNameDecoder.Value;
            BsonMemberMap bsonMemberMap = allMemberMaps[value];
        }
        else
        {
            if (!this._classMap.IgnoreExtraElements)
            {
                throw new FormatException(string.Format("Element '{0}' does not match any field or property of class {1}.", text, this._classMap.ClassType.FullName));
            }
            reader.SkipValue();
        }
    }
}

上面的代碼邏輯很是清楚,要麼 student 中存在 _id 字段,也就是 trieNameDecoder.Found, 要麼使用 忽略未知的元素,也就是 this._classMap.IgnoreExtraElements,添加字段容易,接下來看看怎麼讓 IgnoreExtraElements = true,找了一圈源碼,發現這裏是關鍵:this

也就是: foreach (IBsonClassMapAttribute bsonClassMapAttribute in classMap.ClassType.GetTypeInfo().GetCustomAttributes(false).OfType<IBsonClassMapAttribute>())這句話,這裏的 classMap 就是 student,只有讓 foreach 得以執行纔能有望 classMap.IgnoreExtraElements 賦值爲 true ,接下來找找看在類上有沒有相似 IgnoreExtraElements 的 Attribute,嘿嘿,還真有一個相似的: BsonIgnoreExtraElements ,以下代碼:spa

[BsonIgnoreExtraElements]
    public class Student
    {
        public string StudentName { get; set; }

        public DateTime Created { get; set; }
    }

接下來執行一下代碼,能夠看到問題搞定:3d

若是你想驗證的話,能夠繼續用 dnspy 去驗證一下源碼哈,以下代碼所示:

接下來還有一種辦法就是增長 _id 字段,若是你不知道用什麼類型接,那就用object就好啦,後續再改爲真正的類型。

三: 2號坑 DateTime 時區不對

若是你細心的話,你會發現剛纔案例中的 Created 時間是 2020/8/16 4:24:57, 你們請放心,我不會傻到凌晨4點還在寫代碼,好了哈,看看到底問題在哪吧, 能夠先看看 mongodb 中的記錄數據,以下:

{
    "_id" : ObjectId("5f38b83e0351908eedac60c9"),
    "StudentName" : "hxc",
    "Created" : ISODate("2020-08-16T04:38:22.587Z")
}

從 ISODate 能夠看出,這是格林威治時間,按照0時區存儲,因此這個問題轉成了如何在獲取數據的時候,自動將 ISO 時間轉成 Local 時間就能夠了,若是你看過底層源碼,你會發如今 mongodb 中每一個實體的每一個類型都有一個專門的 XXXSerializer,以下圖:

接下來就好好研讀一下里面的 Deserialize 方法便可,代碼精簡後以下:

public override DateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
    IBsonReader bsonReader = context.Reader;
    BsonType currentBsonType = bsonReader.GetCurrentBsonType();
    DateTime value;
    
    switch (this._kind)
    {
        case DateTimeKind.Unspecified:
        case DateTimeKind.Local:
            value = DateTime.SpecifyKind(BsonUtils.ToLocalTime(value), this._kind);
            break;
        case DateTimeKind.Utc:
            value = BsonUtils.ToUniversalTime(value);
            break;
    }
    return value;
}

能夠看出,若是當前的 this._kind= DateTimeKind.Local 的話,就將 UTC 時間轉成 Local 時間,若是你有上一個坑的經驗,你大概就知道應該也是用特性注入的,

[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
    public DateTime Created { get; set; }

不信的話,我調試給你看看哈。

接下來再看看 this._kind 是怎麼被賦的。

四: 3號坑 自定義ObjectID

在第一個坑中,不知道你們看沒看到相似這樣的語句: ObjectId("5f38b83e0351908eedac60c9") ,乍一看像是一個 GUID,固然確定不是,這是mongodb本身組建了一個 number 組合的十六進制表示,姑且不說性能如何,反正看着不是很舒服,畢竟你們都習慣使用 int/long 類型展現的主鍵ID。

那接下來的問題是:如何改爲我自定義的 number ID 呢? 固然能夠,只要實現 IIdGenerator 接口便可,那主鍵ID的生成,我準備用 雪花算法,完整代碼以下:

class Program
    {
        static void Main(string[] args)
        {
            var client = new MongoClient("mongodb://192.168.1.128:27017");
            var database = client.GetDatabase("school");
            var table = database.GetCollection<Student>("student");

            table.InsertOne(new Student() { Created = DateTime.Now });
            table.InsertOne(new Student() { Created = DateTime.Now });
        }
    }

    class Student
    {
        [BsonId(IdGenerator = typeof(MyGenerator))]
        public long ID { get; set; }

        [BsonDateTimeOptions(Kind = DateTimeKind.Local)]
        public DateTime Created { get; set; }
    }

    public class MyGenerator : IIdGenerator
    {
        private static readonly IdWorker worker = new IdWorker(1, 1);

        public object GenerateId(object container, object document)
        {
            return worker.NextId();
        }

        public bool IsEmpty(object id)
        {
            return id == null || Convert.ToInt64(id) == 0;
        }
    }

而後去看一下 mongodb 生成的 json:

四: 總結

好了,這三個坑,我想不少剛接觸 mongodb 的朋友是必定會遇到的困惑,總結一下方便後人乘涼,結果不重要,重要的仍是探索問題的思路和不擇手段😄😄😄。

相關文章
相關標籤/搜索