0x00 前言
之前一直使用的是 EF ,做了一個簡單的小項目后發現 EF 的表現並不是很好,就比如聯表查詢,因為現在的 EF Core 也沒有啥好用的分析工具,所以也不知道該怎么寫 Linq 生成出來的 Sql 效率比較高,於是這次的期末大作業決定使用性能強勁、輕便小巧的 ORM —— Dapper。
0x01 Repository 模式
Repository 模式幾乎出現在所有的 asp.net 樣例中,主要的作用是給業務層提供數據訪問的能力,與 DAL 的區別就在於:
Repository模式:
Repository 是DDD中的概念,強調 Repository 是受 Domain 驅動的, Repository 中定義的功能要體現 Domain 的意圖和約束,而 Dal 更純粹的就是提供數據訪問的功能,並不嚴格受限於 Business 層。使用 Repository ,隱含着一種意圖傾向,就是 Domain 需要什么我才提供什么,不該提供的功能就不要提供,一切都是以 Domain 的需求為核心。
而使用Dal,其意圖傾向在於我 Dal 層能使用的數據庫訪問操作提供給 Business 層,你 Business 要用哪個自己選.換一個 Business 也可以用我這個 Dal,一切是以我 Dal 能提供什么操作為核心.
0x02 TDD(測試驅動開發)
TDD 的基本思路就是通過測試來推動整個開發的進行。而測試驅動開發技術並不只是單純的測試工作。
在我看來,TDD 的實施可以帶來以下的好處:
- 在一個接口尚未完全確定的時候,通過編寫測試用例,可以幫助我們更好的描述接口的行為,幫助我們更好的了解抽象的需求。
- 編寫測試用例的過程能夠促使我們將功能分解開,做出“高內聚,低耦合”的設計,因此,TDD 也是我們設計高可復用性的代碼的過程。
- 編寫測試用例也是對接口調用方法最詳細的描述,Documation is cheap, show me the examples。測試用例代碼比詳盡的文檔不知道高到哪里去了。
- 測試用例還能夠盡早的幫助我們發現代碼的錯誤,每當代碼發生了修改,可以方便的幫助我們驗證所做的修改對已經有效的功能是否有影響,從而使我們能夠更快的發現並定位 bug。
0x03 建模
在期末作業的系統中,需要實現一個站內通知的功能,首先,讓我們來簡單的建個模:
然后,依照這個模型,我創建好了對應的實體與接口:

