返回介绍

4.1 示例工程

发布于 2025-04-22 19:57:17 字数 5051 浏览 0 评论 0 收藏

本章的示例工程包含了 3 个包: com.oreilly.springdata.jpa 这个基础包以及 core 和 order 两个子包。基础包里面包含了一个 Spring 的 JavaConfig 类,它可以通过简单的 Java 类而不是 XML 来配置 Spring 容器。其他的两个包中包含了我们的领域类以及 Repository 接口。正如其名字所示,core 包中包含了对领域模型的基础抽象:像 AbstractEntity 这种技术化的帮助类,同时也包含了像 EmailAddress、Address、Customer 以及 Product 这样的领域概念。然后,还有 order 包,基于这些基础的概念实现了订单的概念。所以在这里可以看到 Order 以及 OrderItem。在后面的段落中,我们会仔细地查看每个类,介绍其目的以及使用 JPA 映射注解将它们匹配到数据库的方式。

我们领域模型中所有实体的基础类就是 AbstractEntity(如示例 4-1 所示)。它使用了 @MappedSuperclass 来表示它本身并不是受管理的实体类,而是会由其他的实体类进行扩展。我们在这里声明了一个 Long 类型的 id 并且告知持久化提供商自动选择最合适的策略来生成主键。除此之外,我们通过检查 id 属性实现了 equals(…) 和 hashCode(…) 方法,这样一来,具有相同 id 的同种类型的实体会被视为相等。在这个类中,包含了持久化实体的主要技术化信息,因此在具体的实体类中就可以关注实际的领域属性。

示例 4-1 AbstractEntity 类

c0401

让我们继续看一下非常简单的 Address 领域类。如示例 4-2 所示,它是一个带有 @Entity 注解的普通类,并且包含了 3 个 String 属性。因为它们都是基础属性,因此不需要添加额外的注解,持久化提供商会自动将它们匹配到表的列上。如果需要自定义属性要持久化的列名,可以使用 @Column 注解。

示例 4-2 Address 领域类

c0402

Address 会被 Customer 实体所引用。Customer 会包含一些其他的属性(如原始类型的 firstname 和 lastname)。它们在映射上与我们看到的 Address 类似。每个 Customer 还会有一个电子邮件地址,这会通过 EmailAddress 类体现(如示例 4-3 所示)。

示例 4-3 EmailAddress 领域类

c0403-1

c0403-2

这个类是一个值对象(value object, http://domaindrivendesign.org/node/135 ),这个概念是 Eric Evans 在《模型驱动设计》[Evans03]一书中定义的。值对象通常用来表述领域的概念,可以简单地将其实现为原始类型(在本例中就是字符串),但是值对象还允许在其内部实现领域约束。电子邮件地址必须要遵循一定的格式,否则,就会出现不合法的邮件地址。所以,我们实际上会通过一些正则表达式实现格式检查,这样就能阻止实例化非法的 EmailAddress。

这就意味着,在处理这种类型的实例时,能够确保有合法的电子邮件地址,所以我们不再需要专门的组件来校验它。在持久化映射方面,EmailAddress 带有 @Embeddable 注解,这会导致持久化供应商会将其属性放到包含它的类所对应的表中。在我们的场景下,这是一个列,我们将其定义为个性化的名字:email。

可以看到,我们需要为 JPA 持久化厂商提供一个空的构造方法,这样它才能够通过反射实例化 EmailAddress 对象(见示例 4-3)。这是一个很明显的缺点,因为没有办法让 emailAddress 是 final 的或者断言它是非空的。用于 NoSQL 实现的 Spring Data 映射子系统并没有将这一需求暴露给开发人员。作为示例,可以查看 6.3 小节“映射模块”来了解在 MongoDB 中如何对更为严格的值对象实现进行建模。

示例 4-4 Customer 领域类

c0404-1

c0404-2

我们在电子邮件地址上使用了 @Column 注解,以保证某一个电子邮件地址不能被多个客户所使用,这样就能根据电子邮件来唯一地查询客户了。最后,我们声明 Customer 有一个 Address 的集合。这个属性需要更多关注一下,因为我们在这里定义了很多事情。

首先,也是最基础的,我们使用 @OneToMany 注解来指明一个 Customer 可以拥有多个 Address。在这个注解中,我们将级联类型(cascade)设置成了 CascadeType.ALL,并为 Address 启用了子对象移除(orphan removal)。这会产生多种影响。例如,当我们持久化、更新或者删除 Customer 时,Address 也会被持久化、更新或删除。因此,我们不再需要在前端持久化 Address 实例了并且当删除 Customer 时,也不用再关心删除所有的 Address 了。持久化厂商会做到这一点。需要注意的一点是,这并不是数据库级别的级联,而是你的 JPA 持久化厂商所管理的级联。除此之外,将子对象移除设置为 true,那么如果 Address 在集合中被移除了,它们也会在数据库中被删除。

以上所有的结果将会导致 Address 的生命周期被 Customer 所控制,这种关系是一种经典的组合,在 领域驱动设计 (domain-driven design)术语中,Customer 会被成为聚合根(aggregate root, http://en.wikipedia.org/wiki/Domain-driven design#Building blocks_of_DDD ),因为它不仅为自己也为其他实体控制持久化操作以及约束。最后,我们在 addresses 属性上使用 @JoinColumn,这会导致持久化厂商为 Address 对象后端所对应的表添加另外一列。这一列会用来引用 Customer,从而实现表关联。如果我们遗漏了这个注解的话,持久化厂商将会创建一个专门的连接表。

在包 core 中,最后一部分内容是 Product,如示例 4-5 所示。就像前面讨论的其他类一样,它包含了多种基本属性,所以无需添加注解就能使持久化厂商对它们产生映射。我们只是在定义 name 和 price 属性时,使用 @Column 注解,其目的是将其定义为强制性的属性。除此之外,还添加了一个 Map,它会用来存储额外的属性,不同的产品之间这些属性可能是不同的。

示例 4-5 Product 领域类

c0405-1

c0405-2

对于构建基本的客户关系管理(Customer Relation Management,CRM)或库存系统而言,我们已经万事俱备了。接下来,为了实现系统中 Product 的订单,我们需要进行一些抽象。首先,我们引入了 LineItem,它会持有对 Product 的引用以及 Product 的数量和购买的价格。匹配 Product 属性时,我们使用了 @ManyToOne 注解,它实际上会转化成 LineItem 对应表中的 product_id 列,这一列会用来指向 Product,如示例 4-6 所示。

示例 4-6 LineItem 领域类

c0406

完成这个“拼图游戏”的最后一块就是 Order 实体了,它基本上就是一个指针,指向了 Customer、投递地址、账单地址以及表示实际购买的多个 LineItem(如示例 4-7 所示)。LineItem 的映射与前面看到的 Customer 和 Address 之间映射类似。Order 会自动对 LineItem 实例进行级联持久化操作。因此,没有必要单独管理 LineItem 的持久化生命周期。其他的属性都是已经讨论过的多对一关系。要注意的是,我们为 Order 定义了一个个性化的表名,因为在大多数的数据库中,Order 本身就是一个保留字,因此,所生成的建表 SQL 以及为查询和数据操作生成的 SQL 在执行时都会导致异常产生。

示例 4-7 Order 领域类

c0407-1

c0407-2

最后一个值得注意的方面是 Order 类的构造器使用了投递地址和账单地址的副本。这能够保证方法传递进来的 Address 实例如果发生变化的话,不会关联影响到已经存在的订单。如果我们不创建这个副本的话,如果顾客稍后修改他的地址信息,那使用这个地址创建的订单中对应的地址也会随之发生变化。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。