阅读目录:
在上一篇《一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?》博文中,探讨的是如何聚焦领域模型(抛开一些干扰因素,才能把精力集中在领域模型的设计上)?需要注意的是,上一篇我讲的并不是如何设计领域模型(本篇也是)?而是如何聚焦领域模型,领域模型的设计是个迭代过程,不能一概而论,还在路上。
当有一个简单的领域模型用例,完成一个从上而下过程的时候,就需要对领域模型和数据库进行对象关系映射(ORM),首先,在领域驱动设计中,领域模型是活的(具有自己的行为和状态),而映射到数据库中所谓的表是死的(只是一些字段),如何把活的变成死的?又如何把死的变成活的?更重要的是如何保证在这个“死去活来”的过程中,死的和活的是同一个?
转换过程很简单,使用 ORM(对象关系映射)工具就很方便的完成这个“死去活来”的过程,但是有时候我们在这个转换过程中,可能会失去转换对象的本质,以致活的会变成死的,最后转换过程就只有死的变成死的(反复循环)。
在 MessageManager 项目的上一个版本中,主要存在两个领域模型:Messgae 和 User,他们数据库之间的映射关系是一对多的关系,就是一个用户拥有多个消息,但是一个消息只能对应一个用户(发件人或收件人),我们看下领域模型的设计(暂不包含业务逻辑)。
Domain Model-Message:
1 namespace MessageManager.Domain.DomainModel 2 { 3 public class Message : IAggregateRoot 4 { 5 #region 构造方法 6 public Message() 7 { 8 this.ID = Guid.NewGuid().ToString(); 9 } 10 #endregion 11 12 #region 实体成员 13 public string FromUserID { get; set; } 14 public string FromUserName { get; set; } 15 public string ToUserID { get; set; } 16 public string ToUserName { get; set; } 17 public string Title { get; set; } 18 public string Content { get; set; } 19 public DateTime SendTime { get; set; } 20 public bool IsRead { get; set; } 21 public virtual User FromUser { get; set; } 22 public virtual User ToUser { get; set; } 23 #endregion 24 25 #region IEntity成员 26 /// <summary> 27 /// 获取或设置当前实体对象的全局唯一标识。 28 /// </summary> 29 public string ID { get; set; } 30 #endregion 31 } 32 }
Domain Model-User:
1 namespace MessageManager.Domain.DomainModel 2 { 3 public class User : IAggregateRoot 4 { 5 #region 构造方法 6 public User() 7 { 8 this.ID = Guid.NewGuid().ToString(); 9 } 10 #endregion 11 12 #region 实体成员 13 public string Name { get; set; } 14 public virtual ICollection<Message> SendMessages { get; set; } 15 public virtual ICollection<Message> ReceiveMessages { get; set; } 16 #endregion 17 18 #region IEntity成员 19 /// <summary> 20 /// 获取或设置当前实体对象的全局唯一标识。 21 /// </summary> 22 public string ID { get; set; } 23 #endregion 24 } 25 }
乍一看,Message 和 User 领域模型并没有什么问题,只是设计的太贫血(只是包含一些属性字段),抛开业务逻辑,我们看下 Message 和 User 之间的关联,Message 模型中拥有 FromUserID,FromUserName,ToUserID,ToUserName 字段,用来表示和 User 模型的关联,Navigation Properties(导航属性)为:FromUser 和 ToUser,类型为 User,再看一下 User 模型的导航属性:SendMessages 和 ReceiveMessages,类型为 ICollection<Message>,我们如果按照平常的开发模式(脚本驱动模式),这样设计没有一点问题,很方便对 ORM 进行配置:
1 /// <summary> 2 /// Initializes a new instance of <c>MessageConfiguration</c> class. 3 /// </summary> 4 public MessageConfiguration() 5 { 6 HasKey(c => c.ID); 7 Property(c => c.ID) 8 .IsRequired() 9 .HasMaxLength(36) 10 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); 11 Property(c => c.FromUserID) 12 .IsRequired() 13 .HasMaxLength(36); 14 Property(c => c.ToUserID) 15 .IsRequired() 16 .HasMaxLength(36); 17 Property(c => c.Title) 18 .IsRequired() 19 .HasMaxLength(50); 20 Property(c => c.Content) 21 .IsRequired() 22 .HasMaxLength(2000); 23 Property(c => c.SendTime) 24 .IsRequired(); 25 Property(c => c.IsRead) 26 .IsRequired(); 27 ToTable("Messages"); 28 29 // Relationships 30 this.HasRequired(t => t.FromUser) 31 .WithMany(t => t.SendMessages) 32 .HasForeignKey(t => t.FromUserID) 33 .WillCascadeOnDelete(false); 34 this.HasRequired(t => t.ToUser) 35 .WithMany(t => t.ReceiveMessages) 36 .HasForeignKey(t => t.ToUserID) 37 .WillCascadeOnDelete(false); 38 }
上面代码表示 Message 的映射配置,如果外键可以为 NULL,则使用 HasOptional 方法,多对对则使用 HasMany 和 WithMany,WillCascadeOnDelete 用来级联删除配置,EntityTypeConfiguration 的具体详细配置,请参照:http://msdn.microsoft.com/zh-cn/data/jj591620.aspx。
上面的设计到底有没有问题?我们来分析一下,首先 User 领域模型中的 SendMessages 和 ReceiveMessages 属性,如果单独作为导航属性,这是没有什么问题的,因为我们可以使用导航属性很方便的进行映射配置(比如上面代码),但是放在领域模型中就有点不伦不类了,User 是一个用户对象,我们不能在它的身上来挂一些属于它的东西,因为这些并不是用户本身所具有的,这就好像我设计一个用户模型,它拥有手机,电脑,背包,房子,车子等等,然后就必须在这个用户模型中添加这个属性,这样设计就会很不合理,这个应该设计在它所拥有的物品上,因为只有这些物品拥有用户,这些物品相对于用户来说,才有真正的存在意义。
再来看 Message 领域模型,首先 FromUserID,FromUserName,ToUserID,ToUserName 这四个字段就让我们看得很不顺眼,因为这些都是已死的字段,Message 应该关联的是活的 User,而并不是在它身上打上几个 User 的标签,这个表现应该在数据库中(因为数据库中就是存的这些已死的字段),而并不是在活的 Message 领域模型中,FromUser 和 ToUser 的设计是没有问题的,因为关联的就是活的 User 对象。
为什么有了 FromUser 和 ToUser 对象,Message 领域模型中还要添加上面那四个字段呢?主要原因还是受思维模式的影响(脚本驱动模式),虽然是基于领域模型设计,但是在设计过程中就会不自觉的往脚本驱动模式上套,为什么?因为我们要使用数据库,不管怎么设计,这些对象都是要存在数据库中的,而数据库存的都是一些已死的对象(只是包含字段),对象死了,那怎么来表示 Message 和 User 对象之间的关联呢?答案就是 FromUserID 和 ToUserID,因为只有通过这两个字段,才能在数据库中体现 Message 和 User 对象之间的关联,数据库存储中确实是这么做的,但是我们把数据库中的关联表现在领域模型中就很不合适了,最后的结果就是 FromUser 和 ToUser 对象的作用只是用来映射配置,Message 领域模型变成和数据库中的 Message 表一样,状态都是已死,转换也就是死的对象转换为死的对象。
那到底怎么设计?答案就是把 Message 领域模型中的 FromUserID,FromUserName,ToUserID,ToUserName 四个属性去掉,User 领域模型中的 SendMessages 和 ReceiveMessages 属性也去掉,让领域模型变得干净。那有人会问了,你把这些关联字段去掉了,怎么去映射数据库呢?天无绝人之路,使用 EntityFramework(ORM 工具之一)就很方便的进行映射配置,具体配置,可以看下枚举映射和关联映射两个节点。
本节点纯属扯淡,兄台们不感兴趣的话,可以直接略过。
“数据库已死”的这个概念,并不是本人提出的,早在六年前在解道中就有人提出,具体可以参考:
首先,强调一点,数据库已死的概念,并不是说我们项目中不使用数据库(想想应用程序不使用数据库也不可能),只是说应用程序设计的核心不再是基于数据库设计的,而应该是基于面向对象设计,数据库只是存储数据的一种方式,当然也可以配置文件存储或者内存存储。以往我们进行应用程序设计的时候,都是先根据业务需求定义表结构,然后根据表结构用“面向对象”的语言去传递 SQL 放到数据库中执行,这样面向对象语言就成了所谓的 SQL 搬运工,这样造成的问题就是非常难维护,牵一发而动全身,而且性能瓶颈也主要体现在数据库方面,想想应用程序的性能问题(排除代码问题),我们可以使用负载均衡增加服务器,来分担所带来的压力,而应对数据库性能问题呢?从“MySpace”的经历上就可以看出,那是相当的难处理,而且性能问题主要集中在数据库方面,也是设计的不合理所造成的。
我们来看一下 MySqace 的信息系统发展历程:
总结:从 MySpace 看更加验证,数据库是软件系统的瓶颈,而且最不可伸缩,一旦数据库成为系统瓶颈,就得动大手术,实现架构上的变迁,这是伤筋动骨,变迁人员压力巨大的。另外由于是社区,就是变迁数据丢失也没什么大不了,如果是企业那就......
如果我们从软件系统开始之初,就使用对象分析设计,不与数据库沾边,整个流程就完全 OO,分析设计直至代码都摆脱了数据库影响,这个流程如下:
那么数据库在什么时候建立呢?数据库表结构的创建可以延缓到部署运行时,这样,整个上游环节就不涉及数据库技术,而是使用更符合自然的表达 OO 方式,软件质量就更高了。现在,很多人已经理解,分析设计要用 OO,但是数据库是运行阶段缺少不了的,确实,这是正确观点,我们夺取数据库的王位,不是将它打倒,只是理性和平移交权力重心而已,数据库退出主角地位,让位于中间件,也预示着过去数据库为王的时代的结束, 但是数据库会和操作系统一样,成为我们现代软件系统一个不可缺少重要的基础环节。
了解了这么多,回到”设计误区“这一节点,你会发现,造成设计误区的主要原因还是,在设计的时候不自觉以数据库为中心了,而并非领域模型。
在 Message 领域模型中,有个 MessageState 枚举类型,用来表示消息的状态,当然我们也可以使用 Bool 类型的字段来表示,但是消息状态是消息本身的一种状态,用对象来表示更为合适,MessageState 枚举定义如下:
1 namespace MessageManager.Domain.DomainModel 2 { 3 public enum MessageState 4 { 5 Read, 6 NoRead 7 } 8 }
我们使用单元测试对映射转换进行测试,也就是 Code First 模式,测试代码:
1 namespace MessageManager.Repositories.Tests 2 { 3 public class UserRepositoryTest 4 { 5 [Fact] 6 public void AddUserRepository() 7 { 8 IUserRepository userRepository = new UserRepository(new EntityFrameworkRepositoryContext()); 9 User user1 = new User("小菜"); 10 User user2 = new User("大神"); 11 userRepository.Add(user1); 12 userRepository.Add(user2); 13 userRepository.Context.Commit(); 14 } 15 } 16 }
生成数据库发生异常:
这个主要原因是当前 EntityFramework 版本不支持枚举类型映射,当前使用的 EntityFramework 版本为 4.3.1:
1 <?xml version="1.0" encoding="utf-8"?> 2 <packages> 3 <package id="EntityFramework" version="4.3.1" targetFramework="net40" /> 4 </packages>
EntityFramework 的版本太老了,更新版本为 6.1.1,NuGet 更新命令:update-package EntityFramework
EntityFramework 从 4.3.1 升级到 6.1.1 更改的地方(http://msdn.microsoft.com/en-us/data/upgradeef6.aspx):
在升级完 EntityFramework 版本后,重新运行单元测试,但是发现又报如下错误:
解决方案:
在 MessageManagerDbContext 构造函数中添加如下代码:
1 public MessageManagerDbContext() 2 : base("MessageManagerDB") 3 { 4 var ensureDLLIsCopied = System.Data.Entity.SqlServer.SqlProviderServices.Instance; 5 this.Configuration.LazyLoadingEnabled = true; 6 }
重新运行单元测试,测试成功,就会发现在 MessageManagerDB 数据库的 Messages 表中已生成 State 字段,类型为 Int,当然也可以通过 EntityTypeConfiguration 中的 HasColumnType 进行自定义字段类型。
先看一下,如果我们没有进行任何的 EntityTypeConfiguration 关联设置,生成数据库会是怎样?MessageConfiguration 和 UserConfiguration 配置如下:
1 public class MessageConfiguration : EntityTypeConfiguration<Message> 2 { 3 /// <summary> 4 /// Initializes a new instance of <c>MessageConfiguration</c> class. 5 /// </summary> 6 public MessageConfiguration() 7 { 8 HasKey(c => c.ID); 9 Property(c => c.ID) 10 .IsRequired() 11 .HasMaxLength(36) 12 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); 13 Property(c => c.Title) 14 .IsRequired() 15 .HasMaxLength(50); 16 Property(c => c.Content) 17 .IsRequired() 18 .HasMaxLength(2000); 19 Property(c => c.SendTime) 20 .IsRequired(); 21 } 22 }
1 /// <summary> 2 /// Represents the entity type configuration for the <see cref="Customer"/> entity. 3 /// </summary> 4 public class UserConfiguration : EntityTypeConfiguration<User> 5 { 6 #region Ctor 7 /// <summary> 8 /// Initializes a new instance of <c>UserConfiguration</c> class. 9 /// </summary> 10 public UserConfiguration() 11 { 12 HasKey(c => c.ID); 13 Property(c => c.ID) 14 .IsRequired() 15 .HasMaxLength(36) 16 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); 17 Property(c => c.Name) 18 .IsRequired() 19 .HasMaxLength(20); 20 } 21 #endregion 22 }
上面代码中我们并没有进行关联配置,生成 MessageManagerDB 数据库中 Messages 表结构:
可以看到我们虽然没有进行任何的关联设置,Code First 会自动为我们创建外键关联,仅仅是在 Message 领域模型中添加:
1 public virtual User SendUser { get; set; } 2 public virtual User ReceiveUser { get; set; }
以上效果是我们想要的,这也是 EntityFramework 的进步之处,符合领域驱动设计的思想,领域模型中没有数据库中所谓的主外键关联,有的只是对象之间的关联,而数据库只是存储数据的一种表现,这样数据库设计的概念就不存在了,也让我们忘了数据库的存在,而把更多的精力放在领域模型的设计上,这就是领域驱动设计关键所在。
除了 EntityFramework 默认生成关联配置,我们也可以进行自定义配置,比如,上面生成外键字段为:SendUser_ID 和 ReceiveUser_ID,也可以自定义字段名称:
1 HasRequired(x => x.SendUser) 2 .WithMany() 3 .Map(x => x.MapKey("SendUserID")) 4 .WillCascadeOnDelete(false); 5 HasRequired(x => x.ReceiveUser) 6 .WithMany() 7 .Map(x => x.MapKey("ReceiveUserID")) 8 .WillCascadeOnDelete(false);
上面就是自定义外键字段为:SendUserID 和 ReceiveUserID,关于 EntityTypeConfiguration 的配置,比如一对一,一对多,多对多,联合主外键等等,
文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态
文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境
文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn
文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker
文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机
文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk
文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入
文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。 Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。
文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动
文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计
文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;gt;Jni-&amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图
文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法