1. 程式人生 > >是時候重構數據訪問層的代碼了

是時候重構數據訪問層的代碼了

現實 dirty 多個 fluent ons none 比較 work 希望

這篇草稿已經快發黴了,因為讓人很難看懂,所以一直沒有發布。今天厚著臉皮發布出來,希望得到大家的指正

一、背景介紹(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呢,這是因為模型關系,下面來介紹如何配置關系。

配置關系

  1. 多對多的關系表
    還記得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);
        }
    }
  2. 一或零對一
    在背景中ER圖中提到過的,密碼和文章的關系就是1:1/0,文章可以有至多一個密碼。
    這種關系的配置比較比較難以理解,關鍵在於用文章BlogPost的主鍵作為密碼BlogPostPassword的主鍵。

    class BlogPostPasswordMap : EntityTypeConfiguration<BlogPostPassword>
    {
        public BlogPostPasswordMap()
        {
            .HasKey(x => x.PostId)
            .Property(x => x.PostId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);// 設置為主鍵,不允許自增
        }
    }
  3. 一對一
    文章必須要有內容,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:
一對多關系配置

是時候重構數據訪問層的代碼了