返回介绍

6.3 映射模块

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

为了便于持久化对象,Spring Data MongoDB 提供了一个映射模块,它能够检测到领域类的持久化元数据,并自动将这些对象转换成 MongoDB 的 DBObjects。我们接下来介绍领域建模的方法,以及为了满足对象-文档映射的需求都需要哪些元数据。

6.3.1 领域模型

首先,我们为顶级文档引入一个基类,如示例 6-13 所示。它只有一个 id 属性,这样就不用在所有要成为文档的类中重复定义这个属性。@Id 注解是可选的,默认情况下,我们以 id 或者_id 的属性标识文档的 ID 域。因此,想使用不同属性名称或者想表达特殊的含义时,这个注解就可以派上用场。

示例 6-13 AbstractDocument 类

c0613

id 属性的类型为 BigInteger。通常可以对 id 使用任何的类型,但某些类型对文档有着特殊的意义。在持久化文档中最被推荐的 id 类型一般是 ObjectID。ObjectID 是个可支持集群环境的自增值对象。此外,MongoDB 也可以自动生成它们。同样在使用 Java 驱动时也推荐使用 ObjectID 作为 id 的类型,遗憾的是这将让领域对象对 Java 驱动形成依赖,而这可能是要避免的事情。因为 ObjectID 是 12 位的二进制值,它们可以很容易地转换成 String 或者 BigInteger 值。因此,如果使用 String、BigInteger 或者 ObjectID 作为 id 的类型,就可以利用 MongoDB 的 id 自增长功能,它会在保存之前自动将 id 值转换成 ObjectID,并且在读取时转换回来。如果手动把 String 值赋给不能转换成 ObjectID 的 id 字段的话,它将会按照原有的类型进行存储。所有用于 id 的其他类型也都会以这种方式存储。

地址和 Email 地址

如示例 6-14 所示,Address 是一个再简单不过的领域类,它简单地封装了 3 个定义为 final 的原生 String 值。映射模块会将这种类型的对象转换成 DBObject,在这个过程中会使用属性的名字作为字段的键并会恰当地设置它们的值,如示例 6-15 所示。

示例 6-14 Address 领域类

c0614

示例 6-15 Addrss 对象的 JSON 表达方式

c0615

你也许注意到了,Address 类使用了一个复杂的构造函数以避免它的值被设置为非法状态。它与 final 字段结合,形成了一个经典案例:对象值的不可变性(immutable)。Address 之所以永远不会发生变化,原因在于要改变属性的值就会强制要求创建一个新的 Address 实例。这个类不提供无参构造函数,这就带来了一个问题,当 DBObject 从数据库读取后必须转换成 Address 实例时,对象该如何初始化?Spring Data 有一个持久化构造函数(persistence constructor)的概念,这个构造函数用于初始化持久化的对象。类所提供的无参构造函数(隐式或显示)是最便利的方案,映射模块将会使用反射机制通过它来实例化实体对象。如果使用带参的构造函数,它会尝试根据属性名称来匹配参数名,并从存储表现类中获取值,在 MongoDB 的场景下指的也就是 DBObject。

