写在前面

首先,这篇博文是用博客园新发布的 MarkDown编辑器 编写的,这也是我第一次使用,语法也不是很熟悉,但我觉得应该会很爽,博文后面再记录下用过的感受,这边就不多说。

阅读目录:

  1. 上一篇回顾-设计误区
  2. 值对象映射探讨
  3. 走过的坑-正确配置
  4. 后记-附带(CNBlogs 使用 Mardown 小记)

领域驱动设计中,关于领域模型和 EntityFramework 之间的映射配置,其实之前写过一篇《死去活来,而不变质:Domain Model(领域模型) 和 EntityFramework 如何正确进行对象关系映射?》博文,因为当时主要精力是在领域模型的设计中,持久化问题考虑的太早,所以在当时领域驱动设计的道路上跑偏了。现在领域模型设计的差不多了,因为之前都是在 Repository(仓储)中使用静态集合跑程序,现在持久化的问题是该考虑了。

说真的,其实现在来看,上一篇探讨的内容还是蛮有价值的,如果你对领域模型和 EntityFramework 之间映射配置感兴趣,最好还是阅读下上一篇博文,如果没时间阅读也没关系,我来带你简单回顾一下。

上一篇回顾-设计误区

上一篇博文的关键字是:死去活来,而不变质,也就是:如何把活的变成死的?又如何把死的变成活的?更重要的是如何保证在这个“死去活来”的过程中,死的和活的是同一个?

活的:Domain Model(领域模型),主要是领域模型中的 Entity(实体)对象。

死的:使用 ORM 工具映射,把领域模型映射到关系型数据库的表数据。

在领域驱动设计中,数据库设计的概念是被我们所抛弃的,也就是说,在你领域模型设计的过程中,不应该考虑数据库的因素,这个过程应该放到最后,也就是我现在所考虑的,这也就是为什么之前探讨持久化问题是跑偏的原因了。还有一个重要概念,就是数据库不是被设计的,而是应该被生成的,当你应用程序设计完成的时候,你只需要配置下仓储的持久化实现,这样数据库就可以使用 Code First 进行生成了。

过程虽然说起来简单,实现起来却不是那么容易,因为我们长久以往受数据库驱动模式的影响,在应用程序开发的时候,就会不自觉的去考虑数据库。比如一个用户模块,按照我们传统的开发模式,应该是先设计用户模块的表结构(用户表、用户部门表、用户权限表等等),然后根据表结构去设计一大堆的 SQL 语句(左关联、右关联、自己关联等等),数据库访问层(DAL)就充斥着大量的 SQL 代码,其实这些代码就反应了业务需求,以至于我们的业务逻辑层(BLL)变成了一个方法调用者(dal.GetUser....),它确实很薄,薄到可以直接忽略掉,客户端代码是怎样的呢?简单的来说就是从界面上获取值,然后 new 一个 bll 对象,调用方法传入值,没错,就是这样。

那这样致使的结果是怎样的呢?比如要该一个需求,麻烦一点的就是,我们需要改表结构,改完表结构,我们需要改数据访问层的 SQL 代码,改完 SQL 代码,我们需要改业务逻辑层中的方法参数,改完方法参数,我们需要改客户端的调用....没完没了,这还只是一个需求的变更,我相信我们每天遇到的不只是一个吧,想想真是太痛苦了。

好像有点偏离主题了,但是体会这个传统开发模式是很重要的,因为只有体会到它的痛苦,你才会想办法去改变它,当然除非你是处在一个“温水煮青蛙”的环境中,这个就没办法了。

回到领域驱动设计上来,领域模型(主要是实体,后面用实体表示)如何使用 EntityFramework 进行映射配置?简单一点,这个实体没有任何对象的关联,那我们根根不需要什么映射配置,只需要配置一下主键和字段长度就行了。但是如果存在对象关联,我们怎么配置呢?按照之前数据库驱动模式的开发,肯定要在相应的关联表中加入外键,那我们的实体就会变成这样:

