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

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

一、背景介绍(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:
一对多关系配置

posted @ 2017-05-30 18:42  杨铭宇  阅读(155)  评论(0编辑  收藏  举报
友情链接:回力球鞋 | 中老年女装 | 武汉英语学校 | 雅思备考 | 托福备考