另一个通过值对象将领域概念具体化的示例是 EmailAddress(见示例 6-16)。要将业务规则封装成代码,值对象是一种非常强大的方式,它可以让代码变得更有表现力、易读、易测试并且易于维护。更多深入的研究,请参考 Dan Bergh-Johnsson 针对这个主题的讨论,它位于 InfoQ( http://www.infoq.com/presentations/Value-Objects- Dan-Bergh-Johnsson )站点上。如果是用普通的 String 类型来定义 Email 地址,就不能确保它是否进行过校验,或者是否为有效的 Email 地址。因此,这个简单的包装类会使用正则表达式来验证输入值,如果不符合表达式就不能通过验证。通过这种方法,如果客户端得到一个 EmailAddress 实例,就可以确保它们所处理的是一个合法的 Email 地址。

示例 6-16 EmailAddress 领域类

c0616

属性 value 使用了 @Field 注解,它允许以一种自定义的方式将属性映射到 DBObject 中的字段。在我们的场景中,将通用的 value 映射为更为具体的 email。虽然可以将属性简单地命名为“email”,但目前的做法对于以下两种场景都能提供便利:假如要将类映射到已经存在的文档上,而该文档已经选择了你不想泄漏给领域对象的字段键(field key)时,@Field 通常可以将字段键和属性名解耦。其次,与关系模型不同,每一份文档中字段键都是重复的,因此它们会在文件数据中占据很大一部分空间,特别是在存储的值非常小的情况下。因此可以定义很简短的字段键名称来减少所需要的空间,不过这会稍微降低 JSON 表达式的可读性。

现在我们已经借助于基本的领域概念的实现完成了准备工作,接下来看看实际组成文档的类。

Customer

如示例 6-17 所示,在 Customer 领域类中首先会注意到的是 @Document 这个注解。在某种程度上,它其实是个可选的注解。如果没有使用这个注解话,映射模块仍然可以将类转换成 DBObject。那么为什么我们要在这里使用它呢?首先,我们可以配置映射基础设施来扫描要持久化的领域类,它只会选择以 @Document 注解的类。当映射模块无法得知它所要处理的对象类型时,映射模块会马上自动检测这个类以获取映射信息,这会稍微降低第一次转换的效率。使用 @Document 的第二个原因是为了定制领域对象存储时的 MongoDB 集合名称。如果没有使用注解或是没有为 Document 设置集合属性,集合的名称会是将第一个字母改为小写的类名,举个例子,Custome 的集合名称将会是“customer”。

示例项目中的代码稍微有些不同,因为之后在改进映射的时候会对模型进行微调。我们希望在现阶段尽量保持简单以简化介绍的过程,让你先专注于通用的映射部分。

示例 6-17 Customer 领域类

c0617

Customer 类包含两个基本属性,这两个属性用来获取 firstname 和 lastname,除此之外还有 EmailAddress 领域类的属性以及一个 Address 的 Set 集合。emailAddress 属性使用了 @Field 注解,如前所述,它可以让我们在 MongoDB 文档中定制键名。

注意,实际上不需要任何注解来配置 Customer 与 EmailAddress 和 Address 之间的关系,这么做的原因是 MongoDB 文档可能包含更复杂的值(如内嵌文档),这会严重影响类的设计和对象的持久化。从设计的角度来看,Customer 在领域驱动设计术语中叫做聚合根(aggregate root),Address 与 EmailAddresses 必须通过 Customer 实例进行存取。基本上我们在这里建立了一个树状结构以便更好地对应 MongoDB 文档模型。因此相对于对象-关系的场景,对象-文件的映射更为简单。从持久化的观点看,存储整个 Customer 与它的 Address 以及 EmailAddress,就会是一次性的原子操作。在关系型数据库的世界里持久化一个这样的对象,需要逐个插入 Address,再加上一次 Customer 本身的插入操作(假设已经把 EmailAddress 内嵌到 Customer 表的列中)。由于表中每一行数据都是松耦合的关系,因此我们必须使用事务来保证插入的一致性。除此之外,为了满足外键关系还必须按照正确的顺序执行插入操作。

然而,文档模型不仅影响到持久化操作的写入端,还影响到了读取端,这通常会导致更多的数据库访问操作。由于集合中的文档是自包含的实体,存取它并不需要触及到其他的集合、文档等。按照关系型数据库的术语,文档实际上是一组预关联的数据集。特别是如果应用程序以特定的粒度来存取数据(在一定程度,这通常是驱动类设计的因素)的话,会在写入时将数据分开并且在每一次读取时又将它重新组合,这种做法显然不合理。完整的 Customer 文档如示例 6-18 所示。

示例 6-18 Customer 文档

c0618

需要注意,建模 Email 地址作为值对象时,需要将它序列化成内嵌对象,这会产生重复的键,从而让文档增加没必要的复杂度。我们暂且搁置这个话题,在 6.3.4 小节“自定义转换”中将会讨论改善的方法。

Product

Product 领域类(见示例 6-19)不会带来太多的意外了。最有趣的部分或许是原生支持 Map 类型的存储。这也是由于文档本身的特点所致,attributes 会成为嵌套文档,Map 的条目会转换成文档的字段。注意,目前只能使用 String 类型作为 Map 的键。

示例 6-19 Product 领域类

c0619-1

 //… additional methods and accessors
}
//… 其他方法和存取器

