前言
上一章我們把系統所需要的MongoDB集合設計好了,這一章我們的主要任務是使用.NET Core應用程式連線MongoDB並且封裝MongoDB資料倉儲和工作單元模式,因為本章內容涵蓋的有點多關於倉儲和工作單元的使用就放到下一章節中講解了。倉儲模式(Repository )帶來的好處是一套程式碼可以適用於多個類,把常用的CRUD通用方法抽象出來透過介面形式集中管理,從而解除業務邏輯層與資料訪問層之間的耦合,使業務邏輯層在儲存、訪問資料庫時無須關心資料的來源及儲存方式。工作單元模式(UnitOfWork)它是用來維護一個由已經被業務修改(如增加、刪除和更新等)的業務物件組成的列表,跨多個請求的業務,統一管理事務,統一提交從而保障事物一致性的作用。
MongoDB從入門到實戰的相關教程
MongoDB從入門到實戰之Docker快速安裝MongoDB?
MongoDB從入門到實戰之MongoDB工作常用操作命令?
MongoDB從入門到實戰之.NET Core使用MongoDB開發ToDoList系統(1)-後端專案框架搭建?
MongoDB從入門到實戰之.NET Core使用MongoDB開發ToDoList系統(2)-Swagger框架整合?
MongoDB從入門到實戰之.NET Core使用MongoDB開發ToDoList系統(3)-系統資料集合設計?
MongoDB從入門到實戰之.NET Core使用MongoDB開發ToDoList系統(4)-MongoDB資料倉儲和工作單元模式封裝?
YyFlight.ToDoList專案原始碼地址
歡迎各位看官老爺review,有幫助的別忘了給我個Star哦?!!!
GitHub地址:https://github.com/YSGStudyHards/YyFlight.ToDoList
MongoRepository地址:https://github.com/YSGStudyHards/YyFlight.ToDoList/tree/main/Repository/Repository
MongoDB事務使用前提說明
說明:
MongoDB單機伺服器不支援事務【使用MongoDB事務會報錯:Standalone servers do not support transactions】,只有在叢集情況下才支援事務,因為博主接下來都是在單機環境下操作,所以無法來演示Mongo事務操作,但是方法都已經是封裝好了的,大家可以自己搭建叢集實操。
原因:
MongoDB在使用分散式事務時需要進行多節點之間的協調和通訊,而單機環境下無法實現這樣的分散式協調和通訊機制。但是,在MongoDB部署為一個叢集(cluster)後,將多個計算機連線為一個整體,透過協調和通訊機制實現了分散式事務的正常使用。從資料一致性和可靠性的角度來看,在分散式系統中實現事務處理是至關重要的。而在單機環境下不支援事務,只有在叢集情況下才支援事務的設計方式是為了保證資料一致性和可靠性,並且也符合分散式系統的設計思想。
MongoDB.Driver驅動安裝
1、直接命令自動安裝
Install-Package MongoDB.Driver
2、搜尋Nuget手動安裝
MongoSettings資料庫連線配置
前往appsettings.json檔案中配置Mongo資料庫資訊:
"MongoSettings": { "Connection": "mongodb://root:123456@local:27017/yyflight_todolist?authSource=admin", //MongoDB連線字串 "DatabaseName": "yyflight_todolist" //MongoDB資料庫名稱 }
定義Mongo DBContext上下文
現在我們將定義MongoDB DBContext上下文類,具體到一個業務物件或需要被持久化的物件,這個上下文類將封裝資料庫的連線和集合。
該類應負責建立與所需資料庫的連線,在建立連線後,該類將在記憶體中或按請求持有資料庫上下文(基於API管道中配置的生命週期管理。)
定義IMongoContext介面
public interface IMongoContext : IDisposable { /// <summary> /// 新增命令操作 /// </summary> /// <param name="func"></param> /// <returns></returns> Task AddCommandAsync(Func<Task> func); /// <summary> /// 儲存更改 /// </summary> /// <returns></returns> Task<int> SaveChangesAsync(); /// <summary> /// 獲取集合資料 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="name"></param> /// <returns></returns> IMongoCollection<T> GetCollection<T>(string name); }
定義MongoContext類
public class MongoContext : IMongoContext { private IMongoDatabase _database; private MongoClient _mongoClient; private readonly IConfiguration _configuration; private readonly List<Func<Task>> _commands; public IClientSessionHandle? Session = null; public MongoContext(IConfiguration configuration) { _configuration = configuration; // Every command will be stored and it'll be processed at SaveChanges _commands = new List<Func<Task>>(); // Configure mongo (You can inject the config, just to simplify) _mongoClient = new MongoClient(_configuration["MongoSettings:Connection"]); _database = _mongoClient.GetDatabase(_configuration["MongoSettings:DatabaseName"]); } /// <summary> /// 新增命令操作 /// </summary> /// <param name="func">委託</param> /// <returns></returns> public async Task AddCommandAsync(Func<Task> func) { _commands.Add(func); await Task.CompletedTask; } /// <summary> /// 儲存更改 /// TODO:MongoDB單機伺服器不支援事務【使用MongoDB事務會報錯:Standalone servers do not support transactions】,只有在叢集情況下才支援事務 /// 原因:MongoDB在使用分散式事務時需要進行多節點之間的協調和通訊,而單機環境下無法實現這樣的分散式協調和通訊機制。但是,在MongoDB部署為一個叢集(cluster)後,將多個計算機連線為一個整體,透過協調和通訊機制實現了分散式事務的正常使用。從資料一致性和可靠性的角度來看,在分散式系統中實現事務處理是至關重要的。而在單機環境下不支援事務,只有在叢集情況下才支援事務的設計方式是為了保證資料一致性和可靠性,並且也符合分散式系統的設計思想。 /// </summary> /// <returns></returns> public async Task<int> SaveChangesAsync() { using (Session = await _mongoClient.StartSessionAsync()) { Session.StartTransaction(); var commandTasks = _commands.Select(c => c()); await Task.WhenAll(commandTasks); await Session.CommitTransactionAsync(); } return _commands.Count; } /// <summary> /// 獲取MongoDB集合 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="name">集合名稱</param> /// <returns></returns> public IMongoCollection<T> GetCollection<T>(string name) { return _database.GetCollection<T>(name); } /// <summary> /// 釋放上下文 /// </summary> public void Dispose() { Session?.Dispose(); GC.SuppressFinalize(this); } }
定義通用泛型Repository
Repository(倉儲)是DDD(領域驅動設計)中的經典思想,可以歸納為介於實際業務層(領域層)和資料訪問層之間的層,能讓領域層能在感覺不到資料訪問層的情況下,完成與資料庫的互動和以往的DAO(資料訪問)層相比,Repository層的設計理念更偏向於物件導向,而淡化直接對資料表進行的CRUD操作。
定義IMongoRepository介面
定義一個泛型Repository通用介面,抽象常用的增加,刪除,修改,查詢等操作方法。
public interface IMongoRepository<T> where T : class, new() { #region 事務操作示例 /// <summary> /// 事務新增資料 /// </summary> /// <param name="objData">新增資料</param> /// <returns></returns> Task AddTransactionsAsync(T objData); /// <summary> /// 事務資料刪除 /// </summary> /// <param name="id">objectId</param> /// <returns></returns> Task DeleteTransactionsAsync(string id); /// <summary> /// 事務非同步區域性更新(僅更新一條記錄) /// </summary> /// <param name="filter">過濾器</param> /// <param name="update">更新條件</param> /// <returns></returns> Task UpdateTransactionsAsync(FilterDefinition<T> filter, UpdateDefinition<T> update); #endregion #region 新增相關操作 /// <summary> /// 新增資料 /// </summary> /// <param name="objData">新增資料</param> /// <returns></returns> Task AddAsync(T objData); /// <summary> /// 批次插入 /// </summary> /// <param name="objDatas">實體集合</param> /// <returns></returns> Task InsertManyAsync(List<T> objDatas); #endregion #region 刪除相關操作 /// <summary> /// 資料刪除 /// </summary> /// <param name="id">objectId</param> /// <returns></returns> Task DeleteAsync(string id); /// <summary> /// 非同步刪除多條資料 /// </summary> /// <param name="filter">刪除的條件</param> /// <returns></returns> Task<DeleteResult> DeleteManyAsync(FilterDefinition<T> filter); #endregion #region 修改相關操作 /// <summary> /// 指定物件非同步修改一條資料 /// </summary> /// <param name="obj">要修改的物件</param> /// <param name="id">修改條件</param> /// <returns></returns> Task UpdateAsync(T obj, string id); /// <summary> /// 區域性更新(僅更新一條記錄) /// <para><![CDATA[expression 引數示例:x => x.Id == 1 && x.Age > 18 && x.Gender == 0]]></para> /// <para><![CDATA[entity 引數示例:y => new T{ RealName = "Ray", Gender = 1}]]></para> /// </summary> /// <param name="expression">篩選條件</param> /// <param name="entity">更新條件</param> /// <returns></returns> Task UpdateAsync(Expression<Func<T, bool>> expression, Expression<Action<T>> entity); /// <summary> /// 非同步區域性更新(僅更新一條記錄) /// </summary> /// <param name="filter">過濾器</param> /// <param name="update">更新條件</param> /// <returns></returns> Task UpdateAsync(FilterDefinition<T> filter, UpdateDefinition<T> update); /// <summary> /// 非同步區域性更新(僅更新多條記錄) /// </summary> /// <param name="expression">篩選條件</param> /// <param name="update">更新條件</param> /// <returns></returns> Task UpdateManyAsync(Expression<Func<T, bool>> expression, UpdateDefinition<T> update); /// <summary> /// 非同步批次修改資料 /// </summary> /// <param name="dic">要修改的欄位</param> /// <param name="filter">更新條件</param> /// <returns></returns> Task<UpdateResult> UpdateManayAsync(Dictionary<string, string> dic, FilterDefinition<T> filter); #endregion #region 查詢統計相關操作 /// <summary> /// 透過ID主鍵獲取資料 /// </summary> /// <param name="id">objectId</param> /// <returns></returns> Task<T> GetByIdAsync(string id); /// <summary> /// 獲取所有資料 /// </summary> /// <returns></returns> Task<IEnumerable<T>> GetAllAsync(); /// <summary> /// 獲取記錄數 /// </summary> /// <param name="expression">篩選條件</param> /// <returns></returns> Task<long> CountAsync(Expression<Func<T, bool>> expression); /// <summary> /// 獲取記錄數 /// </summary> /// <param name="filter">過濾器</param> /// <returns></returns> Task<long> CountAsync(FilterDefinition<T> filter); /// <summary> /// 判斷是否存在 /// </summary> /// <param name="predicate">條件</param> /// <returns></returns> Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate); /// <summary> /// 非同步查詢集合 /// </summary> /// <param name="filter">查詢條件</param> /// <param name="field">要查詢的欄位,不寫時查詢全部</param> /// <param name="sort">要排序的欄位</param> /// <returns></returns> Task<List<T>> FindListAsync(FilterDefinition<T> filter, string[]? field = null, SortDefinition<T>? sort = null); /// <summary> /// 非同步分頁查詢集合 /// </summary> /// <param name="filter">查詢條件</param> /// <param name="pageIndex">當前頁</param> /// <param name="pageSize">頁容量</param> /// <param name="field">要查詢的欄位,不寫時查詢全部</param> /// <param name="sort">要排序的欄位</param> /// <returns></returns> Task<List<T>> FindListByPageAsync(FilterDefinition<T> filter, int pageIndex, int pageSize, string[]? field = null, SortDefinition<T>? sort = null); #endregion }
實現泛型MongoBaseRepository基類
public class MongoBaseRepository<T> : IMongoRepository<T> where T : class, new() { protected readonly IMongoContext _context; protected readonly IMongoCollection<T> _dbSet; private readonly string _collectionName; protected MongoBaseRepository(IMongoContext context) { _context = context; _collectionName = typeof(T).GetAttributeValue((TableAttribute m) => m.Name) ?? typeof(T).Name; _dbSet = _context.GetCollection<T>(_collectionName); } #region 事務操作示例 /// <summary> /// 事務新增資料 /// </summary> /// <param name="objData">新增資料</param> /// <returns></returns> public async Task AddTransactionsAsync(T objData) { await _context.AddCommandAsync(async () => await _dbSet.InsertOneAsync(objData)); } /// <summary> /// 事務資料刪除 /// </summary> /// <param name="id">objectId</param> /// <returns></returns> public async Task DeleteTransactionsAsync(string id) { await _context.AddCommandAsync(() => _dbSet.DeleteOneAsync(Builders<T>.Filter.Eq(" _id ", id))); } /// <summary> /// 事務非同步區域性更新(僅更新一條記錄) /// </summary> /// <param name="filter">過濾器</param> /// <param name="update">更新條件</param> /// <returns></returns> public async Task UpdateTransactionsAsync(FilterDefinition<T> filter, UpdateDefinition<T> update) { await _context.AddCommandAsync(() => _dbSet.UpdateOneAsync(filter, update)); } #endregion #region 新增相關操作 /// <summary> /// 新增資料 /// </summary> /// <param name="objData">新增資料</param> /// <returns></returns> public async Task AddAsync(T objData) { await _dbSet.InsertOneAsync(objData); } /// <summary> /// 批次插入 /// </summary> /// <param name="objDatas">實體集合</param> /// <returns></returns> public async Task InsertManyAsync(List<T> objDatas) { await _dbSet.InsertManyAsync(objDatas); } #endregion #region 刪除相關操作 /// <summary> /// 資料刪除 /// </summary> /// <param name="id">objectId</param> /// <returns></returns> public async Task DeleteAsync(string id) { await _dbSet.DeleteOneAsync(Builders<T>.Filter.Eq("_id", new ObjectId(id))); } /// <summary> /// 非同步刪除多條資料 /// </summary> /// <param name="filter">刪除的條件</param> /// <returns></returns> public async Task<DeleteResult> DeleteManyAsync(FilterDefinition<T> filter) { return await _dbSet.DeleteManyAsync(filter); } #endregion #region 修改相關操作 /// <summary> /// 指定物件非同步修改一條資料 /// </summary> /// <param name="obj">要修改的物件</param> /// <param name="id">修改條件</param> /// <returns></returns> public async Task UpdateAsync(T obj, string id) { //修改條件 FilterDefinition<T> filter = Builders<T>.Filter.Eq("_id", new ObjectId(id)); //要修改的欄位 var list = new List<UpdateDefinition<T>>(); foreach (var item in obj.GetType().GetProperties()) { if (item.Name.ToLower() == "id") continue; list.Add(Builders<T>.Update.Set(item.Name, item.GetValue(obj))); } var updatefilter = Builders<T>.Update.Combine(list); await _dbSet.UpdateOneAsync(filter, updatefilter); } /// <summary> /// 區域性更新(僅更新一條記錄) /// <para><![CDATA[expression 引數示例:x => x.Id == 1 && x.Age > 18 && x.Gender == 0]]></para> /// <para><![CDATA[entity 引數示例:y => new T{ RealName = "Ray", Gender = 1}]]></para> /// </summary> /// <param name="expression">篩選條件</param> /// <param name="entity">更新條件</param> /// <returns></returns> public async Task UpdateAsync(Expression<Func<T, bool>> expression, Expression<Action<T>> entity) { var fieldList = new List<UpdateDefinition<T>>(); if (entity.Body is MemberInitExpression param) { foreach (var item in param.Bindings) { var propertyName = item.Member.Name; object propertyValue = null; if (item is not MemberAssignment memberAssignment) continue; if (memberAssignment.Expression.NodeType == ExpressionType.Constant) { if (memberAssignment.Expression is ConstantExpression constantExpression) propertyValue = constantExpression.Value; } else { propertyValue = Expression.Lambda(memberAssignment.Expression, null).Compile().DynamicInvoke(); } if (propertyName != "_id") //實體鍵_id不允許更新 { fieldList.Add(Builders<T>.Update.Set(propertyName, propertyValue)); } } } await _dbSet.UpdateOneAsync(expression, Builders<T>.Update.Combine(fieldList)); } /// <summary> /// 非同步區域性更新(僅更新一條記錄) /// </summary> /// <param name="filter">過濾器</param> /// <param name="update">更新條件</param> /// <returns></returns> public async Task UpdateAsync(FilterDefinition<T> filter, UpdateDefinition<T> update) { await _dbSet.UpdateOneAsync(filter, update); } /// <summary> /// 非同步區域性更新(僅更新多條記錄) /// </summary> /// <param name="expression">篩選條件</param> /// <param name="update">更新條件</param> /// <returns></returns> public async Task UpdateManyAsync(Expression<Func<T, bool>> expression, UpdateDefinition<T> update) { await _dbSet.UpdateManyAsync(expression, update); } /// <summary> /// 非同步批次修改資料 /// </summary> /// <param name="dic">要修改的欄位</param> /// <param name="filter">更新條件</param> /// <returns></returns> public async Task<UpdateResult> UpdateManayAsync(Dictionary<string, string> dic, FilterDefinition<T> filter) { T t = new T(); //要修改的欄位 var list = new List<UpdateDefinition<T>>(); foreach (var item in t.GetType().GetProperties()) { if (!dic.ContainsKey(item.Name)) continue; var value = dic[item.Name]; list.Add(Builders<T>.Update.Set(item.Name, value)); } var updatefilter = Builders<T>.Update.Combine(list); return await _dbSet.UpdateManyAsync(filter, updatefilter); } #endregion #region 查詢統計相關操作 /// <summary> /// 透過ID主鍵獲取資料 /// </summary> /// <param name="id">objectId</param> /// <returns></returns> public async Task<T> GetByIdAsync(string id) { var queryData = await _dbSet.FindAsync(Builders<T>.Filter.Eq("_id", new ObjectId(id))); return queryData.FirstOrDefault(); } /// <summary> /// 獲取所有資料 /// </summary> /// <returns></returns> public async Task<IEnumerable<T>> GetAllAsync() { var queryAllData = await _dbSet.FindAsync(Builders<T>.Filter.Empty); return queryAllData.ToList(); } /// <summary> /// 獲取記錄數 /// </summary> /// <param name="expression">篩選條件</param> /// <returns></returns> public async Task<long> CountAsync(Expression<Func<T, bool>> expression) { return await _dbSet.CountDocumentsAsync(expression); } /// <summary> /// 獲取記錄數 /// </summary> /// <param name="filter">過濾器</param> /// <returns></returns> public async Task<long> CountAsync(FilterDefinition<T> filter) { return await _dbSet.CountDocumentsAsync(filter); } /// <summary> /// 判斷是否存在 /// </summary> /// <param name="predicate">條件</param> /// <returns></returns> public async Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate) { return await Task.FromResult(_dbSet.AsQueryable().Any(predicate)); } /// <summary> /// 非同步查詢集合 /// </summary> /// <param name="filter">查詢條件</param> /// <param name="field">要查詢的欄位,不寫時查詢全部</param> /// <param name="sort">要排序的欄位</param> /// <returns></returns> public async Task<List<T>> FindListAsync(FilterDefinition<T> filter, string[]? field = null, SortDefinition<T>? sort = null) { //不指定查詢欄位 if (field == null || field.Length == 0) { if (sort == null) return await _dbSet.Find(filter).ToListAsync(); return await _dbSet.Find(filter).Sort(sort).ToListAsync(); } //指定查詢欄位 var fieldList = new List<ProjectionDefinition<T>>(); for (int i = 0; i < field.Length; i++) { fieldList.Add(Builders<T>.Projection.Include(field[i].ToString())); } var projection = Builders<T>.Projection.Combine(fieldList); fieldList?.Clear(); //不排序 if (sort == null) return await _dbSet.Find(filter).Project<T>(projection).ToListAsync(); //排序查詢 return await _dbSet.Find(filter).Sort(sort).Project<T>(projection).ToListAsync(); } /// <summary> /// 非同步分頁查詢集合 /// </summary> /// <param name="filter">查詢條件</param> /// <param name="pageIndex">當前頁</param> /// <param name="pageSize">頁容量</param> /// <param name="field">要查詢的欄位,不寫時查詢全部</param> /// <param name="sort">要排序的欄位</param> /// <returns></returns> public async Task<List<T>> FindListByPageAsync(FilterDefinition<T> filter, int pageIndex, int pageSize, string[]? field = null, SortDefinition<T>? sort = null) { //不指定查詢欄位 if (field == null || field.Length == 0) { if (sort == null) return await _dbSet.Find(filter).Skip((pageIndex - 1) * pageSize).Limit(pageSize).ToListAsync(); //進行排序 return await _dbSet.Find(filter).Sort(sort).Skip((pageIndex - 1) * pageSize).Limit(pageSize).ToListAsync(); } //指定查詢欄位 var fieldList = new List<ProjectionDefinition<T>>(); for (int i = 0; i < field.Length; i++) { fieldList.Add(Builders<T>.Projection.Include(field[i].ToString())); } var projection = Builders<T>.Projection.Combine(fieldList); fieldList?.Clear(); //不排序 if (sort == null) return await _dbSet.Find(filter).Project<T>(projection).Skip((pageIndex - 1) * pageSize).Limit(pageSize).ToListAsync(); //排序查詢 return await _dbSet.Find(filter).Sort(sort).Project<T>(projection).Skip((pageIndex - 1) * pageSize).Limit(pageSize).ToListAsync(); } #endregion }
工作單元模式
工作單元模式是“維護一個被業務事務影響的物件列表,協調變化的寫入和併發問題的解決”。具體來說,在C#工作單元模式中,我們透過UnitOfWork物件來管理多個Repository物件,同時UnitOfWork還提供了對事務的支援。對於一組需要用到多個Repository的業務操作,我們可以在UnitOfWork中建立一個事務,並將多個Repository操作放在同一個事務中處理,以保證資料的一致性。當所有Repository操作完成後,再透過UnitOfWork提交事務或者回滾事務。
定義IUnitOfWork介面
/// <summary> /// 工作單元介面 /// </summary> public interface IUnitOfWork : IDisposable { /// <summary> /// 提交儲存更改 /// </summary> /// <returns></returns> Task<bool> Commit(); }
定義UnitOfWork類
/// <summary> /// 工作單元類 /// </summary> public class UnitOfWork : IUnitOfWork { private readonly IMongoContext _context; public UnitOfWork(IMongoContext context) { _context = context; } /// <summary> /// 提交儲存更改 /// </summary> /// <returns></returns> public async Task<bool> Commit() { return await _context.SaveChangesAsync() > 0; } public void Dispose() { _context.Dispose(); } }
註冊資料庫基礎操作和工作單元
//註冊資料庫基礎操作和工作單元 builder.Services.AddScoped<IMongoContext, MongoContext>(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
參考文章
NoSQL – MongoDB Repository Implementation in .NET Core with Unit Testing example
ASP.NET CORE – MONGODB REPOSITORY PATTERN & UNIT OF WORK