是时候重构数据访问层的代码了
这篇草稿已经快发霉了,因为让人很难看懂,所以一直没有发布。今天厚着脸皮发布出来,希望得到大家的指正
一、背景介绍(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:
一对多关系配置