Order 与 LineItem

接着我们介绍应用程序的订单模块,首先来看一下 LineItem 类,如示例 6-20 所示。

示例 6-20 LineItem 领域类

c0620

首先可以看到两个基本的属性,也就是 price 和 amount,因为它们会依照原生类型转换到文档字段中,所以在它们的定义中没有使用映射注解。而 product 属性使用了 @DBRef 注解,会使得 LineItem 中的 Product 对象不会被嵌入进来,而是存储一个指针,这个指针指向了 Product 集合中的某一个文档。这种方式很像关系型数据库中的外键。

注意,存储 LineItem 时,所引用的 Product 实例必须已经被存储,因此不会存在级联操作。从存储中读取 LineItem 时,Product 的引用也会被解析,使得被引用的文档会被读取出来,并转换到 Product 实例中。

在结束该章节之前,我们来看一下 Order 领域类(见示例 6-21)。

示例 6-21 Order 领域类

c0621

在这里可以看到它组合使用了我们之前看到过的映射方式。这个类使用了 @Document 注解,因此在应用程序上下文启动时,它可以被映射模块发现并检测到对应的信息。对 Customer 的引用使用了 @DBRef,因为相对于内嵌到文档中,引用是一种更合适的方式。Address 属性以及 LineItem 使用自身的类型嵌入起来。

6.3.2 搭建映射的基础设施

前面已经介绍了领域类持久化的方法,现在来介绍如何搭建映射的基础设施,使之为我们工作。在大多数情况下这非常简单,一些使用基础设施的组件(稍后介绍)将使用适当的默认值来启用映射模块。也可以自行设置参数,稍微调整一下配置。在此发挥作用的两个核心抽象体是:MongoMappingContext 和 MappingMongo Converter。前者实际上负责建立领域类的元模型以避免反射查询(例如在每次持久化操作之前检测 id 属性或者确定字段键)。后者使用 MappingContext 所提供映射信息来执行转换。可以简单地使用这两个抽象形式以编程的方式触发对象到 DBObject 的相互转换(见示例 6-22)。

示例 6-22 以编程的方式使用映射模块

P82

首先创建了 MongoMappingContext 以及 SimpleMongoDbFactory 的对象。如果要立即加载带有 @DBRef 注解的文档,那 SimpleMongoDbFactory 实例就是必要的。接着创建了 Customer 和 BasicDBObject 实例,并且通过调用 converter 实例完成转换。正如预期的一样,DBObject 会被数据填充。

使用 Spring 命名空间

Spring Data MongoDB 提供的 Spring 命名空间中包含<mongo:mapping-converter />元素,用来配置之前介绍过的 MappingMongoConverter 实例。它会在内部创建 MongoMappingContext 实例,并会试图从 ApplicationContext 中获取一个名为 mongoDbFactory 的 Spring Bean,当定义的 MongoDbFactory 实例名称不是 mongoDbFactory 时,可以使用命名空间元素的 db-factory-ref 属性来调整,如示例 6-23 所示。

示例 6-23 在 XML 中设置 MappingMongoConverter

c0623

这个配置片段中,在 Spring 应用上下文中设置了 MappingMongoConverter 并将其 id 设为 mon-goConverter。将 base-package 属性指向工程的基础包,以获取领域类并且在应用上下文启动时构建持久化元数据。

在 Spring JavaConfig 中

当使用 Spring JavaConfg 类时,为了简化配置,Spring Data MongoDB 包含一个配置类,它在默认设置中声明了必要的基础组件,并提供回调的方法以在必要时可对它们进行调整。为了模拟刚才介绍的设置,我们的配置类如示例 6-24 所示。