namespace MessageManager.Domain.DomainModel
{
public class Message : IAggregateRoot
{
#region 构造方法
public Message()
{
this.ID = Guid.NewGuid().ToString();
}
#endregion #region 实体成员
public string FromUserID { get; set; }
public string FromUserName { get; set; }
public string ToUserID { get; set; }
public string ToUserName { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime SendTime { get; set; }
public bool IsRead { get; set; }
public virtual User FromUser { get; set; }
public virtual User ToUser { get; set; }
#endregion #region IEntity成员
/// <summary>
/// 获取或设置当前实体对象的全局唯一标识。
/// </summary>
public string ID { get; set; }
#endregion
}
}

按照我们之前数据库模式,会觉得这样设计没错啊,但是现在是基于领域驱动设计,你会那发现 FromUserIDToUserID 这两个是什么东西啊?只是为了方便数据库映射,就加入这两个“外键”,很显然,这种设计是不合理的。

还有一种设计也是不合理的,就是在实体属性上面加入 EntityFramework 属性配置,领域模型中应该是和技术无关的,如果加入技术实现,那这个领域模型就被污染了,像 EntityFramework 的 Attribute 配置应该放在基础层去实现,当然我个人觉得,这是 EntityFramework 有点误导人的感觉,因为在实体属性上面进行配置更方便,但是在领域驱动设计中,这样实现并不合理,比如下面这段代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; namespace DemoTag.Domain.Entities
{
[Table("TagUseCount")]
public class TagUseCount
{
[Key]
[Column(Order = 1)]
public Guid AppGuid { get; set; } [Key]
[Column(Order = 2)]
[ForeignKey("Tag")]
public int TagId { get; set; } public int UseCount { get; set; } public virtual Tag Tag { get; set; }
}
}

如果我们不这样进行实现,那我们如何进行映射配置呢?这个实现在后面有讲解,在实现之前,要先明确几个重要概念:

1,领域模型不参杂任何的技术实现。

2,数据库的映射配置,不影响领域模型(比如上面的 FromUserIDToUserID,就是很不合理)。

3,数据库的映射配置,属于技术实现,应该放在基础层中。

因为第二点相对比较难理解一点,这边我就再简单说明下,数据库是领域模型存储数据的一种方式(我们也可以使用其他方式进行存储),现在的关系型数据库都是“扁平化”存储,所以像对象之中关联对象,我们一般都是要进行外键配置,这因为有了 ORM 工具,所以我们可以很方便的进行对象关系映射(ORM 的中文意思),对象指的就是领域模型,关系就是关系型数据库。所以我们映射配置不应该影响领域模型,具体怎么进行配置?这是 ORM 工具所考虑的问题,上一篇的内容是主要是关于实体映射配置,下面简单说下领域模型中值对象的映射配置。

值对象映射探讨

有人可能有些疑问,值对象需要映射配置吗?当然,简单一点的枚举类型的值对象,是不需要进行映射配置的,比如下面 MessageState 这个值对象:

/**
* author:xishuai
* address:https://www.github.com/yuezhongxin/MessageManager
**/ namespace MessageManager.Domain.ValueObject
{
public enum MessageState
{
Unread,
Read,
}
}

在 Message 实体中对应的关联:

public MessageState State { get; private set; }

上面这段代码,如果我们使用 EntityFramework,是不需要任何映射配置的,枚举类型的值对象会自动映射为 int 类型,比如上面 MessageState 的映射结果为:0 代表 Unread,1 代表 Read。这个映射过程,在领域驱动设计中是不关心的,在应用层,我只关心从仓储中持久化的对象或者获取的对象,是不是正确的实体对象?是不是正确的值对象?也就是说我现在在应用层中去编写下面这段代码:

using (IRepositoryContext repositoryContext = new EntityFrameworkRepositoryContext())
{
IMessageRepository messageRepository = new MessageRepository(repositoryContext);
Message message = messageRepository.GetByKey(1);
if (message.State == MessageState.Unread)
{
//默认是未读
}
}

message.State == MessageState.Unread 这是我所关心的,我从仓储中取的是不是我所存储的正确值对象。其实这也是 EntityFramework 这一类 ORM 工具的强大之处,在领域驱动设计中更能得到体现,它让我们更专注于领域模型的设计,而不考虑数据是怎样进行存储的,那如何进行隔离他们两者呢?答案就是 Repository(仓储),很多时候,都是由问题引出概念,这样理解的才会更加深刻。

如果我们映射的不是枚举类型的值对象,而是其他类型的值对象,我们怎么进行映射配置呢?比如下面 Contact 值对象:

/**
* author:xishuai
* address:https://www.github.com/yuezhongxin/MessageManager
**/ namespace MessageManager.Domain.ValueObject
{
public class Contact
{
public Contact(string name)
{
this.Name = name;
} public Contact(string name, string displayName)
{
this.Name = name;
this.DisplayName = displayName;
} public string Name { get; private set; }
public string DisplayName { get; private set; }
}
}

先说一下 Contact 值对象的意思,表示 Message 实体中的抽象“联系人”标识,说白了就是发送人和接收人的意思,但这个发送人或接收人不一定是“人”,也可能是邮箱等,就是一个标识的意思,这个“标识”从是外部取得的,也就是说在消息这个系统中是不存储的,我只知道这个标识是什么?那不需要知道它是哪个?这也就是为什么设计成值对象的原因了。

Contact 值对象就不像 MessageState 值对象不需要那样了,这个就必须在 EntityFramework 进行配置的,具体如何进行映射配置,请看下面,走过的坑

走过的坑-正确配置

首先,我试了下,如果不进行映射配置会是怎样的结果,比如我们在 MessageConfiguration 映射配置类中(实现在基础层)配置如下:

using MessageManager.Domain.Entity;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration; namespace MessageManager.Repositories.EntityFramework.ModelConfigurations
{
public class MessageConfiguration : EntityTypeConfiguration<Message>
{
/// <summary>
/// Initializes a new instance of <c>MessageConfiguration</c> class.
/// </summary>
public MessageConfiguration()
{
HasKey(c => c.ID);
Property(c => c.ID)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(c => c.Title)
.IsRequired()
.HasMaxLength(50);
Property(c => c.Content)
.IsRequired()
.HasMaxLength(2000);
Property(c => c.SendTime)
.IsRequired();
}
}
}

可以看到,我们只对一些简单属性进行了简单配置,并没有对 Contact 进行任何的映射配置,那 EntityFramework 生成数据库会是怎样呢(使用 Code First 模式)?答案就是:报错

RepositoryTest_AddMessage 单元测试代码(一定要先进行单元测试,在领域驱动设计开发过程中,非常重要):

/**
* author:xishuai
* address:https://www.github.com/yuezhongxin/MessageManager
**/ using MessageManager.Domain.Entity;
using MessageManager.Domain.Repositories;
using MessageManager.Domain.ValueObject;
using MessageManager.Repositories.EntityFramework;
using Xunit; namespace MessageManager.Repositories.Tests
{
public class MessageRepositoryTest
{
[Fact]
public void RepositoryTest_AddMessage()
{
IMessageRepository messsageRepository = new MessageRepository(new EntityFrameworkRepositoryContext());
messsageRepository.Add(new Message("title", "content", new Sender("1", "小菜"), new Recipient("2", "大神")));
messsageRepository.Context.Commit();
}
}
}

异常信息:

注意红圈里面的信息,因为我只找到这个异常信息(第一段):在 System.Data.Entity.Utilities.Check.NotNull T (T value, String parameterName),完全不知道是什么原因,NotNull 也就是有一个参数为 NULL,具体是什么,并不知道,怎么办呢?难道让我去调试 EntityFramework 源码?把 Google 给忘了,搜索了一下,在 stackoverflow 中找到了类似问题,解决方案就是:

[NotMapped]
public HttpPostedFileBase Photo { get; set; }

NotMapped 顾名思义,就是忽略映射的意思,也就是说在 EntityFramework 生成数据库的时候,Photo 这个属性并不映射。NotMapped 是直接在实体中定义属性配置,这个我们在上面强调过,这样设计不是合理的,我们应该在 MessageConfiguration 中进行配置,那就不能使用 NotMapped 属性了,在 EntityTypeConfiguration 配置中,找到 Ignore 方法,配置如下:

Ignore(c => c.Sender);
Ignore(c => c.Recipient);

配置好了,我们再生成数据库:

可以看到我们是生成成功的,Message 实体对象的 SenderRecipient 是被忽略的,但是这并不是我们想要的结果,因为我们是要映射配置 Contact,这才是我们的目的,怎么把它给忽略了啊。虽然走了弯路,但是让我们发现异常问题,确实是 Contact 映射引起的(我之前还怀疑是不是 EntityFramework 配置有什么问题)。

确定了问题的原因,就要找相应的解决办法。因为值对象强调的是“值”的概念,也就是说映射到数据库的时候,要把值对象进行“扁平化”处理,Contact 值对象包含 NameDisplayName 两个属性(之前还有一个 LoginName 属性,后来考虑了一下,其实并不需要),也就是说,这两个属性都必须映射到 Message 实体中,然后 EntityFramework 进行数据到对象的转化,我们就可以通过 message.Sender 访问到 Contact 值对象了,这是我们想要的效果,在仓储中只需要 Add 和 Get Message对象,并不需要Contact值对象的任何操作,因为Contact值对象是依附于Message实体的,所以必须通过Message` 实体进行操作。

Google 中搜索“entitytypeconfiguration value object”,在 stackoverflow 中找到相似的解决方法,配置如下:

Property(c => c.Sender.Name)
.HasColumnName("SenderName")
.IsRequired()
.HasMaxLength(36);
Property(c => c.Recipient.Name)
.HasColumnName("RecipientName")
.IsRequired()
.HasMaxLength(36);
Property(c => c.Sender.DisplayName)
.HasColumnName("SenderDisplayName")
.HasMaxLength(50);
Property(c => c.Recipient.DisplayName)
.HasColumnName("RecipientDisplayName")
.HasMaxLength(50);

生成相应数据库:

单元测试:

其实在 entitytypeconfiguration 的配置中,不止上面的一些坑,还有很多没有记录到,关于 entitytypeconfiguration 的正确配置,请参考 MSDN 中的相关内容

后记-附带(CNBlogs 使用 Mardown 小记)

CNBlogs 使用 Mardown 使用感受

  1. 写代码,写博文,这种方式很爽。
  2. 以前用其他编辑器写博文,会有很多样式干扰,比如复制编辑器中的内容,会把格式也复制进来,造成 html 的臃肿(看着很多重复的 span 标记,就是不爽)。
  3. 修改起来很方便,比如修改插入的代码,直接在里面修改就可以了。
  4. 方便统一博文内容整体的样式。
  5. 写起来超迅速,流畅,这篇博文内容也不是很少,历时几个小时(平常会多点),写起来的“手感”很好。
  6. 当然是简约了,但不失简单。
  7. 。。。。。

CNBlogs 使用 Mardown 使用小技巧

  1. 如果博文是使用 Mardown 编写的,正文的 div 会添加一个 cnblogs-markdown class 样式,这样方便我们修改用 Mardown 写的博文样式,比如修改字体,就可以添加如下样式:.cnblogs-markdown p { font-size: 15px; }。
  2. 可以使用 Mardown 在线编辑器,这样可以一边写,一边查看样式,然后再复制到 CNBlogs 中。
  3. 暂时发现这么多,后面再补充。。。

回到正题,关于 Value Object(值对象)如何使用 EF 进行正确映射?你会发现,其实也就是这一点内容,但都是踩着坑走过来的,需要注意的是,在进行映射配置的时候,要始终记得:映射配置不能影响到领域模型,也就是说,如果映射配置出现了问题,不能从领域模型中去找解决方案,这是技术问题,不能污染到领域模型。

关于领域驱动设计的实践-MessageManager,也开发不少时间了,同时也整理了几篇博文,如果你对领域驱动设计感兴趣,可以访问下 DDD 标签 进行了解,后面有时间再做个详细总结,这篇内容就到这里,也感谢你可以看到这。

DDD 领域驱动设计-Value Object(值对象)如何使用 EF 进行正确映射的更多相关文章

  1. DDD领域驱动设计之聚合、实体、值对象

    关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真 ...

  2. DDD 领域驱动设计-“臆想”中的实体和值对象

    其他博文: DDD 领域驱动设计-三个问题思考实体和值对象 DDD 领域驱动设计-三个问题思考实体和值对象(续) 以下内容属于博主"臆想",如有不当,请别当真. 扯淡开始: 诺兰的 ...

  3. DDD 领域驱动设计-三个问题思考实体和值对象(续)

    上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...

  4. DDD 领域驱动设计-三个问题思考实体和值对象

    消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A... 现有设计:消息设计为实体并为聚合根,发件人.收件人设计为值对象. 三个问题: 实体最重要的特性是什么? Messag ...

  5. C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)

    前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...

  6. C#进阶系列——DDD领域驱动设计初探(五):AutoMapper使用

    前言:前篇搭建了下WCF的代码,就提到了DTO的概念,对于为什么要有这么一个DTO的对象,上章可能对于这点不太详尽,在此不厌其烦再来提提它的作用: 从安全上面考虑,领域Model都带有领域业务,让Cl ...

  7. C#进阶系列——DDD领域驱动设计初探(六):领域服务

    前言:之前一直在搭建项目架构的代码,有点偏离我们的主题(DDD)了,这篇我们继续来聊聊DDD里面另一个比较重要的知识点:领域服务.关于领域服务的使用,书中也介绍得比较晦涩,在此就根据博主自己的理解谈谈 ...

  8. DDD领域驱动设计之领域基础设施层

    1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 其实这里说的基础设施层只是领域层的一些接口和基类而已,没有其他的如日子工具等代码,仅仅是为了说明领域层的一些基础 ...

  9. DDD领域驱动设计和实践(转载)

    -->目录导航 一. DDD领域驱动设计介绍 1. 什么是领域驱动设计(DDD) 2. 领域驱动设计的特点 3. 如果不使用DDD? 4. 领域驱动设计的分层架构和构成要素 5. 事务脚本和领域 ...

  10. 基于事件驱动的DDD领域驱动设计框架分享(附源代码)

    原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...

随机推荐

  1. java-通过JDBC操作数据库

    一.加载驱动 这里我们用Class.forname();来加载驱动,用此语句会提示排除异常. Class.forName("com.mysql.jdbc.Driver"); 括号中 ...

  2. cordova插件开发注意事项

    1. 编写插件,先创建好cordova项目之后,在项目里开发调试好在去创建插件目录 如何在cordova项目里创建呢,在android文件夹下面的res/xml/config.xml里去加入插件 例如 ...

  3. Tomcat 配置续

    为了便于tomcat升级 不用频繁重新修改相关配置文件 不用重新部署原有项目 新建目录将tomcat安装目录中的conf,log,webapp,work文件夹复制到里面,然后将系统变量里的CATALI ...

  4. linux安装VMware-tools,

    系统中可能预装了open-vm-tools和VMware-tools冲突,所以需要先将前者卸载在进行安装不同系统卸载使用的命令不一样,centos的命令可以使用rpm,ubuntu的命令可以使用dpk ...

  5. 前端-SEO

    SEO是 search Engine Optimization   (搜索引擎优化) SEO: ①白帽SEO(普通SEO做的优化) 网站标题.关键字.描述 网站内容优化 Robot.txt文件 网站地 ...

  6. Android中SQLite数据库小计

    2016-03-16 Android数据库支持 本文节选并翻译<Enterprise Android - Programing Android Database Applications for ...

  7. 初探ReactJS.NET 开发

    ReactJS通常也被称为"React",是一个刚刚在这场游戏中登场的新手.它由Facebook创建,并在2013年首次发布.Facebook认为React在处理SPA问题上可以成 ...

  8. 基于zookeeper实现统一资源管理

    分布式系统中经常涉及到配置资源的管理,比如,一个应用系统需要部署在多台服务器上,但是他们拥有某些的配置项是相同的,如果配置变更,需要修改这些配置,那么需要同时修改每台服务器,这样做比较麻烦而且容易出错 ...

  9. 从零3D基础入门XNA 4.0(1)——3D开发基础

    [题外话] 最近要做一个3D动画演示的程序,由于比较熟悉C#语言,再加上XNA对模型的支持比较好,故选择了XNA平台.不过从网上找到很多XNA的入门文章,发现大都需要一些3D基础,而我之前并没有接触过 ...

  10. 深入挖掘.NET序列化机制——实现更易用的序列化方案

    .NET框架为程序员提供了“序列化和反序列化”这一有力的工具,使用它,我们能很容易的将内存中的对象图转化为字节流,并在需要的时候再将其恢复.这一技术的典型应用场景包括[1] : 应用程序运行状态的持久 ...