EFCore在DDD中的使用

在DDD中,我们对聚合根的操作都会通过仓储去获取聚合实例。

因为聚合根中可能会含有实体属性,值对象属性,并且,在DDD中,我们所设计的领域模型都是充血模型。所以,在对聚合根的持久化中,最方便的还是Mangodb这种KEY-VALUE存储的NOSQL。

不过,关系型数据库通过EF也能方便的解决复杂模型的数据库映射。

本文使用EFCore,部分API不适用于EF;本文不谈DDD。

以下引出几个知识点:

  • backing field
  • releation
  • lazy load
  • data binding
  • navigation property
  • converter

让我们开始吧

我们首先定义一个复杂关系的 对象模型;

大致上描述下这个BookEntity根实体类的几个定义:

  • 拥有只读的属性 Name
  • 拥有两个对象属性AuthorCatalog
  • 枚举EnumBookType类型属性Type
  • 拥有两个私有的列表字段 _chapters_keyWords

简单映射

  1. class BookEntity{
  2. private BookEntity(string name){
  3. Name = name;
  4. }
  5. public string Name { get; }
  6. public string BookCoverImage { get; private set; }
  7. public EnumBookType Type { get; private set; }
  8. //...
  9. }
  10. class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>{
  11. public void Configure(EntityTypeBuilder<BookEntity> builder){
  12. builder.Property<string>("Id").HasColumnName("_id_")
  13. .HasValueGenerator<StringGuidValueGenerator>();
  14. builder.Property(x => x.Name);
  15. builder.Property(x => x.Type)
  16. .HasConversion<string>(k => k.ToString(), v => Enum.Parse<EnumBookType>(v));
  17. }
  18. }

在上述代码中,我们定义了一个简单对象类及它的配置项。

  • BookEntity中未定义Id主键,我们通过 阴影属性 的方式指定了一个主键,并将它映射到db的_id_列;
  • EF中默认绑定 有 setter 方法的 public getter 属性,而我们的Name没有setter方法,我们必须通过在配置中显示调用 Property() 将其加入到绑定中。
  • EF中可以通过构造函数将字段绑定到实体上。
  • 可以能过调用 HasConversion() 方法显示指定使用的转换方法。比如将IDictionary<string,string> 保存为 string

那么我们如何根据 主键 查询呢?

EF 为我们提供了静态方法EF.Property()

  1. var entity = ctx.Set<BookEntity>().FirstOrDefault(x=>EF.Property<string>(x,"Id") == "1");

关系与固有类型

在官方文档中,关系主要使用以下几种方法来配置的。

  • HasOne()
  • HasMany()
  • WithOne()
  • WithMany()

OwnsType (固有类型)是新近推出的API。

  • OwnsOne()
  • OwnsMany()

虽然都会创建导航属性,但是从定义和使用上来看

,还是有很大区别的。

经过测试,导航属性不能通过构造函数绑定,所以以下配置中,均使用 private setter

(如果有读者发现错误,欢迎指正。)

下面我们就对两种API进行配置。

OwnsType的配置

从使用上的角度上来看,OwnsType像其名字一样,强调的是A拥有B,这个属性是这个类固有的,没有懒加载的配置。

扩展我们之前写义的实体类。

  1. class BookEntity{
  2. //...略
  3. public AuthorInfo Author { get; private set; }
  4. private List<KeyWordInfo> _keyWords = new List<KeyWordInfo>();
  5. public IEnumerable<KeyWordInfo> KeyWords => _keyWords;
  6. }
  7. class AuthorInfo
  8. {
  9. public AuthorInfo(string name){
  10. Name = name;
  11. }
  12. public string Name { get; }
  13. }
  14. class KeyWordInfo
  15. {
  16. public KeyWordInfo(string word){
  17. Word = word;
  18. }
  19. public string Word { get; }
  20. }

扩展配置类

  1. class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>{
  2. builder.OwnsOne(x => x.Author, b => {
  3. b.Property(v => v.Name).HasColumnName("AuthorName");
  4. });
  5. builder.OwnsMany(x => x.KeyWords, b =>
  6. {
  7. b.ToTable("BookKeyWords");
  8. b.Property<int>("Id").HasColumnName("_id_");
  9. b.HasKey("Id");
  10. b.Property(x => x.Word);
  11. b.HasForeignKey("BookId");
  12. });
  13. builder.Metadata.FindNavigation(nameof(BookEntity.KeyWords))
  14. .SetPropertyAccessMode(PropertyAccessMode.Field);
  15. }