示例 6-24 使用 JavaConfig 进行 MongoDB 的基本设置

c0624

前两个方法是超类限制必须要实现的,因为它创建了用来访问 MongoDB 的 SimpleMongo-DbFactory 类。除了必须实现的方法,我们还重写了 getMapping BasePackage() 方法用来指定映射模块扫描的包及其子包,以查找带有 @Document 注解的类。但这并不是绝对必须的,因为在默认情况下,映射模块会扫描配置类所在的包,我们在此列出只是为了说明如何进行重新配置。

6.3.3 索引

MongoDB 和关系型数据库一样支持索引。它支持以编码的方式或映射注解的方式设置索引。我们通常会通过 Email 地址来检索 Customer,因此希望对它们建立索引。所以,在 Customer 类的 emailAddre 属性上添加 @Index 注解,如示例 6-25 所示。

示例 6-25 为 Customer 的 emailAddress 配置索引

c0625

为了避免在系统中出现重复的 Email 地址,我们将 unique 标志设置为 true。这样 MongoDB 会避免在新建或者更新 Customer 时,使用与其他 Customer 相同的 Email 地址。我们在领域类中使用 @CompundIndex 注解来定义含多个属性的索引。

当类被 MappingContext 识别时,索引元数据也会被发现。由于信息跟集合存储在一起,保存在这个类所对应的持久化集合中,如果移除了集合,索引信息也会丢失。若要避免这件事发生,请在集合中移除所有文档,而不是移除集合。

可以在样例应用程序的 CustomerRepositoryIntegrationTests 类中找到领域对象中被拒绝持久化的用例。请注意,我们预期将会抛出 DuplicateKeyException 异常,因为所持久化的 customer 中,它持有的 Email 地址已经被其他的 customer 占用了。

6.3.4 自定义转换

映射模块提供了一个通用的方法,实现 Java 对象和 MongoDB DBObjects 的互相转换。然而,有时候会想手动实现指定类型的转换,例如之前曾介绍过值对象获取 Email 地址时会产生内嵌文档,我们想避免这样以保持文档结构的简洁,尤其是想把 EmailAddress 值直接内联到 customer 对象中时。为了回顾这个场景,我们从示例 6-26 开始。

示例 6-26 Customer 类和它的 DBObject 表现形式

c0626

我们最终想要获得更简单的文档形式,如示例 6-27 所示。

示例 6-27 想要得到的 Customer 文档结构

062222

实现自定义的转换器

映射子系统允许手动实现对象到文档的相互转换,这是借助 Spring 转换服务的 Converter 抽象做到的。因为我们想要将复杂的对象变成普通的 String,本质上需要实现一个写入的 Converter <EmailAddress, String>,还需要以 String 类型构建 EmailAddress(即 Converter<String、EamilAddress>),如示例 6-28 所示。

示例 6-28 自定义 EmailAddress 转换器

c0628-1

c0628-2

注册自定义转换器

刚刚实现的转换器必须注册到映射模块。不管使用 Spring XML 命名空间还是 Spring JavaConfig 所提供的配置基类,注册过程都非常简单。在 XML 中,只要在<mongo:mapping-converter />内定义一个子元素,然后配置它的 base-package 属性来启动组件扫描即可,如示例 6-29 所示。

示例 6-29 使用 XML 命名空间注册自定义转换器

c0629

在 JavaConfig 中,配置基类提供了回调方法来返回 CustomConversions 实例。这个类会封装已实现的 Converter 实例,稍后会查找到它,并用它来适当配置 MappingContext 和 MongoConverter,最终由 ConversionService 来执行转换。在示例 6-30 中,启用组件扫描来访问 Converter 实例并将它们自动装配到配置类中,最终将它们封装到 CustomConversions 实例中。

示例 6-30 使用 Spring JavaConfig 注册自定义转换器

c0630

如果如示例 6-22 所示,则从应用上下中获取 MappingMongoConverter 并且触发转换的过程,所输出的结果如示例 6-31 所示。

示例 6-31 应用 EmailAddress 自定义转换器所形成的文档结构

c0631

发布评论

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