是时候重构数据访问层的代码了

这篇草稿已经快发霉了,因为让人很难看懂,所以一直没有发布。今天厚着脸皮发布出来,希望得到大家的指正

一、背景介绍(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进行配置的,不清楚的同学赶紧用[找找看](http://zzk.cnblogs.com/s/blogpost?Keywords=fluntapi)搜索这个关键字。
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);
    }
}
1. 一或零对一
在背景中ER图中提到过的,密码和文章的关系就是1:1/0,文章可以有至多一个密码。
这种关系的配置比较比较难以理解,关键在于用文章BlogPost的主键作为密码BlogPostPassword的主键。
class BlogPostPasswordMap : EntityTypeConfiguration<BlogPostPassword>
{
    public BlogPostPasswordMap()
    {
        .HasKey(x => x.PostId)
        .Property(x => x.PostId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);// 设置为主键,不允许自增
    }
}

1. 一对一
文章必须要有内容,PostBody 和 BlogPost 就是 1:1 关系。需要注意的是在BlogPost中把PostBody标记为[Required],这样如果PostBody为空,EF就会抛出异常。

class PostBodyMap : EntityTypeConfiguration
{
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之间的关系。
[关系详解🔎点我](http://www.cnblogs.com/dudu/archive/2012/01/05/entity_framework_one_to_one_shared_primary_key.html)
依赖关系
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:
[一对多关系配置](https://stackoverflow.com/questions/11033348/is-it-possible-to-remove-child-from-collection-and-resolve-issues-on-savechanges/11033988#11033988)
posted @ 2016-11-11 15:22  蝌蝌  阅读(724)  评论(0编辑  收藏  举报