1 public interface IInsiteMsgService 2 { 3 /// <summary> 4 /// 給一組用戶發送指定的站內消息 5 /// </summary> 6 /// <param name="msgs">站內消息數組</param> 7 Task SentMsgsAsync(IEnumerable<InsiteMsg> msgs); 8 9 /// <summary> 10 /// 發送一條消息給指定的用戶 11 /// </summary> 12 /// <param name="msg">站內消息</param> 13 void SentMsg(InsiteMsg msg); 14 15 /// <summary> 16 /// 將指定的消息設置為已讀 17 /// </summary> 18 /// <param name="msgIdRecordIds">用戶消息記錄的 Id</param> 19 void ReadMsg(IEnumerable<int> msgIdRecordIds); 20 21 /// <summary> 22 /// 獲取指定用戶的所有的站內消息,包括已讀與未讀 23 /// </summary> 24 /// <param name="userId">用戶 Id</param> 25 /// <returns></returns> 26 IEnumerable<InsiteMsg> GetInbox(int userId); 27 28 /// <summary> 29 /// 刪除指定用戶的一些消息記錄 30 /// </summary> 31 /// <param name="userId">用戶 Id</param> 32 /// <param name="insiteMsgIds">用戶消息記錄 Id</param> 33 void DeleteMsgRecord(int userId, IEnumerable<int> insiteMsgIds); 34 }
InsiteMessage
實體:

1 public class InsiteMsg 2 { 3 public int InsiteMsgId { get; set; } 4 /// <summary> 5 /// 消息發送時間 6 /// </summary> 7 public DateTime SentTime { get; set; } 8 9 /// <summary> 10 /// 消息閱讀時間,null 說明消息未讀 11 /// </summary> 12 public DateTime? ReadTime { get; set; } 13 14 public int UserId { get; set; } 15 16 /// <summary> 17 /// 消息內容 18 /// </summary> 19 [MaxLength(200)] 20 public string Content { get; set; } 21 22 public bool Status { get; set; } 23 }
建立測試
接下來,建立測試用例,來描述 Service 每個方法的行為,這里以 SentMsgsAsync
舉例:
- 消息的狀態如果是 false ,則引發
ArgumentException
,且不會被持久化 - 消息的內容如果是空的,則引發
ArgumentException
,且不會被持久化
根據上面的約束,測試用例代碼也就出來了

1 public class InsiteMsgServiceTests 2 { 3 /// <summary> 4 /// 消息發送成功,添加到數據庫 5 /// </summary> 6 [Fact] 7 public void SentMsgTest() 8 { 9 //Mock repository 10 List<InsiteMsg> dataSet = new List<InsiteMsg>(); 11 12 var msgRepoMock = new Mock<IInsiteMsgRepository>(); 13 msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) => 14 { 15 dataSet.AddRange(m); 16 }); 17 18 //Arrange 19 IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object); 20 21 var msgs = new List<InsiteMsg> 22 { 23 new InsiteMsg { Content="fuck", Status=true, UserId=123 }, 24 new InsiteMsg { Content="fuck", Status=true, UserId=123 }, 25 new InsiteMsg { Content="fuck", Status=true, UserId=123 }, 26 new InsiteMsg { Content="fuck", Status=true, UserId=123 }, 27 }; 28 29 //action 30 msgService.SentMsgsAsync(msgs); 31 32 dataSet.Should().BeEquivalentTo(msgs); 33 } 34 35 /// <summary> 36 /// 消息的狀態如果是 false ,則引發 <see cref="ArgumentException"/>,且不會被持久化 37 /// </summary> 38 [Fact] 39 public void SentMsgWithFalseStatusTest() 40 { 41 //Mock repository 42 List<InsiteMsg> dataSet = new List<InsiteMsg>(); 43 var msgRepoMock = new Mock<IInsiteMsgRepository>(); 44 msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) => 45 { 46 dataSet.AddRange(m); 47 }); 48 49 IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object); 50 51 List<InsiteMsg> msgs = new List<InsiteMsg> 52 { 53 new InsiteMsg { Status = false, Content = "fuck" }, 54 new InsiteMsg { Status = true, Content = "fuck" } 55 }; 56 57 var exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs)); 58 exception?.Result.Should().NotBeNull(); 59 Assert.IsType<ArgumentException>(exception.Result); 60 dataSet.Count.Should().Equals(0); 61 } 62 63 /// <summary> 64 /// 消息的內容如果是空的,則引發 <see cref="ArgumentException"/>,且不會被持久化 65 /// </summary> 66 [Fact] 67 public void SentMsgWithEmptyContentTest() 68 { 69 //Mock repository 70 List<InsiteMsg> dataSet = new List<InsiteMsg>(); 71 var msgRepoMock = new Mock<IInsiteMsgRepository>(); 72 msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) => 73 { 74 dataSet.AddRange(m); 75 }); 76 77 78 IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object); 79 80 List<InsiteMsg> msgs = new List<InsiteMsg> 81 { 82 new InsiteMsg { Status = true, Content = "" }// empty 83 }; 84 85 var exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs)); 86 exception?.Result.Should().NotBeNull(because: "消息內容是空字符串"); 87 Assert.IsType<ArgumentException>(exception.Result); 88 dataSet.Count.Should().Equals(0); 89 90 msgs = new List<InsiteMsg> 91 { 92 new InsiteMsg { Status = true, Content = " " }// space only 93 }; 94 95 exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs)); 96 exception?.Result.Should().NotBeNull(because: "消息內容只包含空格"); 97 Assert.IsType<ArgumentException>(exception.Result); 98 dataSet.Count.Should().Equals(0); 99 100 msgs = new List<InsiteMsg> 101 { 102 new InsiteMsg { Status = true, Content = null }// null 103 }; 104 105 exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs)); 106 exception?.Result.Should().NotBeNull(because: "消息內容是 null"); 107 Assert.IsType<ArgumentException>(exception.Result); 108 dataSet.Count.Should().Equals(0); 109 } 110 }
實現接口以通過測試

1 namespace Hive.Domain.Services.Concretes 2 { 3 public class InsiteMsgService : IInsiteMsgService 4 { 5 private readonly IInsiteMsgRepository _msgRepo; 6 7 public InsiteMsgService(IInsiteMsgRepository msgRepo) 8 { 9 _msgRepo = msgRepo; 10 } 11 12 13 public async Task SentMsgsAsync(IEnumerable<InsiteMsg> msgs) 14 { 15 foreach (InsiteMsg msg in msgs) 16 { 17 if (!msg.Status || string.IsNullOrWhiteSpace(msg.Content)) 18 { 19 throw new ArgumentException("不能將無效的消息插入", nameof(msgs)); 20 } 21 msg.SentTime = DateTime.Now; 22 msg.ReadTime = null; 23 } 24 await _msgRepo.InsertAsync(msgs); 25 } 26 27 public void SentMsg(InsiteMsg msg) 28 { 29 if (!msg.Status || string.IsNullOrWhiteSpace(msg.Content)) 30 { 31 throw new ArgumentException("不能將無效的消息插入", nameof(msg)); 32 } 33 msg.SentTime = DateTime.Now; 34 msg.ReadTime = null; 35 _msgRepo.Insert(msg); 36 } 37 38 public void ReadMsg(IEnumerable<int> msgs, int userId) 39 { 40 var ids = msgs.Distinct(); 41 _msgRepo.UpdateReadTime(ids, userId); 42 } 43 44 public async Task<IEnumerable<InsiteMsg>> GetInboxAsync(int userId) 45 { 46 return await _msgRepo.GetByUserIdAsync(userId); 47 } 48 49 public void DeleteMsgRecord(int userId, IEnumerable<int> insiteMsgIds) 50 { 51 _msgRepo.DeleteMsgRecoreds(userId, insiteMsgIds.Distinct()); 52 } 53 } 54 }
上面的一些代碼很明了,就懶得逐塊注釋了,函數注釋足矣~
驗證測試
測試當然全部通過啦,這里就不放圖了
為了將數據訪問與邏輯代碼分離,這里我使用了 Repository
模式—— IInsiteMsgRepository
,下面給出這個接口的定義:

1 namespace Hive.Domain.Repositories.Abstracts 2 { 3 public interface IInsiteMsgRepository 4 { 5 /// <summary> 6 /// 插入一條消息 7 /// </summary> 8 /// <param name="msg">消息實體</param> 9 void Insert(InsiteMsg msg); 10 11 Task InsertAsync(IEnumerable<InsiteMsg> msgs); 12 13 /// <summary> 14 /// 根據消息 id 獲取消息內容,不包含閱讀狀態 15 /// </summary> 16 /// <param name="id">消息 Id</param> 17 /// <returns></returns> 18 InsiteMsg GetById(int id); 19 20 /// <summary> 21 /// 更新消息的閱讀時間為當前時間 22 /// </summary> 23 /// <param name="msgIds">消息的 Id</param> 24 /// <param name="userId">用戶 Id</param> 25 void UpdateReadTime(IEnumerable<int> msgIds,int userId); 26 27 /// <summary> 28 /// 獲取跟指定用戶相關的所有消息 29 /// </summary> 30 /// <param name="id">用戶 id</param> 31 /// <returns></returns> 32 Task<IEnumerable<InsiteMsg>> GetByUserIdAsync(int id); 33 34 /// <summary> 35 /// 刪除指定的用戶的消息記錄 36 /// </summary> 37 /// <param name="userId">用戶 Id</param> 38 /// <param name="msgRIds">消息 Id</param> 39 void DeleteMsgRecoreds(int userId, IEnumerable<int> msgRIds); 40 } 41 }
但是在測試階段,我並不想把倉庫實現掉,所以這里就用上了 Moq.Mock
。

1 List<InsiteMsg> dataSet = new List<InsiteMsg>(); 2 var msgRepoMock = new Mock<IInsiteMsgRepository>(); 3 msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) => 4 { 5 dataSet.AddRange(m); 6 });
上面的代碼模擬了一個 IInsiteMsgRepository
對象,在我們調用這個對象的 InsertAsync
方法的時候,這個對象就把傳入的參數添加到一個集合中去。
模擬出來的對象可以通過 msgMock.Object
訪問。
0x04 實現 Repository
使用事務
在創建並發送新的站內消息到用戶的時候,需要先插入消息本體,然后再把消息跟目標用戶之間在關聯表中建立聯系,所以我們需要考慮到下面兩個問題:
- 數據的一致性
- 在建立聯系前必須獲取到消息的 Id
為了解決第一個問題,我們需要使用事務(Transaction),就跟在 ADO.NET 中使用事務一樣,可以使用一個簡單的套路:

1 _conn.Open(); 2 try 3 { 4 using (var transaction = _conn.BeginTransaction()) 5 { 6 // execute some sql 7 transaction.Commit(); 8 } 9 } 10 finally 11 { 12 _conn.Close(); 13 }
在事務中,一旦部分操作失敗了,我們就可以回滾(Rollback)到初始狀態,這樣要么所有的操作全部成功執行,要么一條操作都不會執行,數據完整性、一致性得到了保證。
在上面的代碼中,using
塊內,Commit()
之前的語句一旦執行出錯(拋出異常),程序就會自動 Rollback。
在數據庫中,Id 是一個自增字段,為了獲取剛剛插入的實體的 Id 可以使用 last_insert_id()
這個函數(For MySql),這個函數返回當前連接過程中,最后插入的行的自增的主鍵的值。
最終實現

1 using Hive.Domain.Repositories.Abstracts; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Threading.Tasks; 6 using Hive.Domain.Entities; 7 using System.Data.Common; 8 using Dapper; 9 10 namespace Hive.Domain.Repositories.Concretes 11 { 12 public class InsiteMsgRepository : IInsiteMsgRepository 13 { 14 private readonly DbConnection _conn; 15 16 public InsiteMsgRepository(DbConnection conn) 17 { 18 _conn = conn; 19 } 20 21 public void DeleteMsgRecoreds(int userId, IEnumerable<int> msgIds) 22 { 23 var param = new 24 { 25 UserId = userId, 26 MsgIds = msgIds 27 }; 28 string sql = $@" 29 UPDATE insite_msg_record 30 SET Status = 0 31 WHERE UserId = @{nameof(param.UserId)} 32 AND Status = 1 33 AND InsiteMsgId IN @{nameof(param.MsgIds)}"; 34 try 35 { 36 _conn.Open(); 37 using (var transaction = _conn.BeginTransaction()) 38 { 39 _conn.Execute(sql, param, transaction); 40 transaction.Commit(); 41 } 42 } 43 finally 44 { 45 _conn.Close(); 46 } 47 48 } 49 50 public InsiteMsg GetById(int id) 51 { 52 throw new NotImplementedException(); 53 } 54 55 public async Task<IEnumerable<InsiteMsg>> GetByUserIdAsync(int id) 56 { 57 string sql = $@" 58 SELECT 59 ReadTime, 60 SentTime, 61 insite_msg.InsiteMsgId, 62 Content, 63 UserId 64 FROM insite_msg_record, insite_msg 65 WHERE UserId = @{nameof(id)} 66 AND insite_msg.InsiteMsgId = insite_msg_record.InsiteMsgId 67 AND insite_msg.Status = TRUE 68 AND insite_msg_record.Status = 1"; 69 var inboxMsgs = await _conn.QueryAsync<InsiteMsg>(sql, new { id }); 70 inboxMsgs = inboxMsgs.OrderBy(m => m.ReadTime); 71 return inboxMsgs; 72 } 73 74 public async Task InsertAsync(IEnumerable<InsiteMsg> msgs) 75 { 76 var msgContents = msgs.Select(m => new { m.Content, m.SentTime }); 77 string insertSql = $@" 78 INSERT INTO insite_msg (SentTime, Content) 79 VALUES (@SentTime, @Content)"; 80 _conn.Open(); 81 // 開啟一個事務,保證數據插入的完整性 82 try 83 { 84 using (var transaction = _conn.BeginTransaction()) 85 { 86 // 首先插入消息實體 87 var insertMsgTask = _conn.ExecuteAsync(insertSql, msgContents, transaction); 88 // 等待消息實體插入完成 89 await insertMsgTask; 90 var msgRecords = msgs.Select(m => new { m.UserId, m.InsiteMsgId }); 91 // 獲取消息的 Id 92 int firstId = (int)(_conn.QuerySingle("SELECT last_insert_id() AS FirstId").FirstId); 93 firstId = firstId - msgs.Count() + 1; 94 foreach (var m in msgs) 95 { 96 m.InsiteMsgId = firstId; 97 firstId++; 98 } 99 // 插入消息記錄 100 insertSql = $@" 101 INSERT INTO insite_msg_record (UserId, InsiteMsgId) 102 VALUES (@UserId, @InsiteMsgId)"; 103 await _conn.ExecuteAsync(insertSql, msgRecords); 104 transaction.Commit(); 105 } 106 } 107 catch (Exception) 108 { 109 _conn.Close(); 110 throw; 111 } 112 113 } 114 115 public void Insert(InsiteMsg msg) 116 { 117 string sql = $@" 118 INSERT INTO insite_msg (SentTime, Content) 119 VALUE (@{nameof(msg.SentTime)}, @{nameof(msg.Content)})"; 120 _conn.Execute(sql, new { msg.SentTime, msg.Content }); 121 string recordSql = $@" 122 INSERT INTO insite_msg_record (UserId, InsiteMsgId) 123 VALUE (@{nameof(msg.UserId)}, @{nameof(msg.InsiteMsgId)})"; 124 _conn.Execute(recordSql, new { msg.UserId, msg.InsiteMsgId }); 125 } 126 127 public void UpdateReadTime(IEnumerable<int> msgsIds, int userId) 128 { 129 var param = new 130 { 131 UserId = userId, 132 Msgs = msgsIds 133 }; 134 // 只更新發送給指定用戶的指定消息 135 string sql = $@" 136 UPDATE insite_msg_record 137 SET ReadTime = now() 138 WHERE UserId = @{nameof(param.UserId)} 139 AND Status = 1 140 AND InsiteMsgId IN @{nameof(param.Msgs)}"; 141 try 142 { 143 _conn.Open(); 144 using (var transaction = _conn.BeginTransaction()) 145 { 146 _conn.Execute(sql, param, transaction); 147 transaction.Commit(); 148 } 149 } 150 finally 151 { 152 _conn.Close(); 153 } 154 } 155 } 156 }
0x05 測試 Repository
測試 Repository 這部分還是挺難的,沒辦法編寫單元測試,EF 的話還可以用 內存數據庫,但是 Dapper 的話,就沒辦法了。所以我就直接
寫了測試用的 API,通過 API 直接調用 Repository 的方法,然后往測試數據庫里面讀寫數據。
轉載:http://www.cnblogs.com/JacZhu/p/6112033.html