@iEricLee Creation time : 2023-05-29 21:04:38 Last Modification Time : 2023-05-29 21:33:08
来自 ABP Framework 研习社 @大风
的提问,感谢 @xiaolipro
的回复:
要想理清和解决以上问题,至少涉及以下两个核心知识点:
结合具体的应用代码来分析。接下来一一展开:
为了更好地理解领域服务和应用服务中的更新操作,我们先看一段框架中的源码,通过示例代码可以更清晰地说明二者的关系。
在 CmsKit应用模块 中,博客实体 Blog
对应一个博客领域服务类 BlogManager
。
public class BlogManager : DomainService
{
//...
}
其对实体更新方法的实现:
public virtual async Task<Blog> UpdateAsync([NotNull] Blog blog, [NotNull] string name, [NotNull] string slug)
{
if (slug != blog.Slug)
{
await CheckSlugAsync(slug);
}
blog.SetName(name);
blog.SetSlug(slug);
return blog;
}
更新方法接收参数:博客实体
,名称
,slug
。第一个参数表示原有的博客实体,后面传入的参数都是对该实体对应属性的赋值更新。
在更新方法中首先对 slug
进行状态有效性检测,是否和实体中的值一样,如果不一样则查询其他实体是否已经存在相同 slug
值,存在则抛出异常,这里实现实体 slug
值不能相同的业务逻辑,因为需要通过 slug
获取对应的博客内容,必须保持唯一性。
博客应用服务 BlogAdminAppService
,通过构造函数注入博客领域服务 BlogManager
:
public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppService
{
protected BlogManager BlogManager { get; }
//...
public BlogAdminAppService(
//...
BlogManager blogManager,
//...)
{
//...
BlogManager = blogManager;
//...
}
}
博客应用服务中的更新方法:
public virtual async Task<BlogDto> UpdateAsync(Guid id, UpdateBlogDto input)
{
var blog = await BlogRepository.GetAsync(id);
blog = await BlogManager.UpdateAsync(blog, input.Name, input.Slug);
blog.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
await BlogRepository.UpdateAsync(blog);
return ObjectMapper.Map<Blog, BlogDto>(blog);
}
首先根据实体ID,通过仓储获取原实体;然后调用博客领域服务更新方法更新 Name
和 Slug
属性值,该方法对 Slug
和 Name
执行有效性检测。
接收传递过来的 ConcurrencyStamp
值,如果不为空则更新实体对应属性的值,防止更新时出现并发操作。
最后调用仓储,传入更新后的实体,执行更新操作。(注意:这里并不会立即执行更新操作。)
通过以上代码的逐行分析,可以总结如下结论:
对于更新操作,领域服务和应用服务的职责是什么? 领域服务负责为实体属性赋值,并检测值状态的有效性;应用服务负责调用仓储的更新方法执行更新操作。
简而言之,领域服务负责维护实体的状态,应用服务负责执行更新操作。不建议在领域服务中直接调用仓储执行更新操作,避免重复更新。
领域服务和应用服务中都可以通过构造函数注入仓储,通常在领域服务中注入仓储是为了验证实体状态的有效性执行查询操作。
应用服务中的更新操作,最终调用仓储的更新方法来实现的。为了更透彻地理解仓储中的更新操作,有必要查看下实现的源码。在 ABP Framework 中基于 EF 的仓储默认实现是 EfCoreRepository
,查看更新方法 UpdateAsync
源码:
public override async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
var updatedEntity = dbContext.Update(entity).Entity;
if (autoSave)
{
await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return updatedEntity;
}
仓储 UpdateAsync
方法接收的参数是一个实体 IEntity
。
获取数据库上下文,附加实体,更新实体返回状态跟踪对象。根据传递的参数决定是否立即执行更新操作,默认通过工作单元在当前任务完成之后统一执行更新操作。
通过上述分析,更新操作的标准实现方式是:在应用服务中获取实体,然后更新实体状态,最后调用仓储的更新方法更新实体。
注意两个问题:
1. 为什么要先查询实体? 为了保持实体中未更改属性的值一致,更新的属性赋最新值。通常不会对实体的所有属性都赋最新值。例如:实体中的部分审计属性 CreatorId
CreatorTime
LastModifierId
LastModificationTime
,不需要每次手动修改,框架会自动更新。
这里有一个特殊的属性:并发属性,对实体的编辑操作,都应该考虑并发操作,将原 ConcurrencyStamp
时间戳保存在编辑页面,提交编辑时,在更新之前将实体的时间戳属性设置为该值,这样并发检测才会有效。
2. 是否可以手动创建一个实体? 简单实体可以,复杂实体不可以。手动创建的实体必须为所有属性赋值,但是某些属性的默认值依然要从数据库中查询,直接执行更新会导致覆盖原实体的有效数据,导致更新数据出现不一致的情况!
QQ群:ABP Framework 研习社
专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!
公众号:dotNET兄弟会
专注.Net开源技术及跨平台开发!致力于构建完善的.Net开放技术文库!为.Net爱好者提供学习交流家园!
网站:知识乐,基于 ABP Framework 搭建的学习社区,目前上线系列教程和博客频道: