【DDD】学习笔记-对象关系映射

领域模型的持久化

领域驱动设计强调对领域建模来应对业务复杂度,通过分层架构来隔离业务复杂度与技术复杂度,这就使得我们在考虑领域逻辑时,尽量规避对领域模型持久化的考虑,引入抽象的资源库正是为了解决这一问题。领域驱动设计的驱动力是领域逻辑而非数据库样式,因此是先有领域模型,然后再根据领域模型定义数据模型,此为领域模型驱动设计与数据模型驱动设计的根本区别。

对象关系映射

领域模型是面向对象的,数据模型是面向关系表的。倘若采用领域模型驱动设计,领域模型一方面充分地表达了系统的领域逻辑,同时它还将映射为数据模型,成为操作数据库的持久化对象。这就是采用面向对象设计编写基础设施层的持久化功能时,无法绕过的对象关系映射(Object Relationship Mapping,ORM)。

对象关系阻抗不匹配

如果持久化的数据库为关系数据库,就会出现所谓“对象关系阻抗不匹配”的问题。这种阻抗不匹配主要体现为以下三个方面:

  • 类型的阻抗不匹配:例如不同关系型数据库对浮点数的不同表示,字符串类型在数据库的最大长度约束等,又例如 Java 等语言的枚举内建类型本质上仍然属于基础类型,关系数据库中却没有对应的类型来匹配。
  • 样式的阻抗不匹配:领域模型与数据模型不具备一一对应的关系。领域模型是一个具有嵌套层次的对象图结构,数据模型在关系数据库中却是扁平的关系结构,要让数据库能够表示领域模型,就只能通过关系来变通地映射实现。
  • 对象模式的阻抗不匹配:面向对象的封装、继承与多态无法在关系数据库得到直观体现。通过封装可以定义一个高内聚的类来表达一个细粒度的基本概念,但数据表往往不这么设计;数据表只有组合关系,无法表达对象之间的继承关系;既然无法实现继承关系,就无法满足 Liskov 替换原则,自然也就无法满足多态。

ORM 框架正是为了解决这些阻抗不匹配问题应运而生,这个问题如此的重要,因此 Java 语言甚至定义了持久化的规范,用以指导面向对象的语言要素与关系数据表之间的映射,如 SUN 在 JDK 5 中引入的 JPA(Java Persistence API),作为 JCP 组织发布的 Java EE 标准,就起到了在 Java 社区指导 ORM 技术实现的规范。

JPA 的应对之道

顾名思义,ORM 框架的目的是在对象与关系之间建立一种映射。为了满足这一目标,往往通过配置文件或者在领域模型中声明元数据来表现这种映射关系。JPA 作为一种规范,它全面地考虑了各种阻抗不匹配的情形,然后规定了标准的映射元数据,如 @Entity、@Table 和 @Column 等 Java 标注。一旦领域模型声明了这些标注,具体的JPA框架如 Hibernate 等就可以通过反射识别这些元数据,获得对象与关系之间的映射信息,从而实现领域模型的持久化。

类型的阻抗不匹配

针对类型的阻抗不匹配,JPA 元数据通过 @Column 标注的属性来指定长度、精度还有对 null 的支持;通过 Lob 标注来表示字节数组;通过 @ElementCollection 等标注来表达集合。至于枚举、日期和 ID 等特殊类型,JPA 也针对性地给出了元数据定义。

枚举类型

关系数据库的内建类型没有枚举类型。如果领域模型的字段被定义为自定义的枚举,通常会在数据库中将相应的列定义为 smallint 类型,然后通过 @Enumerated 表示枚举的含义,例如:

public enum EmployeeType {
    Hourly, Salaried, Commission
}

public class Employee {
    @Enumerated
    @Column(columnDefinition = "smallint")
    private EmployeeType employeeType;
}

使用 smallint 表示枚举类型虽然能够体现值的有序性,但在管理和运维数据库时,查询得到的枚举值却是没有任何业务含义的数字,这不利于对数据的理解。这时,可以将这样的列定义为 VARCHAR,而在领域模型中声明为:

public enum Gender {
    Male, Female
}

public class Employee {
    @Enumerated(EnumType.STRING)
    private Gender gender;
}

通过在字段上标注 @Enumerated(EnumType.STRING),可以将枚举类型转换为字符串。注意,数据库的字符串应与枚举类型的字符串值以及大小写保持一致。

日期类型

针对 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 类型,JPA 定义了 @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 的版本。JPA 2.2 版本已经支持 Java 8 日期时间 API 中除 java.time.Duration 外的其他日期和时间类型。因此,若选择了这个版本的 JPA,无需再为 JDK 8 的日期或时间类型做任何设置,与诸如 String、int 等类型一视同仁。

如果 JPA 的版本是 2.1 及以下版本,由于这些版本发布在 Java 8 之前,因此无法直接支持这两种类型,需要为其定义 AttributeConverter。例如为 LocalDate 定义转换器:

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.sql.Date;
import java.time.LocalDate;

@Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<LocalDate, Date> {
    @Override
    public Date convertToDatabaseColumn(LocalDate locDate) {
        return locDate == null ? null : Date.valueOf(locDate);
    }

    @Override
    public LocalDate convertToEntityAttribute(Date sqlDate) {
        return sqlDate == null ? null : sqlDate.toLocalDate();
    }
}

主键类型

在关系数据库中,每个表的主键都是至为关键的列,通过它可以标注每一行记录的唯一性。主键还是建立表关联的关键列,通过主键与外键的关系可以间接支持领域模型对象之间的导航,同时也保证了关系数据库的完整性。无论是单一主键还是联合主键,主键作为身份标识(Identity),只要能够确保它在同一张表中的唯一性,原则上可以定义为各种类型,如 BigInt、VARCHAR 等。在数据表定义中,只要某个列被声明为 PRIMARY KEY,在领域模型对象的定义中,就可以使用 JPA 提供的 @Id 标注。这个标注还可以和 @Column 标注组合使用:

@Id
@Column(name = "employeeId")
private int id;

主流的关系数据库都支持主键的自动生成,JPA 提供了 @GeneratedValue 标注说明了该主键是自动生成的。该标注还定义了 strategy 属性用以指定自动生成的策略。JPA 还定义了 @SequenceGenerator 与 @TableGenerator 等特殊的 ID 生成器。

在建立领域模型时,我们强调从领域逻辑出发考虑领域类的定义。尤其对于实体类而言,ID 代表的是实体对象的身份标识。它与数据表的主键有相似之处,例如都要求唯一性,但二者的本质完全不同:前者代表业务含义,后者代表技术含义。前者用于对实体对象生命周期的管理与跟踪,后者用于标记每一行在数据表中的唯一性。因此,领域驱动设计往往建议定义 Identity 值对象作为实体的身份标识。一方面,值对象类型可以清晰表达该身份标识的业务含义;另一方面值对象类型的封装也有利于应对未来主键类型可能的变化。

Identity 值对象的定义,体现了面向对象的封装思想,JPA 定义了一个特殊的标注 @EmbeddedId 来建立数据表主键与身份标识值对象之间的映射。例如,为 Employee 实体对象定义了 EmployeeId 值对象,则 Employee 的定义为:

@Entity
@Table(name="employees")
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
    @EmbeddedId
    private EmployeeId employeeId;
}

JPA 对主键类有两个要求:相等性比较与序列化支持。这就需要 EmployeeId 实现 Serializable 接口,并重写 Object 的 equals() 与 hashcode() 方法,同时在类定义之上声明 Embeddable 标注:

@Embeddable
public class EmployeeId implements Identity<String>, Serializable {
    @Column(name = "id")
    private String value;

    private static Random random;

    static {
        random = new Random();
    }

    // 必须提供默认的构造函数
    public EmployeeId() {
    }

    private EmployeeId(String value) {
        this.value = value;
    }

    @Override
    public String value() {
        return this.value;
    }

    public static EmployeeId of(String value) {
        return new EmployeeId(value);
    }

    public static Identity<String> next() {
        return new EmployeeId(String.format("%s%s%s",
                        composePrefix(),
                        composeTimestamp(),
                        composeRandomNumber()));
    }

    @Override
    public 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);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

使用时,可以直接传入 EmployeeId 对象作为主键查询条件:

Optional<Employee> optEmployee = employeeRepo.findById(EmployeeId.of("emp200109101000001"));

样式的阻抗不匹配

样式(Schema)的阻抗不匹配,实则就是对象图与关系表之间的不匹配。要做到二者的匹配,就需要做到图结构与表结构之间的互相转换。在领域模型的对象图中,一个实体组合了另一个实体,由于两个实体都有各自的身份标识,因此在数据库中可以通过主外键关系建立关联。这些关联关系分别体现为一对一、一对多或者多对一、多对多。

例如,在领域模型中,HourlyEmployee 聚合根实体与 TimeCard 实体之间的关系可以定义为:

@Entity
@Table(name="employees")
public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @OneToMany
    @JoinColumn(name = "employeeId", nullable = false)
    private List<TimeCard> timeCards = new ArrayList<>();
}

@Entity
@Table(name = "timecards")
public class TimeCard {
    private static final int MAXIMUM_REGULAR_HOURS = 8;

    @Id
    @GeneratedValue
    private String id;
    private LocalDate workDay;
    private int workHours;

    public TimeCard() {
    }
}

在数据模型中,timecards 表则通过外键 employeeId 建立与 employees 表之间的关联:

CREATE TABLE employees(
    id VARCHAR(50) NOT NULL,
    ......
    PRIMARY KEY(id)
);

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 Address(String country, String province, String city, String street, String zip) {
        this.country = country;
        this.province = province;
        this.city = city;
        this.street = street;
        this.zip = zip;
    }
}

@Entity
@Table(name="employees")
public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<SalariedEmployee> {
    private static final int WORK_DAYS_OF_MONTH = 22;

    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfMonth;

    @ElementCollection
    @CollectionTable(name = "absences", joinColumns = @JoinColumn(name = "employeeId"))
    private List<Absence> absences = new ArrayList<>();

    public SalariedEmployee() {
    }
}

@ElementCollection 说明了字段 absences 是 SalariedEmployee 实体的字段元素,类型为集合;@CollectionTable 标记了关联的数据表以及关联的外键。其数据模型如下 SQL 语句所示:

CREATE TABLE employees(
    id VARCHAR(50) NOT NULL,
    ......
    PRIMARY KEY(id)
);

CREATE TABLE absences(
    employeeId VARCHAR(50) NOT NULL,
    leaveDate DATE NOT NULL,
    leaveReason VARCHAR(20) NOT NULL
);

数据表 absences 没有自己的主键,employeeId 列是 employees 表的主键。注意,在 Absence 值对象的定义中,无需再定义 employeeId 字段,因为 Absence 值对象并不能脱离 SalariedEmployee 聚合根单独存在。

对象模式的阻抗不匹配

符合面向对象设计原则的领域模型,其中一个重要特征是建立了高内聚低耦合的对象图。要做到这一点,就需得将具有高内聚关系的概念封装为一个类,通过显式的类型体现领域中的概念,这样既提高了代码的可读性,又保证了职责的合理分配,避免出现一个庞大的实体类。领域驱动设计更强调这一点,并因此还引入了值对象的概念,用以表现那些无需身份标识却又具有内聚知识的领域概念。因此,一个设计良好的领域模型,往往会形成一个具有嵌套层次的对象图模型结构。

虽然嵌套层次的领域模型与扁平结构的关系数据模型并不匹配,但通过 JPA 提供的 @Embedded 与 @Embeddable 标注可以非常容易实现这一嵌套组合的对象关系,例如 Employee 类的 address 属性和 email 属性:

@Entity
@Table(name="employees")
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
    @EmbeddedId
    private EmployeeId employeeId;

    private String name;

    @Embedded
    private Email email;

    @Embedded
    private 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;
    }
}

以上定义的领域类,都是 Employee 实体的值对象。注意,为了支持 JPA 实现框架通过反射创建对象,若为值对象定义了带参的构造函数,就需要显式定义默认构造函数,如 Address 类的定义。

对比 EmployeeId 类的定义,你会发现该类的定义仍然属于值对象的范畴,只是由于该类型在数据模型中作为主键,故而应将该字段声明为 @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),
    salary DECIMAL(10, 2),
    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 NULL
    PRIMARY KEY(id)
);

如果一个值对象在数据模型中被设计为一个独立的表,但由于它无需定义主键,需要依附于一个实体表,因此在领域模型中依旧标记为 @Embeddable。这既体现了面向对象的封装思想,又表达了一对一或一对多的关系。SalariedEmployee 聚合中的 Absence 值对象就遵循了这样的设计原则。

面向对象的封装思想体现了对细节的隐藏,正确的封装还体现为对职责的合理分配。遵循“信息专家模式”,无论是领域模型中的实体,还是值对象,都应该从它们拥有的数据出发,判断领域行为是否应该分配给这些领域模型类。如 HourlyEmployee 实体类的 payroll(Period) 方法、Absence 值对象的 isIn(Period) 与 isPaidLeave() 方法,乃至于 Salary 值对象的 add(Salary) 等方法,都充分体现了对领域行为的合理封装,避免了贫血模型的出现:

public class HourlyEmployee extends AbstractEntity<EmployeeId> 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 建立继承体系,则它的标识列就是 employeeType 列。

若子类之间的差异太大,采用 Single-Table 策略实现继承的方式会让表的冗余显得格外明显。因为有的子类并没有这些列,却不得不为属于该类型的行记录提供这些列的存储空间。要避免这种冗余,可以采用 Joined-Subclass 策略实现继承。采用这种策略时,继承关系中的每一个实体类,无论是具体类还是抽象类,数据库中都有一个单独的表与之对应。子实体对应的表无需定义从根实体继承而来的列,而是通过共享主键的方式进行关联。

由于 Single-Table 策略是 ORM 默认的继承策略,若要采用 Joined-Subclass 策略,需要在父实体类的定义中显式声明其继承策略,如下所示:

@Entity 
@Inheritance(strategy=InheritanceType.JOINED) 
@Table(name="employees") 
public class Employee {}

采用 Joined-Subclass 策略实现继承时,数据模型中子实体表与父实体表之间的关系实则是一对一的连接关系,这可以认为是为了解决对象模式阻抗不匹配的无奈之举,毕竟用连接关联关系表达继承,怎么看都显得有些别扭。当领域模型中继承体系的子类较多时,这一设计还会影响查询效率,因为它可能牵涉到多张表的连接。

如果既不希望产生不必要的数据冗余,又不愿意表连接拖慢查询的速度,则可以采用 Table-Per-Class 策略。采用这种策略时,继承体系中的每个实体类都对应一个独立的表,其中,父实体对应的表仅包含父实体的字段,子实体对应的表不仅包含了自身的字段,同时还包含了父实体的字段。这相当于用数据表样式的冗余来避免数据的冗余,用单表来避免不必要的连接。如果子类之间的差异较大,我更倾向于采用 Table-Per-Class 策略,而非 Joined-Subclass 策略。

继承的目的绝不仅仅是为了重用,甚至可以说重用并非它的主要价值,毕竟“聚合/合成优先重用原则”已经成为了面向对象设计的金科玉律。继承的主要价值在于支持多态,这样就能利用 Liskov 替换原则,子类能够替换父类而不改变其行为,并允许定义新的子类来满足功能扩展的需求,保证对扩展是开放的。在 Java 或 C# 这样的语言中,由于受到单继承的约束,定义抽象接口以实现多态更为普遍。无论是继承多态还是接口多态,都应站在领域逻辑的角度,思考是否需要引入合理的抽象来应对未来需求的变化。在采用继承多态时,需要考虑对应的数据模型是否能够在对象关系映射中实现继承,并选择合理的继承策略来确定关系表的设计。至于接口多态是对领域行为的抽象,与领域模型的持久化无关,在定义抽象接口时,无需考虑领域模型与数据模型之间的映射。

与持久化无关的领域模型

并非所有的领域模型对象都需要持久化到数据表,一些领域概念之所以定义为值对象,仅仅是为了封装领域行为,表达一种高内聚的领域概念,以便于领域对象更好地分配职责,隐藏实现细节,支持良好的行为协作。例如,与 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,因此是一种 POJO 类。因为它并不需要持久化,为示区别,可称呼这样的类为瞬态类(Transient Class)。对应的,倘若在一个支持持久化的领域类中,需要定义一个无需持久化的字段,可称呼这样的字段为瞬态字段(Transient Field)。JPA 定义了 @Transient 标注用以显式声明这样的字段,例如:

@Entity
@Table(name="employees")
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
    @EmbeddedId
    private EmployeeId employeeId;

    private String firstName;
    private String middleName;
    private String lastName;

    @Transient
    private String fullName;
}

Employee 类对应的数据表定义了 firstName、middleName 与 lastName 列,为了调用方便,该类又定义了 fullName 字段,该值并不需要持久化到数据库中,因此需声明为瞬态字段。

理想的领域模型类应该如瞬态类这样的 POJO 类,这也符合整洁架构的思想,即处于内部核心的领域类不依赖任何外部框架。由于需要为领域模型与数据模型建立关系映射,就必须通过某种元数据机制对其进行表达,ORM 框架才能实现对象与关系的映射。在 Java 语言中,可供选择的元数据机制就是 XML 或标注(Annotation)。XML 因其冗长繁杂与不直观的表现力等缺陷,在相对大型的产品或项目开发中,已被渐渐摒弃,因而更建议使用标注。由于 JPA 是 Oracle(Sun)为持久化接口制定的规范,我们也可自我安慰地说,这些运用到领域模型类上的标注仍然属于 Java 语言的一部分,不算是违背整洁架构的设计原则。

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农丁丁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值