编写MongoDB存储库
MongoDB 是另一个著名的 NoSQL 数据库。Cassandra 是一个列存储的数据库,而 MongoDB 则被视为文档数据库。更具体地讲,MongoDB 会将文档存储为 BSON(即二进制 JSON)格式,它的查询和检索方式与在其他的数据库中查询数据的方式类似。
与 Cassandra 一样,理解 MongoDB 并非关系型数据库非常重要。对于管理 MongoDB 服务器集群和对数据进行建模的方式,我们都需要采取与其他类型的数据库不同的思维。
与 Spring Data 组合使用时,Mongo DB 的使用方式其实与使用 Spring Data 实现 JPA 和 Cassandra 的持久化并没有太大的差异。我们需要为领域类添加注解,将领域类型映射为文档结构。我们需要编写存储库接口,它们在很大程度上遵循与前文中的 JPA 和 Cassandra 一样的编程模型。但是,在我们开始之前,必须要在项目中启用 Spring Data MongoDB。
启用Spring Data MongoDB
要开始使用 Spring Data MongoDB,我们需要在项目的构建文件中添加 Spring Data MongoDB starter 依赖。与 Spring Data Cassandra 类似,Spring Data MongoDB 有两个独立的 starter 可供选择,其中一个用于反应式场景,另一个用于非反应式场景。我们会在第 13 章学习其反应式方案。现在,添加如下的依赖到构建文件,就可以使用非反应式的 MongoDB starter 了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-data-mongodb
</artifactId>
</dependency>
这个依赖也可以通过在 Spring Initializr 中选择 NoSQL 下的 MongoDB 复选框来进行添加。
添加完这个 starter 依赖之后,就会触发自动配置功能,从而启用 Spring Data 对编写自动化存储库接口的支持,这与我们在第 3 章看到的 JPA 和本章前面的 Cassandra 类似。
默认情况下,Spring Data MongoDB 会假定我们有一个在本地运行的 MongoDB 服务器并监听 27017 端口。如果已经安装了 Docker 环境,可以使用如下的命令以一种非常简单的方式将 MongoDB 服务器运行起来:
$ docker run -p 27017:27017 -d mongo:latest
但是,为了便于测试或开发,还可以选择嵌入式的 Mongo 服务器。为了实现这一点,在构建文件中添加如下所示的 Flapdoodle 嵌入式 MongoDB 依赖:
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<!-- <scope>test</scope> -->
</dependency>
Flapdoodle 嵌入式数据库为我们提供了使用内存 Mongo 数据库的便利,就像使用 H2 数据库处理关系型数据那样。也就是说,我们不需要运行一个单独的数据库,但是当重启应用的时候,所有的数据都会被清除掉。
嵌入式数据库对于开发和测试是非常棒的,但是将应用部署到生产环境时,需要设置一些属性,确保 Spring Data MongoDB 能够知道生产环境的 Mongo 数据库在哪里以及如何连接,这些属性如下所示:
spring:
data:
mongodb:
host: mongodb.tacocloud.com
port: 27017
username: tacocloud
password: s3cr3tp455w0rd
database: tacoclouddb
这些属性并不都需要设置,但是如果 Mongo 数据库不在本地运行,这些属性能够为 Spring Data MongoDB 指明正确的方向。具体来讲,每个属性配置了如下的内容。
-
spring.data.mongodb.host:运行 Mongo 的主机名(默认值为 localhost)。
-
spring.data.mongodb.port:Mongo 服务器所监听的端口(默认值为 27017)。
-
spring.data.mongodb.username:访问安全 Mongo 数据库所使用的用户名。
-
spring.data.mongodb.password:访问安全 Mongo 数据库所使用的密码。
-
spring.data.mongodb.database:数据库名(默认值为 test)。
现在,我们已经在项目中启用了 Spring Data MongoDB,接下来,需要为领域对象添加注解,使其能够以文档的形式持久化到 MongoDB 中。
将领域类型映射为文档
Spring Data MongoDB 提供了一些注解,能够将领域对象映射为文档结构,进而持久化到 MongoDB 中。尽管 Spring Data MongoDB 为映射提供了不少的注解,但是如下所示的 4 个是最常用的。
-
@Id:指定某个属性为文档的 ID(该注解出自 Spring Data Commons)。
-
@Document:将领域类型声明为要持久化到 MongoDB 的文档。
-
@Field:声明在持久化存储的文档中该属性的字段名称(我们还可以选择性地配置顺序)。
-
@Transient:声明该属性是否要进行持久化。
上述的注解中,只有 @Id 和 @Document 是严格需要的。除非我们显式声明,否则没有使用 @Field 或 @Transient 注解的属性会假定字段名与属性名是一致的。
将这些注解用到 Ingredient 类上,如下所示:
package tacos;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Document
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class Ingredient {
@Id
private String id;
private String name;
private Type type;
public enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
可以看到,我们在类级别使用了 @Document 注解,表明 Ingredient 是一个文档实体,可以写入 Mongo 数据库,也可以从中进行读取。默认情况下,集合名(集合在 Mongo 中类似于关系型数据库中的表)是基于类名的,其中第一个字母会变成小写。我们没有特殊指定,所以 Ingredient 对象会被持久化到名为 ingredient 的集合中,但是我们可以通过设置 @Document 的 collection 属性来修改这一点,如下所示:
@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@Document(collection = "ingredients")
public class Ingredient {
...
}
还可以看到,id 属性添加了 @Id,这表明该属性将会作为持久化文档的ID。@Id 注解可以放到任意 Serializable 类型的属性上,包括 String 和 Long。在本例中,已经使用 String 类型的 id 作为自然的标识符,所以没有必要将它改成其他类型了。
到目前为止,一切顺利。但是,你可能还记得,在前文中,Ingredient 是介绍 Cassandra 时最容易映射的域类型。其他的领域类型,如 Taco,就比较有挑战性了。我们来看看如何映射 Taco 类,看看它会带给我们什么惊喜。
MongoDB 文档持久化的做法非常适合在聚合根上以领域驱动设计的方式进行持久化。MongoDB 中的文档往往被定义为聚合根,聚合成员则会作为子文档。
对 Taco Cloud 来说,这意味着 Taco 会作为 TacoOrder 聚合根的成员进行持久化,所以 Taco 类没有必要添加 @Document 注解,也不需要带有 @Id 注解的属性。Taco 类没有任何持久化相关的注解,如下所示:
package tacos;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public class Taco {
@NotNull
@Size(min = 5, message = "Name must be at least 5 characters long")
private String name;
private Date createdAt = new Date();
@Size(min = 1, message = "You must choose at least 1 ingredient")
private List<Ingredient> ingredients = new ArrayList<>();
public void addIngredient(Ingredient ingredient) {
this.ingredients.add(ingredient);
}
}
但是,TacoOrder 类要作为聚合根,需要使用 @Document 注解,并且要有一个带有 @Id 注解的属性,如下所示:
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
@Data
@Document
public class TacoOrder implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
private Date placedAt = new Date();
// other properties omitted for brevity's sake
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}
为简洁起见,我省略掉了各种投递和信用卡相关的字段。从剩下的内容来看,很明显,与其他领域类型一样,我们所需要的只是 @Document 和 @Id 注解。
然而,需要注意,id 属性已经被改成了一个 String(而不是 JPA 版本中的 Long 或 Cassandra 版本中的 UUID)。正如之前所说,@Id 可以应用于任何 Serializable 类型。如果选择使用 String 属性作为 ID,我们会享受到 Mongo 在保存时自动为其赋值的好处(假设它的值为 null)。通过选择使用 String,我们会有一个数据库管理的 ID 分配策略,不需要关心如何手动设置该属性的值。
还有一些更高级和不太常用的场景需要额外的映射,但是我们会发现,对于大多数情况,@Document、@Id,以及偶尔用到的 @Field 或 @Transient,已经能够满足 MongoDB 映射的要求了。对于 Taco Cloud 的领域类型来说,它们足以完成我们的任务。
接下来,就要编写存储库接口了。
编写MongoDB存储库接口
Spring Data MongoDB 提供了自动化存储库的支持,就像 Spring Data JPA 和 Spring Data Cassandra 所提供的那样。
首先定义将 Ingredient 对象持久化为文档的存储库。像以前一样,我们需要编写扩展 CrudRepository 的 IngredientRepository 接口,如下所示:
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;
public interface IngredientRepository
extends CrudRepository<Ingredient, String> {
}
稍等一下!它看上去与我们在 4.1 节为 Cassandra 所编写的 IngredientRepository 接口完全一样。确实,它们是相同的接口,没有任何区别。这凸显了扩展 CrudRepository 的一个好处:在各种数据库类型之间更具可移植性,对 MongoDB 和 Cassandra 有同样的效果。
我们再转向 OrderRepository 接口,它非常简单直接:
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.TacoOrder;
public interface OrderRepository
extends CrudRepository<TacoOrder, String> {
}
与 IngredientRepository 类似,OrderRepository 扩展了 CrudRepository 接口,从而能够获得其优化的 insert() 方法。除此之外,与目前定义的其他存储库相比,它并没有什么特别之处。但是,请注意,在扩展 CrudRepository 时,ID 的参数类型是 String 而不是 Long(JPA 中所使用的类型)或 UUID(Cassandra 中所使用的类型)。这反映了 TacoOrder 类中的变更,其目的是支持 ID 的自动分配。
最后,使用 Spring Data MongoDB 与我们之前使用的其他 Spring Data 项目并没有太大的差异。领域类型的注解会有所差异,但除了在扩展 CrudRepository 时所指定的 ID 参数外,存储库接口基本都是相同的。