是時候重構數據訪問層的代碼了
這篇草稿已經快發黴了,因為讓人很難看懂,所以一直沒有發布。今天厚著臉皮發布出來,希望得到大家的指正
一、背景介紹(Why)
在用DDD時,我們一般都會抽象出UnitOfWork類型來進行CRUD。
例如有如下領域模型:
public class BlogPost { public int Id { get; set; } [Required] public PostBody Body { get; set; } public ICollection<PostToTag> PostToTags { get; set; } public BlogPostPassword Password { get; set; } } public class PostBody { public int PostId {get;set;} public string Text { get; set; } } public class PostToTag { public int Id { get; set; } public int PostId { get; set; } public int TagId { get; set; } public PostTag PostTag { get; set; } public BlogPost BlogPost { get; set; } } public class PostTag { public int Id { get; set; } public string Name { get; set; } public int BlogId { get; set; } public ICollection<PostToTag> PostToTags { get; set; } } public class BlogPostPassword { public int PostId { get; set; } [MaxLength(100)] public string Password { get; set; } }
現在我們要修改BlogPost模型,增加密碼Password,刪除所有關聯的標簽PostToTags,增加內容PostBody。示意代碼如下:
public async Task Update(BlogPost post) { BlogPost originPost = FromDB();//Get model from DB, tracked by EF. _unitOfWork.Add<BlogPostPassword>(new BlogPostPassword { Password = post.Password }); _unitOfWork.RemoveRange<PostToTag>(originPost.PostToTags); originPost.Body = new PostBody { Text = post.PostBody }; await _unitOfWork.CommitAsync();// SaveChanges() }
這只是一個簡單的場景,現實中的業務會更加復雜,如此就會產生非常難以理解的代碼,不利於維護,總而言之就是不優雅。
二、如何解決這個問題(How)
有一個非常好的方式就是:“實體模型即是數據模型”。
其實就是在EF中配置好各個實體之間的關系,無非就是那麽幾種(1:1;1:n; n:m),利用EF托管實體模型到數據庫的交互。直接保存實體模型,EF通過關系自動操作數據庫。
於是基於此重構這些代碼,把通過UnitOfWork對象操作數據的方式,改成在領域模型中操作,這樣使代碼更加優雅。
不過有一點需要註意,這些實體對象必須要被EF上下文跟蹤(track)才行。
//擴展方法,不直接把代碼寫在BlogPost實體中,好看。 public static class BlogPostExtensions { public static void UpdatePassword(this BlogPost post, string pwd) { if (string.IsNullOrEmpty(pwd)) { post.Password = null; } else { post.Password = post.Password ?? new BlogPostPassword(); post.Password.Password = pwd; } } //這裏的業務邏輯比較復雜,只是讓你知道它是可以處理復雜的邏輯的。 public static void UpdatePostToTags(this BlogPost post, string tagStr, IEnumerable<PostTag> allMyTags, Func<IEnumerable<PostTag>, object> removeDirtyPostTags) { post.PostToTags = post.PostToTags ?? new List<PostToTag>(); var dirtyPostToTags = new List<PostToTag>(); var dirtyPostTags = new List<PostTag>(); if (string.IsNullOrEmpty(tagStr)) { post.PostToTags.ForEach(t => { t.PostTag.UseCount--; if (t.PostTag.UseCount <= 0) dirtyPostTags.Add(t.PostTag); }); dirtyPostToTags = post.PostToTags.ToList(); } else { string[] tagArray = tagStr.Split(‘,‘); tagArray = tagArray.Distinct().Where(x => !string.IsNullOrEmpty(x)).Select(x => x.Trim()).Take(10).ToArray(); //diff post.PostToTags.Where(t => !tagArray.Contains(t.PostTag.Name)).ForEach(dirtyItem => { dirtyItem.PostTag.UseCount--; if (dirtyItem.PostTag.UseCount <= 0) { dirtyPostTags.Add(dirtyItem.PostTag); } dirtyPostToTags.Add(dirtyItem); }); tagArray.Where(t => !post.PostToTags.Select(g => g.PostTag.Name).Contains(t)).ForEach(freshName => { //if exist old tag var existTag = allMyTags.FirstOrDefault(t => t.Name == freshName); if (existTag == null) { existTag = new PostTag { BlogId = post.BlogId, CreateTime = DateTime.Now, Name = freshName, UseCount = 1 }; } else { existTag.UseCount++; } post.PostToTags.Add(new PostToTag() { PostTag = existTag, BlogId = post.BlogId, PostId = post.Id, TagId = existTag.Id }); }); } dirtyPostToTags.ForEach(d => post.PostToTags.Remove(d)); removeDirtyPostTags?.Invoke(dirtyPostTags); } }
最後我們只要通過EF獲取到BlogPost對象,然後通過以上擴展方法修改對象,最後調用SaveChanges()保存該對象。EF就會把跟蹤到的變化,生成SQL語句並執行。
敲黑板,註意聽,畫重點了。
EF如何跟蹤實體模型的變化,就能生成對應的SQL呢,這是因為模型關系,下面來介紹如何配置關系。
配置關系
-
多對多的關系表
還記得PostTag 和 BlogPost 嗎?tag標簽和文章之間的關系就是典型的多對多關系,我們用來一張中間表PostToTag來進行關聯。多對多關系的配置核心就是這個中間表。
下面代碼是通過FluentApi進行配置的,不清楚的同學趕緊用找找看搜索這個關鍵字。public class PostToTagMap : EntityTypeConfiguration<PostToTag> { public PostToTagMap() { HasKey(x => new { x.Id, x.PostId, x.TagId }); // 這裏要設置多個key,因為設置單個key會在刪除時出現異常,詳情請點擊文末的引用。 Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); HasRequired(x => x.PostTag).WithMany(x => x.PostToTags).HasForeignKey(x => x.TagId); HasRequired(x => x.BlogPost).WithMany(x => x.PostToTags).HasForeignKey(x => x.PostId); } }
-
一或零對一
在背景中ER圖中提到過的,密碼和文章的關系就是1:1/0,文章可以有至多一個密碼。
這種關系的配置比較比較難以理解,關鍵在於用文章BlogPost的主鍵作為密碼BlogPostPassword的主鍵。class BlogPostPasswordMap : EntityTypeConfiguration<BlogPostPassword> { public BlogPostPasswordMap() { .HasKey(x => x.PostId) .Property(x => x.PostId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);// 設置為主鍵,不允許自增 } }
-
一對一
文章必須要有內容,PostBody 和 BlogPost 就是 1:1 關系。需要註意的是在BlogPost中把PostBody標記為[Required],這樣如果PostBody為空,EF就會拋出異常。class PostBodyMap : EntityTypeConfiguration<PostBody> { public PostBodyMap() { ToTable("CNBlogsText__blog_PostBody") .HasKey(b => b.PostId) .Property(b => b.PostId).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); Ignore(b => b.PlainText); } }
下面是重點中的重點,必考!!!
最重要的還是設置BlogPost 和 PostBody, BlogPostPassword之間的關系。
關系詳解??點我
依賴關系public class BlogPostMap : EntityTypeConfiguration<BlogPost> { public BlogPostMap() { ToTable("blog_Content"); HasKey(b => b.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); HasRequired(p => p.Body).WithRequiredPrincipal(); HasRequired(p => p.Password).WithRequiredPrincipal(); } }
最後
需要數據庫連接中加入 MultipleActiveResultSets=true; 以啟動MultipleActiveResultSets支持。這個東西簡單點說就是提高數據庫連接的復用率,同一個連接中進行多向操作,Sql server 2005+版本才支持。
References:
一對多關系配置
是時候重構數據訪問層的代碼了