默认情况下,OwnsOne()会与实体映射在同一张表,OwnsMany()没有做具体测试。

这里我们对导航属性KeyWords进行了配置,因为它是只读的,所以我们将它配置为绑定为字段,这个私有字段叫做backing field(支持字段??),在EF中默认有以下4种格式,当然这是支持自定义的:

  • _< camel-cased property name >
  • _< property name >
  • m_< camel-cased property name >
  • m_< property name >

那什么是backing field ???

ReleationShip 配置

HasOne()这种关系API,更适合于A与B之前的关系,比如 1-* (一对多)的关系、1-1(一对一)的关系等等,所以这种配置必须在不同表中。

  1. class BookEntity{
  2. //...略
  3. private IList<BookChapterEntity> _chapters = new List<BookChapterEntity>();
  4. public IEnumerable<BookChapterEntity> Chapters => _chapters;
  5. }
  1. class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>
  2. {
  3. public void Configure(EntityTypeBuilder<BookEntity> builder)
  4. {
  5. //...略
  6. builder.HasMany(x => x.Chapters).WithOne().HasForeignKey("BookId");
  7. builder.Metadata.FindNavigation(nameof(BookEntity.Chapters))
  8. .SetPropertyAccessMode(PropertyAccessMode.Field);
  9. }
  10. }
  11. class BookChapterEntityTypeConfiguration : IEntityTypeConfiguration<BookChapterEntity>
  12. {
  13. public void Configure(EntityTypeBuilder<BookChapterEntity> builder)
  14. {
  15. builder.Property(x => x.Title);
  16. builder.Property(x => x.Index);
  17. builder.Property<string>("Id");
  18. }
  19. }

从配置上来看,我们的两个实体都是分开配置的,而从实体类角度上看,这里是两个类体的关系,我们配置的是 1-的关系。

使用HasOne()
ReleationShip* Api配置的属性,默认是不加载的,我们可以通过配置进行立即加载或延迟加载。

我们可以查看官方文档查看懒加载的方式。

https://docs.microsoft.com/en-us/ef/core/querying/related-data

backing field (支持字段)

我们都知道C#中有这样子的写法。

  1. class Foo{
  2. public string Name{get;set;}
  3. }

写完整了是这样的。

  1. class Foo{
  2. private string _name;
  3. public string Name{
  4. get{return _name;}
  5. set{_name = value;}
  6. }
  7. }

而在其它语言中,可能是这样的。

  1. class Foo{
  2. private string _name;
  3. public string GetName(){
  4. return _name;
  5. }
  6. public void SetName(value){
  7. _name = value;
  8. }
  9. }

我认为以上的_name就是一个backing field; 以字面意思解释就是属性底层的字段。

查询过滤器 Query Filter

我们公司的业务设计上,数据不能真删,通过一个 IsDeleted 字段进行控制。这样在有必要的情况下,我们可以将数据进行还原。

  1. class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>
  2. {
  3. public void Configure(EntityTypeBuilder<BookEntity> builder)
  4. {
  5. //...略
  6. builder.Property<bool>("IsDeleted");
  7. builder.HasQueryFilter(x=>!EF.Property<bool>(x,"IsDeleted"));
  8. }
  9. }

我们通过HasQueryFilter()配置一个全局过滤器。

什么?你说又要查询的时候要查询IsDeleted == true的数据??

  1. var allBooks = ctx.Set<BookEntity>()
  2. .IgnoreQueryFilters()
  3. .ToList();
  4. //通过 IgnoreQueryFilters 忽视掉全局过滤器;

更多的查看官方文档

https://docs.microsoft.com/en-us/ef/core/querying/filters

结尾总结

在我们项目切换到DDD模式下开发的时候,使用关系型数据库作为仓储的实现真是头疼。还好,我们有EF,但是如果对EF的API和映射不熟悉的话,会导致出现因技术原因修改领域模型的情况,而这种情况是我们应该避免的。

如发现文中有误,欢迎指正。

