背景 最近公司系統還原用戶時偶爾會出現部分用戶信息未還原成功的問題,最為開發人員,最頭疼的不是代碼存在bug,而是測試發現了bug,但一旦我去重現,它就不見了。Are you kidding me? 經過漫長的溝通與嘗試,終於發現了端倪,這個問題只有在多人同時操作修改同一用戶信息時才會出現。 哦,那 ...
背景
最近公司系統還原用戶時偶爾會出現部分用戶信息未還原成功的問題,作為開發人員,最頭疼的不是代碼存在bug,而是測試發現了bug,但一旦我去重現,它就不見了。Are you kidding me?
經過漫長的溝通與嘗試,終於發現了端倪,這個問題只有在多人同時操作修改同一用戶信息時才會出現。
哦,那你死定了,小bug。
分析
經過短暫的代碼review,發現還原用戶時,代碼中會先把用戶獲取出來,然後修改用戶信息,最後再將修改後的用戶更新至資料庫中。
var user1 = collection.Find(x => x.Id == "user1").FirstOrDefault(); user1.Name = "B"; collection.ReplaceOneAsync(x => x.Id == "user1", user1);
這就導致代碼併發運行時,後面的可能覆蓋前方的操作。
如下圖操作1及操作2執行結束後,資料庫中用戶1的名稱為A,年齡為18;操作1的修改用戶名稱為B被覆蓋.
所以我們需要採用原子操作來修改用戶信息,我們調整代碼如下
UpdateDefinition<Persion> update = Builders<Persion>.Update.Set(y => y.Name, "B"); collection.UpdateOne(x => x.Id == "user1", update);
這樣就把修改操作交給資料庫來執行,僅修改要修改的屬性,避免操作互相影響;
看到這裡有的大神就會吐槽,這麼簡單的東西也好意思拿來說,請在耐心往下看,重點在下邊。
重點
如果我們的數據結構類似這樣:
/// <summary> /// 人 /// </summary> [BsonIgnoreExtraElements] public class Persion : BaseEntity { /// <summary> /// 名稱 /// </summary> [BsonElement("name")] public string Name { get; set; } /// <summary> /// 年齡 /// </summary> [BsonElement("age")] public int Age { get; set; } /// <summary> /// 親戚 /// </summary> [BsonElement("relatives")] public List<Relative> Relatives { get; set; } }
/// <summary> /// 親屬 /// </summary> public class Relative { /// <summary> /// 名稱 /// </summary> [BsonElement("name")] public string Name { get; set; } /// <summary> /// 與本人關係 /// </summary> [BsonElement("relationship")] public Relationship Relationship { get; set; } }
/// <summary> /// 與本人關係 /// </summary> public enum Relationship { /// <summary> /// 爸爸 /// </summary> father = 0, /// <summary> /// 媽媽 /// </summary> mother = 1, /// <summary> /// 兒子 /// </summary> son = 2, /// <summary> /// 女兒 /// </summary> daughter = 3, /// <summary> /// 不明 /// </summary> unknow = 100 }
如果我們想更新名稱為“趙小明”的人的名稱為“趙剛”的親戚與本人關係為“爸爸”,請考慮,應該怎麼處理。
註意:人的親戚可以有多個,所以是List。
這時我們就用到了ArrayFilters對象,其存在於UpdateOptions中,如果沒有系統的看過MongoDB的介面,我相信大部分人都會忽略它。
好了,廢話不多說,讓我們來看看它的用法吧
FilterDefinition<Persion> filter = Builders<Persion>.Filter.Where(x => x.Name == "趙小明" && x.Relatives != null && x.Relatives.Count > 0); UpdateDefinition<Persion> update = Builders<Persion>.Update.Set("relatives.$[i].relationship", Relationship.father); var option = new UpdateOptions() { ArrayFilters = new List<ArrayFilterDefinition> { new JsonArrayFilterDefinition<Relationship>("{'i.name': '趙剛'}") } }; collection.UpdateMany(filter, update, option);
可以看到,我們先生成一個查詢條件,名稱為“趙小明”,存在親戚的人;
然後更新其"relatives.$[i].relationship"屬性,為Relationship.father,其中的$[i]為占位符;
再生成一個決定$[i]值的JsonArrayFilterDefinition<Relationship>("{'i.name': '趙剛'}");
最後用這些條件來更新資料庫。
引申
好了,更新是實現了,那有求知欲的小伙伴就會想查詢怎麼辦呢?
這還不簡單,一行語句就搞定了
var user = collection.Find(x => x.Name == "趙小明" && x.Relatives != null && x.Relatives.Count > 0 && x.Relatives.Exists(y => y.Name == "趙剛")).FirstOrDefault();
沒錯,但如果此人存在幾千萬個親戚(現實生活中怎麼可能,笑),我只需要其與一個名為“趙剛”的親戚的關係,不想把整個對象都載入到記憶體中怎麼辦?
這時我們就需要用到ProjectionDefinitionBuilder對象了,
FilterDefinition<Persion> filter = Builders<Persion>.Filter.Where(x => x.Name == "趙小明" && x.Relatives != null && x.Relatives.Count > 0); var findOptions = new FindOptions<Persion, Relative>() { Projection = new ProjectionDefinitionBuilder<Persion>().Expression(x => x.Relatives.FirstOrDefault(r => r.Name != "趙剛")) }; Relative cursor = collection.FindSync(filter, findOptions).FirstOrDefault();
我們就得到了我們想要的親戚對象,而不是包含幾千萬親戚信息的完整Persion對象了。
結語
作為一名博客萌新,我只是將我遇到的問題總結下來並分享給大家,有不對的地方,務必幫忙指正。
當然上面的內容對於大佬來說可能是常規操作,但如果對你有一點點用處,請點贊,評論,並關註下。
後面我會將我在工作學習中遇到的有趣的問題分享給大家,謝謝!!!