【Spring连载】使用Spring Data访问 MongoDB(四)----对象映射Object Mapping
一、对象映射基础
见对象映射基础。
二、基于约定的映射Convention-based Mapping
当没有提供额外的映射元数据时,MappingMongoConverter有一些将对象映射到documents的约定。约定如下:
- 短Java类名以以下方式映射到集合名称。类com.bigbank.SavingsAccount映射到savingsAccount集合名称。
- 所有嵌套对象都存储为document中的嵌套对象,而不是DBRefs。
- 转换器使用向其注册的任何Spring转换器来覆盖对象属性到document字段和值的默认映射。
- 对象的字段用于和document中的字段来回转换。不使用公共JavaBean属性。
- 如果您有一个非零参数构造函数,其构造函数参数名称与document的顶级字段名称匹配,则使用该构造函数。否则,将使用零参数构造函数。如果存在多个非零参数构造函数,则会引发异常。
2.1如何在映射层中处理_id字段。
MongoDB要求所有documents都有一个_id字段。如果不提供,driver将分配一个有生成值的ObjectId。“_id”字段可以是除数组之外的任何类型,只要它是唯一的。driver自然支持所有基本类型和日期。当使用MappingMongoConverter时,有一些规则控制Java类的属性如何映射到此_id字段。
以下概述了什么字段将映射到document _id字段:
- 用@Id(org.springframework.data.annotation.Id)注解的字段将映射到_id字段。
- 没有注解但命名为id的字段将映射到_id字段。
- 标识符的默认字段名为_id,可以通过@Field注解进行自定义。
表1:_id字段定义的翻译示例
Field definition | Resulting Id-Fieldname in MongoDB |
---|---|
String id | _id |
@Field String id | _id |
@Field(“x”) String id | x |
@Id String x | _id |
@Field(“x”) @Id String x | _id |
以下概述了对映射到document _id字段的属性执行的类型转换(如果有的话)。
- 如果一个名为id的字段在Java类中被声明为String或BigInteger,那么它将被转换为ObjectId并存储(如果可能的话)。作为字段类型的ObjectId也是有效的。如果在应用程序中为id指定一个值,则会检测到MongoDB driver对ObjectId的转换。如果指定的id值无法转换为ObjectId,则该值将按原样存储在document的_id字段中。如果字段用@Id进行注解,这也适用。
- 如果一个字段在Java类中用@MongoId注解,它将被转换并使用其实际类型存储。除非@MongoId声明了所需的字段类型,否则不会进行进一步的转换。如果没有为id字段提供值,则将创建一个新的ObjectId并将其转换为属性类型。
- 如果在Java类中用@MongoId(FieldType.…)注解字段,则会尝试将该值转换为声明的FieldType。如果没有为id字段提供值,则将创建一个新的ObjectId并将其转换为声明的类型。
- 如果名为id 的 id字段在Java类中未声明为String、BigInteger或ObjectID,则应在应用程序中为其赋值,以便将其“原样”存储在 document的_id字段中。
- 如果Java类中不存在名为id的字段,那么driver将生成一个隐式_id文件,但不会映射到Java类的属性或字段。
在查询和更新时,MongoTemplate将使用转换器来处理与上述保存文档规则相对应的查询和更新对象的转换,以便查询中使用的字段名和类型能够与域类中的字段名匹配。
三、数据映射和类型转换
Spring Data MongoDB支持所有可以表示为BSON的类型,BSON是MongoDB的内部document格式。除了这些类型之外,Spring Data MongoDB还提供了一组内置的转换器来映射其他类型。你可以提供自己的转换器来调整类型转换。有关更多详细信息,请参见自定义转换-覆盖默认映射。
内置类型转换:
Type | Type conversion | Sample |
---|---|---|
String | native | {“firstname” : “Dave”} |
double, Double, float, Float | native | {“weight” : 42.5} |
int, Integer, short, Short | native 32-bit integer | {“height” : 42} |
long, Long | native 64-bit integer | {“height” : 42} |
Date, Timestamp | native | {“date” : ISODate(“2019-11-12T23:00:00.809Z”)} |
byte[] | native | {“bin” : { “$binary” : “AQIDBA==”, “$type” : “00” }} |
java.util.UUID (Legacy UUID) | native | {“uuid” : { “$binary” : “MEaf1CFQ6lSphaa3b9AtlA==”, “$type” : “03” }} |
Date | native | {“date” : ISODate(“2019-11-12T23:00:00.809Z”)} |
ObjectId | native | {“_id” : ObjectId(“5707a2690364aba3136ab870”)} |
Array, List, BasicDBList | native | {“cookies” : [ … ]} |
boolean, Boolean | native | {“active” : true} |
null | native | {“value” : null} |
Document | native | {“value” : { … }} |
Decimal128 | native | {“value” : NumberDecimal(…)} |
AtomicInteger calling get() before the actual conversion | converter 32-bit integer | {“value” : “741” } |
AtomicLong calling get() before the actual conversion | converter 64-bit integer | {“value” : “741” } |
BigInteger | converter String | {“value” : “741” } |
BigDecimal | converter String | {“value” : “741.99” } |
URL | converter | {“website” : “https://spring.io/projects/spring-data-mongodb/” } |
Locale | converter | {"locale : “en_US” } |
char, Character | converter | {“char” : “a” } |
NamedMongoScript | converter Code | {“_id” : “script name”, value: (some javascript code)} |
java.util.Currency | converter | {“currencyCode” : “EUR”} |
Instant (Java 8) | native | {“date” : ISODate(“2019-11-12T23:00:00.809Z”)} |
Instant (Joda, JSR310-BackPort) | converter | {“date” : ISODate(“2019-11-12T23:00:00.809Z”)} |
LocalDate (Joda, Java 8, JSR310-BackPort) | converter / native (Java8)[1] | {“date” : ISODate(“2019-11-12T00:00:00.000Z”)} |
LocalDateTime, LocalTime (Joda, Java 8, JSR310-BackPort) | converter / native (Java8)[2] | {“date” : ISODate(“2019-11-12T23:00:00.809Z”)} |
DateTime (Joda) | converter | {“date” : ISODate(“2019-11-12T23:00:00.809Z”)} |
ZoneId (Java 8, JSR310-BackPort) | converter | {“zoneId” : “ECT - Europe/Paris”} |
Box | converter | {“box” : { “first” : { “x” : 1.0 , “y” : 2.0} , “second” : { “x” : 3.0 , “y” : 4.0}} |
Polygon | converter | {“polygon” : { “points” : [ { “x” : 1.0 , “y” : 2.0} , { “x” : 3.0 , “y” : 4.0} , { “x” : 4.0 , “y” : 5.0}]}} |
Circle | converter | {“circle” : { “center” : { “x” : 1.0 , “y” : 2.0} , “radius” : 3.0 , “metric” : “NEUTRAL”}} |
Point | converter | {“point” : { “x” : 1.0 , “y” : 2.0}} |
GeoJsonPoint | converter | {“point” : { “type” : “Point” , “coordinates” : [3.0 , 4.0] }} |
GeoJsonMultiPoint | converter | {“geoJsonLineString” : {“type”:“MultiPoint”, “coordinates”: [ [ 0 , 0 ], [ 0 , 1 ], [ 1 , 1 ] ] }} |
Sphere | converter | {“sphere” : { “center” : { “x” : 1.0 , “y” : 2.0} , “radius” : 3.0 , “metric” : “NEUTRAL”}} |
GeoJsonPolygon | converter | {“polygon” : { “type” : “Polygon”, “coordinates” : [[ [ 0 , 0 ], [ 3 , 6 ], [ 6 , 1 ], [ 0 , 0 ] ]] }} |
GeoJsonMultiPolygon | converter | {“geoJsonMultiPolygon” : { “type” : “MultiPolygon”, “coordinates” : [[ [ [ -73.958 , 40.8003 ] , [ -73.9498 , 40.7968 ] ] ],[ [[ -73.973 , 40.7648 ] , [ -73.9588 , 40.8003 ] ] ]] }} |
GeoJsonLineString | converter | { “geoJsonLineString” : { “type” : “LineString”, “coordinates” : [ [ 40 , 5 ], [ 41 , 6 ] ] }} |
GeoJsonMultiLineString | converter | {“geoJsonLineString” : { “type” : “MultiLineString”, coordinates: [[ [ -73.97162 , 40.78205 ], [ -73.96374 , 40.77715 ] ],[ [ -73.97880 , 40.77247 ], [ -73.97036 , 40.76811 ] ]] }} |
1.使用UTC区域偏移。通过MongoConverterConfigurationAdapter进行配置
2.使用UTC区域偏移。通过MongoConverterConfigurationAdapter进行配置
集合处理
集合处理取决于MongoDB返回的实际值。
- 如果document 不包含映射到集合的字段,则映射不会更新属性。这意味着该值将保持为null、java默认值或在对象创建期间设置的任何值。
- 如果document 包含要映射的字段,但该字段包含null值(如:{ ‘list’ : null }),则属性值设置为null。
- 如果document 包含要映射到非null集合的字段(如:{ ‘list’ : [ … ] }), 集合中会填充映射的值。
一般来说,如果使用构造函数创建,则可以获取要设置的值。如果查询响应没有提供属性值,则属性填充可以使用默认初始化值。
四、映射配置
除非明确配置,否则在创建MongoTemplate时,默认情况下会创建MappingMongoConverter的实例。你可以创建自己的MappingMongoConverter实例。这样做可以指定域类在类路径中的位置,以便Spring Data MongoDB可以提取元数据并构建索引。此外,通过创建自己的实例,你可以注册Spring转换器来将特定类映射到数据库和从数据库映射特定类。
你可以配置MappingMongoConverter以及com.mongodb.client.MongoClient和MongoTemplate,使用基于Java或基于XML的元数据。以下示例展示了配置:
@Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {
@Override
public String getDatabaseName() {
return "database";
}
// the following are optional
@Override
public String getMappingBasePackage() { --------1
return "com.bigbank.domain";
}
@Override
void configureConverters(MongoConverterConfigurationAdapter adapter) { --------2
adapter.registerConverter(new org.springframework.data.mongodb.test.PersonReadConverter());
adapter.registerConverter(new org.springframework.data.mongodb.test.PersonWriteConverter());
}
@Bean
public LoggingEventListener<MongoMappingEvent> mappingEventsListener() {
return new LoggingEventListener<MongoMappingEvent>();
}
}
1. 映射base包定义用于扫描用于预初始化MappingContext的实体的根路径。默认情况下使用配置类包。
2. 为特定域类型配置额外的自定义转换器,用你的自定义实现替换这些类型的默认映射过程。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xsi:schemaLocation="
http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- Default bean name is 'mongo' -->
<mongo:mongo-client host="localhost" port="27017"/>
<mongo:db-factory dbname="database" mongo-ref="mongoClient"/>
<!-- by default look for a Mongo object named 'mongo' - default name used for the converter is 'mappingConverter' -->
<mongo:mapping-converter base-package="com.bigbank.domain">
<mongo:custom-converters>
<mongo:converter ref="readConverter"/>
<mongo:converter>
<bean class="org.springframework.data.mongodb.test.PersonWriteConverter"/>
</mongo:converter>
</mongo:custom-converters>
</mongo:mapping-converter>
<bean id="readConverter" class="org.springframework.data.mongodb.test.PersonReadConverter"/>
<!-- set the mapping converter to be used by the MongoTemplate -->
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>
<constructor-arg name="mongoConverter" ref="mappingConverter"/>
</bean>
<bean class="org.springframework.data.mongodb.core.mapping.event.LoggingEventListener"/>
</beans>
AbstractMongoClientConfiguration要求你实现定义“com.mongodb.client.MongoClient”以及提供数据库名称的方法。AbstractMongoClientConfiguration还有一个名为getMappingBasePackage(…)的方法,你可以覆盖它来告诉转换器在哪里扫描用@Document注解的类。
你可以通过重写customConversionsConfiguration方法添加其他转换器。MongoDB的原生JSR-310支持可以通过MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs()启用。在前面的例子中还显示了一个LoggingEventListener,它记录发布到Spring的ApplicationContextEvent基础结构上的MongoMappingEvent实例。
Java时间类型
我们建议通过MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs() 使用MongoDB的原生JSR-310支持,如上所述,因为它使用的是基于UTC的方法。从Spring Data Commons继承的对java.time类型的默认JSR-310支持使用本地机器时区作为参考,并且应该仅用于向后兼容。
AbstractMongoClientConfiguration创建一个MongoTemplate实例,并将其注册到名为mongoTemplate的容器中。
base-package属性告诉它在哪里扫描用“@org.springframework.data.mongodb.core.mapping.Document”注解的类。
如果你想依靠Spring Boot来引导Data MongoDB,但仍想覆盖配置的某些方面,则可能需要公开该类型的bean。对于自定义转换,例如,你可以选择注册一个类型为MongoCustomConversions的bean,该bean将由Boot基础结构获取。要了解更多信息,请务必阅读Spring Boot参考文档。
五、基于元数据的映射
为了充分利用Spring Data MongoDB支持中的对象映射功能,你应该使用@Document来注解映射的对象。尽管映射框架没有必要有这个注解(即使没有任何注解,POJO也能正确映射),但它可以让类路径扫描程序找到并预处理域对象,以提取必要的元数据。如果不使用此注解,则在第一次存储域对象时,应用程序的性能会受到轻微影响,因为映射框架需要建立其内部元数据模型,以便了解域对象的属性以及如何持久化这些属性。以下示例展示了一个域对象:
例1:域对象示例
package com.mycompany.domain;
@Document
public class Person {
@Id
private ObjectId id;
@Indexed
private Integer ssn;
private String firstName;
@Indexed
private String lastName;
}
@Id注解告诉映射器你要为MongoDB _id属性使用哪个属性,@Indexed注解告诉映射框架对document的该属性调用createIndex(…),从而加快搜索速度。仅对使用@Document注解的类型执行自动索引创建。
默认情况下,自动索引创建处于禁用状态,需要通过配置启用(请参阅创建索引)。
5.1 映射注解概述
MappingMongoConverter可以使用元数据来驱动对象到documents的映射。以下注解可用:
- @Id:应用于字段级别,以标记用于标识目的的字段。
- @MongoId:应用于字段级别,以标记用于标识目的的字段。接受可选的FieldType以自定义id转换。
- @Document:应用于类级别,表示该类是映射到数据库的候选类。你可以指定存储数据的集合的名称。
- @DBRef:应用于字段,指示它将使用com.mongodb.DBRef进行存储。
- @DocumentReference:应用于字段,表示它将作为指向另一个文档的指针存储。这可以是单个值(默认情况下为id),也可以是通过转换器提供的Document。
- @Indexed:应用于字段级别,用于描述如何为字段编制索引。
- @CompoundIndex(可重复):在类型级别应用以声明复合索引。
- @GeoSpatialIndexed:应用于字段级别,用于描述如何对字段进行地理索引。
- @TextIndexed:应用于字段级别,用于标记要包含在文本索引中的字段。
- @HashIndexed:应用于字段级别,用于在哈希索引中跨分片集群对数据进行分区。
- @Language:应用于字段级别,用于设置文本索引的语言覆盖属性。
- @Transient:默认情况下,所有字段都映射到文档。此注解禁止了该字段在数据库中存储。暂态属性不能在持久构造函数中使用,因为转换器无法为构造函数参数实现(materialize)值。
- @PersistenceConstructor:标记给定的构造函数,甚至是受包保护的构造函数,以便在从数据库实例化对象时使用。构造函数参数按名称映射到检索到的Document中的键值。
- @Value:此注解是Spring Framework的一部分。在映射框架中,它可以应用于构造函数参数。这使你可以使用Spring Expression Language语句来转换在数据库中检索到的键的值,然后再将其用于构造域对象。为了引用给定document的属性,必须使用以下表达式:@Value(“#root.myProperty”),其中root指的是给定document的根。
- @Field:应用于字段级别,它允许描述字段的名称和类型,因为它将在MongoDB BSON document中表示,因此允许名称和类型与类的字段名以及属性类型不同。
- @Version:在字段级别用于乐观锁定,并检查保存操作的修改。初始值为零(对于原始类型为一),每次更新时都会自动更改。
映射元数据基础设施是在一个独立的spring-data-commons项目中定义的,该项目与技术无关。MongoDB支持中使用了特定的子类来支持基于注解的元数据。如果有需求,也可以采取其他策略。
下面是一个更复杂映射的示例
@Document
@CompoundIndex(name = "age_idx", def = "{'lastName': 1, 'age': -1}")
public class Person<T extends Address> {
@Id
private String id;
@Indexed(unique = true)
private Integer ssn;
@Field("fName")
private String firstName;
@Indexed
private String lastName;
private Integer age;
@Transient
private Integer accountTotal;
@DBRef
private List<Account> accounts;
private T address;
public Person(Integer ssn) {
this.ssn = ssn;
}
@PersistenceConstructor
public Person(Integer ssn, String firstName, String lastName, Integer age, T address) {
this.ssn = ssn;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
public String getId() {
return id;
}
// no setter for Id. (getter is only exposed for some unit testing)
public Integer getSsn() {
return ssn;
}
// other getters/setters omitted
}
当映射基础结构推断出的native MongoDB类型与预期类型不匹配时,@Field(targetType=…)可以派上用场。就像BigDecimal一样,它被表示为String而不是Decimal128,只是因为MongoDB Server的早期版本不支持它。
public class Balance {
@Field(targetType = DECIMAL128)
private BigDecimal value;
// ...
}
你甚至可以考虑使用自己的自定义注释。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Field(targetType = FieldType.DECIMAL128)
public @interface Decimal128 { }
// ...
public class Balance {
@Decimal128
private BigDecimal value;
// ...
}
5.2 特殊字段名称
一般来说,MongoDB使用点(.)字符作为嵌套documents或数组的路径分隔符。这意味着在查询(或更新语句)中,像a.b.c这样的键以如下所示的对象结构为目标:
{
'a' : {
'b' : {
'c' : …
}
}
}
因此,在MongoDB 5.0之前,字段名不能包含点(.)。使用MappingMongoConverter#setMapKeyDotReplacement可以通过在写入时用另一个字符替换点来规避存储Map结构时的一些限制。
converter.setMapKeyDotReplacement("-");
// ...
source.map = Map.of("key.with.dot", "value")
converter.write(source,...) // -> map : { 'key-with-dot', 'value' }
随着MongoDB 5.0的发布,取消了对包含特殊字符的Document字段名的限制。建议阅读MongoDB参考中更多关于在字段名称中使用点的限制。
要在Map结构中允许点,请在MappingMongoConverter上设置preserveMapKeys。
使用@Field可以通过两种方式自定义字段名称以包含点。
- @Field(name = “a.b”):该名称被认为是路径。操作需要一个嵌套对象的结构,如{ a : { b : … } }。
- @Field(name = “a.b”, fieldNameType = KEY):名称被认为是按原样命名的。操作需要一个给定值为{ ‘a.b’ : …… }的字段
由于MongoDB查询和更新语句中点字符的特殊性质,包含点的字段名不能直接作为目标,因此在派生查询方法中不能使用。考虑以下Item具有映射到名为cat.id的字段的categoryId属性。
public class Item {
@Field(name = "cat.id", fieldNameType = KEY)
String categoryId;
// ...
}
它的原始表示将是这样的
{
'cat.id' : "5b28b5e7-52c2",
...
}
由于我们不能直接针对cat.id字段(因为这将被解释为路径),我们需要聚合框架的帮助。
名称中带点的查询字段
template.query(Item.class)
// $expr : { $eq : [ { $getField : { input : '$$CURRENT', 'cat.id' }, '5b28b5e7-52c2' ] }
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("5b28b5e7-52c2"))) --------1
.all();
1. 映射层负责将属性名value转换为实际的字段名。在这里使用目标字段名也是绝对有效的。
名称中带点的更新字段
template.update(Item.class)
.matching(where("id").is("r2d2"))
// $replaceWith: { $setField : { input: '$$CURRENT', field : 'cat.id', value : 'af29-f87f4e933f97' } }
.apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "af29-f87f4e933f97")))) --------1
.first();
1. 映射层负责将属性名value转换为实际的字段名。在这里使用目标字段名也是绝对有效的。
上面显示了一个简单的示例,其中特殊字段位于顶级document级别。增加的嵌套级别增加了与字段交互所需的聚合表达式的复杂性。
5.3 自定义对象构造
映射子系统允许通过使用@PersistenceConstructor注解构造函数来自定义对象构造。要用于构造函数参数的值是通过以下方式解析的:
- 如果使用@Value对参数进行注解,则会计算给定的表达式,并将结果用作参数值。
- 如果Java类型有一个属性,其名称与输入document的给定字段匹配,那么它的属性信息将用于选择适当的构造函数参数以传递输入字段值。只有当参数名称信息存在于java .class文件中时,这才有效,这可以通过编译源代码的调试信息或在java 8中使用javac的新-parameters命令行开关来实现。
- 否则,将抛出一个MappingException,指示无法绑定给定的构造函数参数。
class OrderItem {
private @Id String id;
private int quantity;
private double unitPrice;
OrderItem(String id, @Value("#root.qty ?: 0") int quantity, double unitPrice) {
this.id = id;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// getters/setters ommitted
}
Document input = new Document("id", "4711");
input.put("unitPrice", 2.5);
input.put("qty",5);
OrderItem item = converter.read(OrderItem.class, input);
如果给定的属性路径无法解析,则quantity参数的@Value注解中的SpEL表达式将回退到值0。
使用@PersistenceConstructor注解的其他示例可以在MappingMongoConverterUnitTests测试套件中找到。
5.4 映射框架事件
事件在映射过程的整个生命周期中触发。生命周期事件一文对此进行了描述。在Spring ApplicationContext中声明这些bean会导致在分派事件时调用它们。