DDD中的EFCore的更多相关文章

  1. EFCore:关于DDD中值对象(Owns)无法更新数值

    最近使用DDD+EFCore时,使用EFCore提供的OwnsOne或者OwnsMany关联值对象保存数据,没想到遇到一个很奇怪的问题:值对象中的值竟然无法被EFCore保存!也没有抛出任何异常!我瞬 ...

  2. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  3. DDD~Unity在DDD中的使用

    回到目录 上一讲介绍了DDD中的领域层,并提到下次要讲Unity,所以这篇文章当然就要介绍它了,呵呵,Unity是Microsoft.Practices中的一部分,主要实现了依赖注入的功能,或者叫它控 ...

  4. Repository在DDD中的应用

    Repository在DDD中的应用2014-10-09 08:55 by Jesse Liu, 98 阅读, 0 评论, 收藏, 编辑 概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值 ...

  5. DDD中的分层架构

    DDD中的分层架构很好的应用了关注点分离原则Separation of Concerns(SOC),每一层做好自己的事情,减少交叉 表现层 表现层提供用来完成任务的用户界面,如webform wpf ...

  6. 对DDD中领域服务的理解

    CZ 能不能清晰具体区分service和实体的区别 网上有人用DCI来解决 不知道对不对 STST 我复习下DDD中的服务的概念了参与讨论啊CZ 这个我也看过 但是太过于笼统 STST STST 复习 ...

  7. DDD中的聚合和UML中的聚合以及组合的关系

    UML:聚合关系:成员对象是整体的一部分,但是成员对象可以脱离整体对象独立存在.如汽车(Car)与引擎(Engine).轮胎(Wheel).车灯(Light)之间的关系为聚合关系,引擎.轮胎.车灯可以 ...

  8. DDD中的值对象如何用NHibernate进行映射

    原文:DDD中的值对象如何用NHibernate进行映射 <component/>是NHibernate中一个有趣的特性,即是用来映射DDD(Data-Display-Debuger)概念 ...

  9. Java实现DDD中UnitOfWork

    Java实现DDD中UnitOfWork 背景 Maintains a list of objects affected by a business transaction and coordinat ...

随机推荐

  1. PMBOK项目管理认知概要

    2015年6月,通过努力取得PMP证书,很是欣喜,也是对努力付出的一种奖励吧! 通过学习PMP相关的项目管理的知识,对国外的项目管理技术有更加系统的认知.理解.掌握,熟悉全项目生命周期的管理. 其实对 ...

  2. Backup--完整备份会打破现有的日志备份链么?

    --问题描述: --对数据库有一个周期性数据库备份和事务日志备份的维护计划,在维护计划外有工作人员对数据库进行完整备份,该备份会打乱现有的日志备份链么? --===================== ...

  3. uwsgi怎么启动停止

    ## 二.启动停止重启 uWSGI 通过 xxx.ini 启动后会在相同目录下生成一个 xxx.pid 的文件,里面只有一行内容是 uWSGI 的主进程的进程号. uWSGI 启动:uwsgi --i ...

  4. Python【filter、map、reduce】

    filter和map和reduce map(function,iterable...) -> list 映射,对列表中的每个值操作 返回操作后的数值组成列表 # 给列表值+1 l = [1,2, ...

  5. C#中List调用库函数sort进行升序排序

    private void button1_Click(object sender, EventArgs e) { List<int> demo2 = new List<int> ...

  6. linux命令之系统管理命令(下)

    1.chkconfig:管理开机服务 该命令为linux系统中的系统服务管理工具,可以查询和更新不同的运行等级下系统服务的启动状态. 选项 说明 --list(常用) 显示不同运行级别下服务的启动状态 ...

  7. fread和fwrite用法小结

    fwrite和fread是以记录为单位的I/O函数,fread和fwrite函数一般用于二进制文件的输入输出. #include <stdio.h>size_t fread(void *p ...

  8. cookie的优缺点

    优点  :极高的扩展性和可用性 1.通过良好的编程,控制保存在cookie中的session对象的大小. 2.通过加密和安全传输技术(ssl),减少cookie被破解的可能性 3.只有cookie中存 ...

  9. 驼峰转大写(javaScript)

    var a = function(s){return s.replace(/([A-Z])/g,"_$1").toUpperCase();} F12控制台可以直接用

  10. Linux CentOs 下 安装 mysql nginx redis

    SCP 的使用 来源于: https://blog.csdn.net/qq_30968657/article/details/72912070 scp [参数] <源地址(用户名@IP地址或主机 ...