孝感做网站公司,做网站负责人有法律风险吗,无锡网站排名哪家好,莱芜雪野湖游玩攻略领域驱动设计主要通过限界上下文应对复杂度#xff0c;它是绑定业务架构、应用架构和数据架构的关键架构单元。设计由领域而非数据驱动#xff0c;且为了保证定义了领域模型的应用架构和定义了数据模型的数据架构的变化方向相同#xff0c;就应该在领域建模阶段率先定义领域…领域驱动设计主要通过限界上下文应对复杂度它是绑定业务架构、应用架构和数据架构的关键架构单元。设计由领域而非数据驱动且为了保证定义了领域模型的应用架构和定义了数据模型的数据架构的变化方向相同就应该在领域建模阶段率先定义领域模型再根据领域模型定义数据模型。这就是领域驱动设计与数据驱动设计的根本区别。
一、对象关系映射
如果领域建模采用对象建模范式存储数据则使用关系数据库那么领域模型就是面向对象的数据模型则是面向关系表的。在领域驱动设计中领域模型一方面充分地表达了系统的领域逻辑同时还映射了数据模型作为持久化对象完成数据的读写。要持久化领域模型对象需要为对象与关系建立映射即所谓的“对象关系映射”(object relationship mappingORM)。当然这主要针对关系数据库。
对象与关系往往存在“阻抗不匹配”的问题主要体现为以下3个方面。
类型的阻抗不匹配例如不同关系数据库对浮点数的不同表示方法字符串类型在数据库的最大长度约束等又例如Java等语言的枚举类型本质上仍然属于基本类型关系数据库中却没有对应的类型来匹配。样式的阻抗不匹配领域模型与数据模型不具备一一对应的关系。领域模型是一个具有嵌套层次的对象图结构数据模型在关系数据库中却是扁平的关系结构要让数据库能够表示领域模型就只能通过关系来变通地映射实现。对象模式的阻抗不匹配面向对象的封装、继承和多态无法在关系数据库得到直观体现。通过封装可以定义一个高内聚的类来表达一个细粒度的基本概念但数据表往往不这么设计。数据表只有组合关系无法表达对象之间的继承关系。既然无法实现继承关系就无法满足Liskov替换原则自然也就无法满足多态。
二、解决方式
一使用JPA
1.枚举类型
关系数据库的基本类型没有枚举类型。如果领域模型的字段定义为枚举通常会在数据库中将相应的列定义为smallint类型然后通过Enumerated表示枚举的含义。
public enum EmployeeType {Hourly, Salaried, Commission
}
public class Employee {EnumeratedColumn(columnDefinition smallint)private EmployeeType employeeType;
}smallint虽然能够体现值的有序性但在管理和运维数据库时查询得到的枚举值却是没有任何业务含义的数字制造了理解障碍。为此可将列定义为VARCHAR而在领域模型中定义枚举
public enum Gender {Male, Female
}
public class Employee {Enumerated(EnumType.STRING)private Gender gender;
}注解Enumerated(EnumType.STRING)可将枚举类型转换为字符串。注意数据库的字符串应与枚举类型的字符串值以及大小写保持一致。
2.日期类型
处理针对Java的日期和时间类型进行映射要相对复杂一些因为Java定义了多种日期和时间类型
用以表达数据库日期类型的java.sql.Date类和表达数据库时间类型的java.sql.Timestamp类Java库用以表达日期、时间和时间戳类型的java.util.Date类或java.util.Calendar类Java 8引入的新日期类型java.time.LocalDate类与新时间类型java.time.LocalDateTime类。数据库本身支持java.sql.Date或java.sql.Timestamp类型若领域模型对象的日期或时间字段属于这一类型则无须任何配置即可使用和使用其他基础类型一般自然。
通过columnDefinition属性值甚至还可以为其设置默认值例如设置为当期日期
Column(name START_DATE, columnDefinition DATE DEFAULT CURRENT_DATE)
private java.sql.Date startDate;如果字段定义为java.util.Date或java.util.Calendar类型可通过Temporal注解将其映射为日期、时间或时间戳例如
Temporal(TemporalType.DATE)
private java.util.Calendar birthday;
Temporal(TemporalType.TIME)
private java.util.Date birthday;
Temporal(TemporalType.TIMESTAMP)
private java.util.Date birthday;如果字段定义为Java 8新引入的LocalDate或LocalDateTime类型情况稍显复杂取决于JPA的版本。
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.sql.Date;
import java.time.LocalDate;
Converter(autoApply true)
public class LocalDateAttributeConverter implements AttributeConverterLocalDate, Date {Overridepublic Date convertToDatabaseColumn(LocalDate locDate) {return locDate null ? null : Date.valueOf(locDate);}Overridepublic LocalDate convertToEntityAttribute(Date sqlDate) {return sqlDate null ? null : sqlDate.toLocalDate();}
}3.主键类型
关系数据库表的主键列至为关键通过它可以标注每一行记录的唯一性。
主键还是建立表关联的关键列通过主键与外键的关系可以间接支持领域模型对象之间的导航同时也保证了关系数据库的完整性。无论是单一主键还是联合主键主键作为身份标识(identity)只要能够确保它在同一张表中的唯一性原则上都可以被定义为各种类型如BigInt、VARCHAR等。在数据表定义中只要某个列被声明为PRIMARY KEY在领域模型对象的定义中就可以使用JPA提供的Id注解。
这个注解还可以和Column注解组合使用
Id
Column(name employeeId)
private int id;主流关系数据库都支持主键的自动生成JPA提供了GeneratedValue注解说明了该主键通过自动生成。该注解还定义了strategy属性用以指定自动生成的策略。JPA还定义了SequenceGenerator与TableGenerator等特殊的ID生成器。
在建立领域模型时我们强调从领域逻辑出发考虑领域类的定义。尤其对实体类而言ID代表的是实体对象的身份标识。
它与数据表的主键有相似之处例如二者都要求唯一性但二者的本质完全不同前者代表业务含义后者代表技术含义前者用于对实体对象生命周期的管理与跟踪后者用于标记每一行在数据表中的唯一性。
领域驱动设计往往建议定义值对象作为实体的身份标识。一方面值对象类型可以清晰表达该身份标识的业务含义另一方面值对象类型的封装也有利于应对未来主键类型可能的变化。
JPA定义了一个特殊的注解EmbeddedId来建立数据表主键与身份标识值对象之间的映射。例如为Employee实体对象定义了EmployeeId值对象则Employee的定义为
Entity
Table(nameemployees)
public class Employee extends AbstractEntityEmployeeId implements AggregateRoot
Employee {EmbeddedIdprivate EmployeeId employeeId;
}JPA对主键类有两个要求相等性比较与序列化支持即需要主键类实现Serializable接口并重写Object的equals()与hashcode()方法。值对象的类定义还需要声明Embeddable注解。由于框架需要通过反射创建值对象因此如果值对象定义了带参数的构造函数还需要为其定义默认的构造函数
Embeddable
public class EmployeeId implements IdentityString, Serializable {Column(name id)private String value;private static Random random;static {random new Random();}// 必须提供默认的构造函数public EmployeeId() {}private EmployeeId(String value) {this.value value;}Overridepublic String value() {return this.value;}public static EmployeeId of(String value) {return new EmployeeId(value);}public static IdentityString next() {return new EmployeeId(String.format(%s%s%s,composePrefix(),composeTimestamp(),composeRandomNumber()));}Overridepublic boolean equals(Object o) {if (this o) return true;if (o null || getClass() ! o.getClass()) return false;EmployeeId that (EmployeeId) o;return value.equals(that.value);}Overridepublic int hashCode() {return Objects.hash(value);}
}使用时可以直接传入EmployeeId对象作为主键查询条件
OptionalEmployee optEmployee employeeRepo.findById(EmployeeId.of(emp200109101000001));4.样式的阻抗不匹配
样式(schema)的阻抗不匹配就是对象图与关系表之间的不匹配。要做到二者的匹配需要做到图结构与表结构之间的互相转换。
在领域模型的对象图中一个实体组合了另一个实体由于两个实体都有各自的身份标识映射到数据库就可通过主外键关系建立关联。
关联关系包括一对一、一对多、多对一和多对多。
例如在领域模型中HourlyEmployee聚合根实体与TimeCard实体之间的关系可以定义为
Entity
Table(namehourly_employees)
public class HourlyEmployee extends AbstractEntityEmployeeId implements AggregateRoot
HourlyEmployee {EmbeddedIdprivate EmployeeId employeeId;OneToMany // 该注解定义了一对多关系JoinColumn(name employeeId, nullable false)private ListTimeCard timeCards new ArrayList();
}
Entity
Table(name timecards)
public class TimeCard {private static final int MAXIMUM_REGULAR_HOURS 8;IdGeneratedValueprivate String id;private LocalDate workDay;private int workHours;public TimeCard() {}
}在数据模型中timecards表通过外键employeeId建立与employees表之间的关联
CREATE TABLE hourly_employees(employeeId VARCHAR(50) NOT NULL,......PRIMARY KEY(employeeId)
);
CREATE TABLE timecards(id INT NOT NULL AUTO_INCREMENT,employeeId VARCHAR(50) NOT NULL,workDay DATE NOT NULL,workHours INT NOT NULL,PRIMARY KEY(id)
);如果对象图的实体和值对象之间形成了一对多的关联由于值对象没有唯一的身份标识因此它对应的数据模型也没有主键而将实体表的主键作为外键由此来表达彼此之间的归属关系。
这时领域模型仍然通过集合来表达一对多的关联但使用的注解并非OneToMany而是ElementCollection。
例如领域模型中的SalariedEmployee聚合根实体与Absence值对象之间的关系可以定义为
Embeddable
public class Absence {private LocalDate leaveDate;Enumerated(EnumType.STRING)private LeaveReason leaveReason;public Absence() {}public Absence(LocalDate leaveDate, LeaveReason leaveReason) {this.leaveDate leaveDate;this.leaveReason leaveReason;}
}
Entity
Table(namesalaried_employees)
public class SalariedEmployee extends AbstractEntityEmployeeId implements AggregateRoot
SalariedEmployee {private static final int WORK_DAYS_OF_MONTH 22;EmbeddedIdprivate EmployeeId employeeId;Embeddedprivate Salary salaryOfMonth;ElementCollectionCollectionTable(name absences, joinColumns JoinColumn(name employeeId))private ListAbsence absences new ArrayList();public SalariedEmployee() {}
}ElementCollection说明了字段absences是SalariedEmployee实体的字段元素类型为集合CollectionTable标记了关联的数据表以及关联的外键。其数据模型的SQL语句如下
CREATE TABLE salaried_employees(employeeId VARCHAR(50) NOT NULL,......PRIMARY KEY(employeeId)
);
CREATE TABLE absences(employeeId VARCHAR(50) NOT NULL,leaveDate DATE NOT NULL,leaveReason VARCHAR(20) NOT NULL
);数据表absences没有自己的主键employeeId列是employees表的主键。注意在Absence值对象的定义中无须再定义employeeId字段因为Absence值对象并不能脱离SalariedEmployee聚合根单独存在。这是聚合对领域模型产生的影响也可视为聚合的设计约束。
5.对象模式的阻抗不匹配
领域模型要符合面向对象的设计原则一个重要特征是建立了高内聚松耦合的对象图。
要做到这一点就需要将具有高内聚关系的概念封装为一个类通过显式的类型体现领域中的概念。
这样既提高了代码的可读性又保证了职责的合理分配避免出现一个庞大的实体类。领域驱动设计更强调这一点并因此引入了值对象的概念用以表现那些无须身份标识却又具有内聚知识的领域概念。
因此一个设计良好的领域模型往往会呈现出一个具有嵌套层次的对象图模型结构。虽然嵌套层次的领域模型与扁平结构的关系数据模型并不匹配但通过JPA提供的Embedded与Embeddable注解可以非常容易实现这一嵌套组合的对象关系例如Employee类的address属性和email属性
Entity
Table(nameemployees)
public class Employee extends AbstractEntityEmployeeId implements AggregateRoot
Employee {EmbeddedIdprivate EmployeeId employeeId;private String name;Embeddedprivate Email email;Embeddedprivate Address address;
}
Embeddable
public class Address {private String country;private String province;private String city;private String street;private String zip;public Address() {}
}
Embeddable
public class Email {Column(name email)private String value;public String value() {return this.value;}
}Address类和Email类都是Employee实体的值对象。注意为了支持JPA框架通过反射创建对象若为值对象定义了带参的构造函数需要显式定义默认构造函数。
EmployeeId类的定义与Address类的定义相同也属于值对象只是前者由于作为了实体的身份标识并映射了数据模型的主键因此应声明为EmbeddedId注解。无论是Address、Email还是EmployeeId类在领域对象模型中虽然被定义为独立的类但在数据模型中却都是employees表中的列。
其中Email类仅仅对应表中的一个列之所以要定义为类目的是在领域模型中体现电子邮件的领域概念并有利于封装对邮件地址的验证逻辑Address类封装了多个内聚的值体现为country、province等列以利于维护地址概念的完整性同时也可以实现对领域概念的复用。创建employees表的SQL脚本如下所示
CREATE TABLE employees(id VARCHAR(50) NOT NULL,name VARCHAR(20) NOT NULL,email VARCHAR(50) NOT NULL,employeeType SMALLINT NOT NULL,gender VARCHAR(10),currency VARCHAR(10),country VARCHAR(20),province VARCHAR(20),city VARCHAR(20),street VARCHAR(100),zip VARCHAR(10),mobilePhone VARCHAR(20),homePhone VARCHAR(20),officePhone VARCHAR(20),onBoardingDate DATE NOT NULLPRIMARY KEY(id)
);一个值对象如果在数据模型中被设计为一个独立的表由于无须定义主键依附于实体对应的数据表因此在领域模型中依旧标记为Embeddable。这既体现了面向对象的封装思想又表达了一对一或一对多的关系。
SalariedEmployee聚合中的Absence值对象就遵循了这样的设计原则。面向对象的封装思想体现了对细节的隐藏正确的封装还体现为对职责的合理分配。
遵循“信息专家模式”无论是针对领域模型中的实体还是针对值对象都应该从它们拥有的数据出发判断领域行为是否应该分配给这些领域模型类。
如HourlyEmployee实体类的payroll(Period)方法、Absence值对象的isIn(Period)与isPaidLeave()方法乃至于Salary值对象的add(Salary)等方法都充分体现了对领域行为的合理封装避免了贫血模型的出现
public class HourlyEmployee extends AbstractEntityEmployeeId implements AggregateRoot
HourlyEmployee {public Payroll payroll(Period period) {if (Objects.isNull(timeCards) || timeCards.isEmpty()) {return new Payroll(this.employeeId, period.beginDate(), period.endDate(),
Salary.zero());}Salary regularSalary calculateRegularSalary(period);Salary overtimeSalary calculateOvertimeSalary(period);Salary totalSalary regularSalary.add(overtimeSalary);return new Payroll(this.employeeId, period.beginDate(), period.endDate(), totalSalary);}
}
public class Absence {public boolean isIn(Period period) {return period.contains(leaveDate);}public boolean isPaidLeave() {return leaveReason.isPaidLeave();}
}
public class Salary {public Salary add(Salary salary) {throwExceptionIfNotSameCurrency(salary);return new Salary(value.add(salary.value).setScale(SCALE), currency);}public Salary subtract(Salary salary) {throwExceptionIfNotSameCurrency(salary);return new Salary(value.subtract(salary.value).setScale(SCALE), currency);}public Salary multiply(double factor) {return new Salary(value.multiply(toBigDecimal(factor)).setScale(SCALE), currency);}public Salary divide(double multiplicand) {return new Salary(value.divide(toBigDecimal(multiplicand), SCALE, BigDecimal.
ROUND_DOWN), currency);}
}这充分证明领域模型对象既可以作为持久化对象搭建起对象与关系表之间的桥梁又可以体现包含丰富领域行为在内的领域概念与领域知识。
合二者为一体的领域模型对象定义在领域层可被南向网关的资源库端口与适配器直接访问无须再定义单独的数据模型对象。前面提到的数据模型实际上指的是数据库中创建的数据表。
对象模式中的泛化关系通过继承体现更为特殊因为关系表自身不具备继承能力这与对象之间的关联关系不同。继承体现了“差异式编程”父类与子类以及子类之间存在属性的差异但在数据模型中却可以将父类与子类所有的属性无论差异都放在一张表中就好似对集合求并集一般。
这种策略在ORM中被称为Single-Table策略。为了区分子类的类型差异需要在这张单表中额外定义一个列作为区分子类的标识列对应的JPA注解为DiscriminatorColumn。例如如果Employee存在继承体系若选择Single-Table策略整个继承体系映射到employees表中则它的标识列就是employeeType列。
若子类之间的差异太大采用Single-Table策略实现继承会让数据表的行数据出现太多不必要的列又不得不为这些列提供存储空间。要避免这种存储空间的冗余可采用Joined-Subclass策略实现继承。
继承体系中的父实体与子实体在数据库中都有一个单独的表与之对应子实体对应的表无须为继承自父实体的属性定义列而是通过共享主键的方式与之关联。由于Single-Table策略是ORM默认的继承策略若要采用Joined-Subclass策略需要在父实体类的定义中显式声明继承策略如下所示
Entity
Inheritance(strategyInheritanceType.JOINED)
Table(nameemployees)
public class Employee {}采用Joined-Subclass策略实现继承时子实体与父实体在数据模型中的表现实则为一对一的连接关系这可以认为是为了解决对象关系阻抗不匹配的无奈之举毕竟用表的连接关系表达类的泛化关系怎么看怎么觉得别扭。
若领域模型中继承体系的子类较多这一设计还会影响查询效率因为它可能牵涉到多张表的连接。如果既不希望产生不必要的数据冗余又不愿意表连接拖慢查询的速度则可以采用Table- Per-Class策略。采用这种策略时继承体系中的每个实体类都对应一个独立的表与Joined-Subclass策略不同之处在于父实体对应的表仅包含父实体的字段子实体对应的表不仅包含了自身的字段同时还包含了父实体的字段。
这相当于用数据表样式的冗余避免数据的冗余、用单表来避免不必要的连接。如果子类之间的差异较大那么Table-Per-Class策略明显优于Joined-Subclass策略。
继承的目的绝不仅仅是复用甚至可以说复用并非它的主要价值毕竟“聚合/合成优先复用原则”已经成为面向对象设计的金科玉律。
继承的主要价值在于支持多态以利用Liskov替换原则使得子类能够替换父类而不改变其行为并允许定义新的子类来满足功能扩展的需求保证对扩展是开放的。在Java或C#中由于受到单继承的约束定义抽象接口以实现多态更为普遍。
无论是继承多态还是接口多态都应站在领域逻辑的角度思考是否需要引入合理的抽象来应对未来需求的变化。
在采用继承多态时需要考虑对应的数据模型是否能够在对象关系映射中实现继承并选择合理的继承策略以确定关系表的设计。如果继承多态与接口多态针对领域行为则与领域模型的持久化无关也就无须考虑领域模型与数据模型之间的映射。
6.瞬态领域模型
领域服务作为对领域行为的封装自然无须考虑持久化如果不是采用事件溯源模式领域事件也无须考虑持久化。位于聚合内部的实体和值对象需要持久化否则就无须引入资源库来管理它们的生命周期了。
除此之外在设计领域模型时往往会发现存在一些游离在聚合边界外的领域对象它们拥有自己的属性值体现了高内聚的领域概念并遵循“信息专家模式”封装了操作自身信息的领域行为但却没有身份标识无须进行持久化例如与HourlyEmployee聚合根交互的Period类其作用是体现一个结算周期作为薪资计算的条件
public class Period {private LocalDate beginDate;private LocalDate endDate;public Period(LocalDate beginDate, LocalDate endDate) {this.beginDate beginDate;this.endDate endDate;}public Period(YearMonth yearMonth) {int year yearMonth.getYear();int month yearMonth.getMonthValue();int firstDay 1;int lastDay yearMonth.lengthOfMonth();this.beginDate LocalDate.of(year, month, firstDay);this.endDate LocalDate.of(year, month, lastDay);}
public Period(int year, int month) {if (month 1 || month 12) {throw new InvalidDateException(Invalid month value.);}
int firstDay 1;int lastDay YearMonth.of(year, month).lengthOfMonth();
this.beginDate LocalDate.of(year, month, firstDay);this.endDate LocalDate.of(year, month, lastDay);}
public LocalDate beginDate() {return beginDate;}
public LocalDate endDate() {return endDate;}
public boolean contains(LocalDate date) {if (date.isEqual(beginDate) || date.isEqual(endDate)) {return true;}return date.isAfter(beginDate) date.isBefore(endDate);}
}结算周期提供了成对的起止日期缺少任何一个日期就无法正确地进行薪资计算。将beginDate与endDate封装到Period类中再利用构造函数限制实例的创建就能避免起止日期任意一个值的缺失。
引入Period类还能封装领域行为让对象之间的协作变得更加合理。它的类型没有声明Entity并不需要持久化也没有被定义在聚合边界内。为示区别可将这样的类称为瞬态类(transientclass)由此创建的对象则称为瞬态对象。
对应地倘若在一个支持持久化的领域类中需要定义一个无须持久化的字段可将其称为瞬态字段(transient field)。JPA定义了Transient注解用以显式声明这样的字段例如
Entity
Table(nameemployees)
public class Employee extends AbstractEntityEmployeeId implements AggregateRoot
Employee {EmbeddedIdprivate EmployeeId employeeId;private String firstName;private String middleName;private String lastName;Transientprivate String fullName;
}Employee类对应的数据模型定义了firstName、middleName和lastName列。为了调用方便该类又定义了fullName字段。该值并不需要持久化到数据库中 因此声明为瞬态字段。瞬态类属于领域模型的一部分。相较于聚合内的实体和值对象它更加纯粹无须依赖任何外部框架属于真正的POJO类它的设计符合整洁架构思想即处于内部核心的领域类不依赖任何外部框架。
7.JPA使用注意事项
我们可以使用 JPA 的级联更新实现聚合根的持久化。在实际操作中发现JPA 并不好用。其实这不是 JPA 的问题是因为 JPA 做的太多了JPA 不仅有各种状态转换还有多对多关系。
如果保持克制就可以使用 JPA 实现 DDD尝试遵守下面的规则
不要使用 ManyToMany 特性多对多关系太复杂。只给聚合根配置 Repository 对象。聚合根内有其他内部实体虽然需要持久化但不要为它配置Repository对象。避免造成网状的关系互相循环依赖。读写分离。关联等复杂查询读写分离查询不要给 JPA 做JPA 只做单个对象的查询复杂查询可以给mybatis做。
二、领域模型与数据模型
在领域模型内部聚合是最小的设计单元资源库是持久化实现的抽象。一个资源库对应一个聚合故而聚合也是领域模型最小的持久化单元。
当领域模型引入限界上下文与聚合之后领域模型类与数据表之间就有可能突破类与表之间一一对应的关系。
因此在遵循领域驱动设计原则实现持久化时需要考虑领域模型与数据模型之间的关系而在进行领域建模时一定是先有领域模型后有数据模型
在定义了领域模型之后将其映射为数据模型时不能破坏限界上下文和聚合确定的边界。
至于聚合内部的实体和值对象则不必保证类与表的一对一关系也不应该将其设计为一对一关系。不能忽视物理边界对架构的影响。
限界上下文以进程为物理边界确定了与业务架构对应的应用架构。进程内与进程间对领域模型的调用方式迥然不同。菱形对称架构限制了进程内直接调用领域模型的方式这就为应用架构提供了演进的可能。
在限界上下文与菱形对称架构的基础上系统的应用架构可以很容易地从单体架构演进到微服务架构。那么数据架构能无缝演进吗数据模型以数据库为物理边界数据表为逻辑边界由此确定了数据架构。
但是限界上下文的物理边界无法做到与数据模型物理边界的一对一关系例如数据库共享架构就破坏了这种关系。
此时就需要逻辑边界的约束力。领域模型必须与数据模型建立映射关系才能使资源库适配器通过ORM框架进行持久化。
领域模型属于哪一个数据库领域模型类属于哪一个数据表类属性属于哪一个数据列都是通过映射关系来配置和表达的。这种映射关系并不受数据库边界的影响。只要保证数据模型的逻辑边界与限界上下文的逻辑边界保持一致就能保证数据架构的演进能力前提是数据模型需按照领域模型进行设计。
以薪资管理系统为例员工管理和薪资结算分属两个不同的限界上下文员工上下文和薪资上下文。
员工上下文关注员工基本信息的管理薪资上下文需要对各种类型的员工进行薪资结算。
既然限界上下文是领域模型的知识语境就可以在这两个限界上下文中同时定义员工Employee领域类在领域设计模型中体现为不同的聚合。
根据领域模型设计数据模型就应该为不同限界上下文的员工领域概念建立不同的员工数据表。
考虑到限界上下文物理边界的不同数据模型存在两种不同的设计方案。
进程内边界设计为单库多表所有限界上下文共享同一个数据库员工上下文的员工领域模型映射为员工表薪资上下文的员工领域模型各自映射对应员工类型的员工表表之间由共同的员工ID进行关联。这一方案满足单体架构风格。进程间边界设计为多库多表为不同限界上下文建立不同的数据库数据表的定义与单库多表一致。这一方案符合微服务架构风格。无论数据模型采用哪一种设计方案领域模型都几乎不会受到影响唯一的影响是ORM元数据定义需要修改对库的映射。如图所示的领域模型代码结构不受数据模型设计方案的影响。 在领域模型中员工上下文的Employee聚合根实体与薪资上下文的HourlyEmployee、SalariedEmployee和CommissionedEmployee这3个聚合根实体之间存在隐含的员工ID关联。设计数据模型时这4个聚合根实体对应4张数据主表它们的id主键都是员工ID彼此之间的关系如图所示。 员工领域类的设计充分体现了限界上下文作为领域模型的知识语境而数据模型与领域模型的对应关系又充分支持了限界上下文对业务能力的纵向切分。领域模型的战略设计与战术设计就是通过限界上下文和聚合的边界有机融合起来的。
三、聚合的持久化
使用JPA实现领域驱动设计的领域模型持久化虽然很方便但是还是有以下问题
1.领域模型引入了技术因素各领域模型增加了Entity、Column等与数据库表相关的注解当设计领域模型时首先肯定没考虑数据库的因素而是考虑业务因素2.JPA对多表查询的支持很差若对报表有很强的需求使用JPA进行实现需要绕很多弯子
另外在对mysql这样的关系型数据库时聚合的持久化也有以下问题
关系的映射不好处理层级比较深的对象不好转换。将数据转换为聚合时会有 n1 的问题不好使用关系数据库的联表特性。全量的数据更新数据库的事务较大性能低下。
聚合的持久化是 DDD 美好愿景落地的最大拦路虎这些问题有部分可以被解决而有部分必须取舍。
一自己实现Repository
一般一个聚合对应一个资源库若不使用JPA进行实现则可以使用mybatis进行实现那么需要自己实现Repository的功能。
使用 Mybatis Mapper对 Mapper 再次封装。
class OrderRepository {private OrderMapper orderMapper;private OrderItemMapper orderItemMapper;public Order get(String orderId) {Order order orderMapper.findById(orderId);order.setOrderItems(orderItemMapper.findAllByOrderId(orderId))return order;}
}这种做法有一个小点问题领域对象 Order 中有 orderItems 这个属性但是数据库中不可能有 Items一些开发者会认为这里的 Order 和通常数据库使用的 OrderEntity 不是一类对象于是进行繁琐的类型转换。
类型转换和多余的一层抽象加大了工作量。
如果使用 Mybatis其实更好的方式是直接使用 Mapper 作为 Repository 层并在 XML 中使用动态 SQL 实现上述代码。
还有一个问题是一对多的关系发生了移除操作怎么处理呢
比较简单的方式是直接删除再存入新的数组即可也可以实现对象的对比记录对象的历史版本有选择的进行新增、删除和更新。完成了这些恭喜你你变相实现了JPA的特性。
二使用 Spring Data JDBC
Mybatis 就是一个 SQL 模板引擎而 JPA 做的太多有没有一个适中的 ORM 来持久化聚合呢
Spring Data JDBC 就是人们设计出来持久化聚合从名字来看他不是 JDBC而是使用 JDBC 实现了部分 JPA 的规范让你可以继续使用 Spring Data 的编程习惯。
Spring Data JDBC 的一些特点
没有 Hibernate 中 session 的概念没有对象的各种状态没有懒加载保持对象的完整性除了 Spring Data 的基本功能保持简单只有保存方法、事务、审计注解、简单的查询方法等。可以搭配 JOOQ 或 Mybatis 实现复杂的查询能力。
需要注意的是Spring Data JDBC 的逻辑
如果聚合根是一个新的对象Spring Data JDBC 会递归保存所有的关联对象。如果聚合根是一个旧的对象Spring Data JDBC 会删除除了聚合根之外旧的对象再插入聚合根会被更新。因为没有之前对象的状态这是一种不得不做的事情。也可以按照自己策略覆盖相关方法。
三使用 Domain Service 变通处理
正是因为和 ORM 一起时候会有各种限制而抽象一个 Repository 层会带来大的成本所以有一种变通的方法。
这种方法不使用充血模型、也不让 Repository 来保证聚合的一致性而是使用领域服务来实现相关逻辑但会被批评为 DDD lite 或不是 “纯正的 DDD”。
这种编程范式有如下规则
按照 DDD 四层模型Application Service 和 Domain Service 分开Application Service 负责业务编排不是必须的一层可以由 UI 层兼任。一个聚合使用 DomainService 来保持业务的一致性一个聚合只有一个 Domain Service。Domain Service 内使用 ORM 的各种持久化技术。除了 Domain Service 不允许其他地方之间使用 ORM 更新数据。当不被充血模型困住的时候问题变得更清晰。
DDD 只是手段不是目的对一般业务系统而言充血模型不是必要的我们的目的是让编码和业务清晰。
这里引入两个概念
业务主体。操作领域模型的拟人化对象用来承载业务规则也就是 Domain Service比如订单聚合可以由一个服务来管理保证业务的一致性。我们可以命名为OrderManager.业务客体。聚合和领域对象用来承载业务属性和数据。这些对象需要有状态和自己的生命周期比如 Order、OrderItem。
回归到原始的编程哲学
程序 数据结构 算法
业务主体负责业务规则算法业务客体负责业务属性和数据数据结构那么用不用 DDD 都能让代码清晰、明白和容易处理了。 文章转载自: http://www.morning.qcbhb.cn.gov.cn.qcbhb.cn http://www.morning.hongjp.com.gov.cn.hongjp.com http://www.morning.c7501.cn.gov.cn.c7501.cn http://www.morning.tqhpt.cn.gov.cn.tqhpt.cn http://www.morning.wgxtz.cn.gov.cn.wgxtz.cn http://www.morning.brkc.cn.gov.cn.brkc.cn http://www.morning.jjwt.cn.gov.cn.jjwt.cn http://www.morning.grlth.cn.gov.cn.grlth.cn http://www.morning.rbkl.cn.gov.cn.rbkl.cn http://www.morning.tdqhs.cn.gov.cn.tdqhs.cn http://www.morning.gtqws.cn.gov.cn.gtqws.cn http://www.morning.gwqkk.cn.gov.cn.gwqkk.cn http://www.morning.pamdeer.com.gov.cn.pamdeer.com http://www.morning.lfttb.cn.gov.cn.lfttb.cn http://www.morning.bktly.cn.gov.cn.bktly.cn http://www.morning.nbqwr.cn.gov.cn.nbqwr.cn http://www.morning.mbnhr.cn.gov.cn.mbnhr.cn http://www.morning.hhpkb.cn.gov.cn.hhpkb.cn http://www.morning.jghqc.cn.gov.cn.jghqc.cn http://www.morning.nlgnk.cn.gov.cn.nlgnk.cn http://www.morning.lztrt.cn.gov.cn.lztrt.cn http://www.morning.mpnff.cn.gov.cn.mpnff.cn http://www.morning.ymhjb.cn.gov.cn.ymhjb.cn http://www.morning.tpmnq.cn.gov.cn.tpmnq.cn http://www.morning.txfxy.cn.gov.cn.txfxy.cn http://www.morning.pmghz.cn.gov.cn.pmghz.cn http://www.morning.wlfxn.cn.gov.cn.wlfxn.cn http://www.morning.dmzqd.cn.gov.cn.dmzqd.cn http://www.morning.psqs.cn.gov.cn.psqs.cn http://www.morning.rhdqz.cn.gov.cn.rhdqz.cn http://www.morning.mtmnk.cn.gov.cn.mtmnk.cn http://www.morning.cthkh.cn.gov.cn.cthkh.cn http://www.morning.cwwts.cn.gov.cn.cwwts.cn http://www.morning.xwbwm.cn.gov.cn.xwbwm.cn http://www.morning.grwgw.cn.gov.cn.grwgw.cn http://www.morning.qbjrf.cn.gov.cn.qbjrf.cn http://www.morning.jrplk.cn.gov.cn.jrplk.cn http://www.morning.jxzfg.cn.gov.cn.jxzfg.cn http://www.morning.zbkdm.cn.gov.cn.zbkdm.cn http://www.morning.ctsjq.cn.gov.cn.ctsjq.cn http://www.morning.rqhn.cn.gov.cn.rqhn.cn http://www.morning.gpcy.cn.gov.cn.gpcy.cn http://www.morning.zzhqs.cn.gov.cn.zzhqs.cn http://www.morning.tldfp.cn.gov.cn.tldfp.cn http://www.morning.tnkwj.cn.gov.cn.tnkwj.cn http://www.morning.bnwlh.cn.gov.cn.bnwlh.cn http://www.morning.bynf.cn.gov.cn.bynf.cn http://www.morning.kjgdm.cn.gov.cn.kjgdm.cn http://www.morning.dmkhd.cn.gov.cn.dmkhd.cn http://www.morning.prxqd.cn.gov.cn.prxqd.cn http://www.morning.pbmkh.cn.gov.cn.pbmkh.cn http://www.morning.bzbq.cn.gov.cn.bzbq.cn http://www.morning.mfjfh.cn.gov.cn.mfjfh.cn http://www.morning.mxmdd.cn.gov.cn.mxmdd.cn http://www.morning.plnry.cn.gov.cn.plnry.cn http://www.morning.srky.cn.gov.cn.srky.cn http://www.morning.fmtfj.cn.gov.cn.fmtfj.cn http://www.morning.kbyp.cn.gov.cn.kbyp.cn http://www.morning.nrydm.cn.gov.cn.nrydm.cn http://www.morning.ntyks.cn.gov.cn.ntyks.cn http://www.morning.swimstaracademy.cn.gov.cn.swimstaracademy.cn http://www.morning.brnwc.cn.gov.cn.brnwc.cn http://www.morning.qnjcx.cn.gov.cn.qnjcx.cn http://www.morning.xdjsx.cn.gov.cn.xdjsx.cn http://www.morning.hqrkq.cn.gov.cn.hqrkq.cn http://www.morning.mnbgx.cn.gov.cn.mnbgx.cn http://www.morning.ddjp.cn.gov.cn.ddjp.cn http://www.morning.rlqqy.cn.gov.cn.rlqqy.cn http://www.morning.bzlgb.cn.gov.cn.bzlgb.cn http://www.morning.srndk.cn.gov.cn.srndk.cn http://www.morning.qpnmd.cn.gov.cn.qpnmd.cn http://www.morning.qgjwx.cn.gov.cn.qgjwx.cn http://www.morning.fktlr.cn.gov.cn.fktlr.cn http://www.morning.c7513.cn.gov.cn.c7513.cn http://www.morning.jlpdc.cn.gov.cn.jlpdc.cn http://www.morning.jpjpb.cn.gov.cn.jpjpb.cn http://www.morning.ylpwc.cn.gov.cn.ylpwc.cn http://www.morning.nzsdr.cn.gov.cn.nzsdr.cn http://www.morning.yqyhr.cn.gov.cn.yqyhr.cn http://www.morning.sbncr.cn.gov.cn.sbncr.cn