[MongoDB] 记一次Mongo对象与Java对象POJO互转的经过
一、事件起因
最近的项目中使用到了MongoDB,主要基于官方提供的JavaAPI进行CRUD操作。一开始只借助Document作为数据转换的媒介,其中的数据转换还是较为繁琐的,如果能直接将MongDB中的数据映射为Java对象,就能减少很多冗余的转换。通过查阅文档,找到了Mongo数据与Java对象相互转换的方法。
二、版本说明
以下为本次示例相关环境的说明:
1、数据库
MongoDB server version: 4.2.10
2、JDK版本
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
3、依赖
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>4.2.2</version>
</dependency>
三、探索(踩坑)过程记录
以下对本次探索(踩坑)过程进行记录。
首先是官方给出的简单示例:
1、借助PojoCodecProvider自动映射
这里借助官方示例代码进行试验,文档参见:quick-start-pojo,代码参见:PojoQuickTour,关于PojoCodecProvider。
(1) 相关代码
首先是目录结构:
/src
/**
/entity/human
/Address.java
/Person.java
/HumanMapper.java
1) Address.java和Person.java
// Address.java
public class Address {
private String street;
private String city;
private String zip;
// 此处省略getter/setter,以及构造方法
}
// Person.java
public class Person {
private ObjectId id;
private String name;
private int age;
private Address address;
// 同上
}
2) HumanMapper.java
public class HumanMapper {
private static final String URI = "mongodb://gavin:123456@192.168.0.100:27017/crawlers";
private final MongoCollection<Person> collection;
private HumanMapper() {
/*
* 创建连接,以及获取相应的集合对象
*/
MongoClient client = MongoClients.create(URI);
MongoDatabase database = client.getDatabase("crawlers");
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(
// 通过制定automatic为true, 来实现自动映射
PojoCodecProvider.builder().automatic(true).build()
)
);
this.collection = database.getCollection("hm_person", Person.class).withCodecRegistry(codecRegistry);
}
/**
* 写入一条记录
* @param person 一条记录
*/
private void insertOne(Person person) {
this.collection.insertOne(person);
}
/**
* 批量写入
* @param people 多条记录
*/
private void insertMany(List<Person> people) {
this.collection.insertMany(people);
}
/**
* 查询
* @param query 查询条件, JSON格式
* @param projection 需要返回的字段, JSON格式
*/
private List<Person> find(String query, String projection) {
List<Person> result = new ArrayList<>();
// 此处不对query, projection的值进行校验
FindIterable<Person> iterable = projection == null ? this.collection.find(Document.parse(query)) :
this.collection.find(Document.parse(query)).projection(Document.parse(projection));
iterable.forEach(result::add);
return result;
}
public static void main(String[] args) {
HumanMapper mapper = new HumanMapper();
/*
* 写入一条记录
*/
Person p1 = randomPerson();
System.out.println(p1);
mapper.insertOne(p1);
/*
* 写入多条条记录
*/
List<Person> people = new ArrayList<Person>() {
private static final long serialVersionUID = 2L;
{
add(randomPerson());
add(randomPerson());
add(randomPerson());
}};
mapper.insertMany(people);
/*
* 根据查询条件查询
*/
String query = "{}"; // 所有数据
String projection = "{\"_id\": 0}"; // 不返回_id字段
mapper.find(query, projection).forEach(System.out::println);
/*
* 查询所有
*/
query = "{}"; // 所有数据
mapper.find(query, null).forEach(System.out::println);
}
/**
* 随机创建一个person对象
* @return person对象
*/
private static Person randomPerson() {
Random random = new Random();
int personId = random.nextInt(10);
int addressId = random.nextInt(5);
Address address = new Address("street" + addressId, "city" + addressId, "zip" + addressId);
return new Person(new ObjectId(), "name" + personId, random.nextInt(40), address);
}
}
其中,关键代码为:
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(
// 通过指定automatic为true, 来实现自动映射
PojoCodecProvider.builder().automatic(true).build()
)
);
this.collection = database.getCollection("hm_person", Person.class)
// 指定相应的codecRegistry
.withCodecRegistry(codecRegistry);
关于CodecRegistry的说明,参见:https://mongodb.github.io/mongo-java-driver/4.2/bson/codecs/。
(2) 实际遇到的问题
上述方法在正常情况下,都能转换成功。然而,我的项目里比较奇葩:MongoDB中各个字段都是大写开头的,如Field_01,在这种情况下,PojoCodecProvider中的automatic就没法正确映射了。其原因如下:源代码中会根据get/set方法截取相应的字段名,截取后首字母转为小写:
// org.bson.codecs.pojo.PropertyReflectionUtils.java:52-58
static String toPropertyName(final Method method) {
String name = method.getName();
String propertyName = name.substring(name.startsWith(IS_PREFIX) ? 2 : 3, name.length());
char[] chars = propertyName.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}
详细的转换细节,请查看org.bson.codecs.pojo.PojoBuilderHelper的configureClassModelBuilder方法。
2、通过实现Codec接口完成映射
此处参考:MongoDB查询使用Codec的简单示例。
(1) MongoDB数据结构
{
"_id": ObjectId("6050b5053e2e00006b000603"),
"Application": {
"Application_A": {
"Field_1": "1",
"Field_2": "2"
},
"Application_B": {
"Field_11": "3",
"Field_12": "4"
},
"Field_00": "000",
"Field_01": "001"
}
}
(2) 相关代码
1) 实体类
// AppTutorial.java
public class AppTutorial implements Serializable {
private static final long serialVersionUID = 2L;
private ObjectId id;
private Application Application;
// 省略getter/setter
}
// Application.java
public class Application implements Serializable {
private static final long serialVersionUID = 2L;
private ApplicationA Application_A;
private ApplicationB Application_B;
private String Field_00;
private String Field_01;
// 省略getter/setter
}
// ApplicationA.java
public class ApplicationA implements Serializable {
private static final long serialVersionUID = 2L;
private String Field_1;
private String Field_2;
}
// ApplicationB.java
public class ApplicationB implements Serializable {
private static final long serialVersionUID = 2L;
private String Field_11;
private String Field_12;
}
2) Codec和CodecProvider
本方法需要对各个结点逐一进行解析,看代码:
// AppTutorialCodec.java
public class AppTutorialCodec implements Codec<AppTutorial> {
private final CodecRegistry codecRegistry;
public AppTutorialCodec(final CodecRegistry codecRegistry) {
this.codecRegistry = codecRegistry;
}
/**
* 将Mongo数据映射为Java对象
* @param reader
* @param decoderContext
* @return
*/
@Override
public AppTutorial decode(BsonReader reader, DecoderContext decoderContext) {
AppTutorial appTutorial = new AppTutorial();
reader.readStartDocument();
/*
* 需要逐级解析
*/
readTopLevel(reader, appTutorial);
readTopLevel(reader, appTutorial);
reader.readEndDocument();
return appTutorial;
}
/**
* 解析根结点
* @param reader
* @param appTutorial
*/
private void readTopLevel(BsonReader reader, AppTutorial appTutorial) {
reader.readName();
String currentName = reader.getCurrentName();
switch (currentName) {
case "id":
case "_id":
ObjectId objectId = reader.readObjectId();
appTutorial.setId(objectId);
break;
case "Application":
Application application = new Application();
reader.readStartDocument();
readApplicationLevel(reader, application);
readApplicationLevel(reader, application);
readApplicationLevel(reader, application);
readApplicationLevel(reader, application);
reader.readEndDocument();
appTutorial.setApplication(application);
break;
default:
throw new IllegalArgumentException("illegal element: " + currentName);
}
}
/**
* 解析Application结点
* @param reader
* @param application
*/
private void readApplicationLevel(BsonReader reader, Application application) {
reader.readName();
String currentName1 = reader.getCurrentName();
switch (currentName1) {
case "Field_00":
String field00 = reader.readString();
application.setField_00(field00);
break;
case "Field_01":
String field01 = reader.readString();
application.setField_01(field01);
break;
case "Application_A":
ApplicationA applicationA = new ApplicationA();
reader.readStartDocument();
readApplicationALevel(reader, applicationA);
readApplicationALevel(reader, applicationA);
reader.readEndDocument();
application.setApplication_A(applicationA);
break;
case "Application_B":
ApplicationB applicationB = new ApplicationB();
reader.readStartDocument();
readApplicationBLevel(reader, applicationB);
readApplicationBLevel(reader, applicationB);
reader.readEndDocument();
application.setApplication_B(applicationB);
break;
default:throw new IllegalArgumentException("illegal element: " + currentName1);
}
}
/**
* 解析Application_B结点
* @param reader
* @param applicationB
*/
private void readApplicationBLevel(BsonReader reader, ApplicationB applicationB) {
reader.readName();
String currentName = reader.getCurrentName();
switch (currentName) {
case "Field_11":
String field11 = reader.readString();
applicationB.setField_11(field11);
break;
case "Field_12":
String field12 = reader.readString();
applicationB.setField_12(field12);
break;
default:throw new IllegalArgumentException("illegal element: " + currentName);
}
}
/**
* 解析Application_A结点
* @param reader
* @param applicationA
*/
private void readApplicationALevel(BsonReader reader, ApplicationA applicationA) {
reader.readName();
String currentName = reader.getCurrentName();
switch (currentName) {
case "Field_1":
String field1 = reader.readString();
applicationA.setField_1(field1);
break;
case "Field_2":
String field2 = reader.readString();
applicationA.setField_2(field2);
break;
default:throw new IllegalArgumentException("illegal element: " + currentName);
}
}
/**
* 将Java对象映射为Mongo数据结点
* @param writer
* @param value
* @param encoderContext
*/
@Override
public void encode(BsonWriter writer, AppTutorial value, EncoderContext encoderContext) {
// 根结点映射
writer.writeStartDocument();
writer.writeName("_id");
writer.writeObjectId(value.getId());
// Application结点映射
writer.writeName("Application");
writer.writeStartDocument();
writer.writeName("Field_00");
writer.writeString(value.getApplication().getField_00());
writer.writeName("Field_01");
writer.writeString(value.getApplication().getField_01());
// 将ApplicationA对象映射为Application_A结点
writer.writeName("Application_A");
writer.writeStartDocument();
writer.writeName("Field_1");
writer.writeString(value.getApplication().getApplication_A().getField_1());
writer.writeName("Field_2");
writer.writeString(value.getApplication().getApplication_A().getField_2());
writer.writeEndDocument();
// 将ApplicationB对象映射为Application_B结点
writer.writeName("Application_B");
writer.writeStartDocument();
writer.writeName("Field_11");
writer.writeString(value.getApplication().getApplication_B().getField_11());
writer.writeName("Field_12");
writer.writeString(value.getApplication().getApplication_B().getField_12());
writer.writeEndDocument();
writer.writeEndDocument();
writer.writeEndDocument();
}
@Override
public Class<AppTutorial> getEncoderClass() {
return AppTutorial.class;
}
}
// AppTutorialCodecProvider.java
public class AppTutorialCodecProvider implements CodecProvider {
@Override
@SuppressWarnings("unchecked")
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
if (clazz == AppTutorial.class) {
return (Codec<T>) new AppTutorialCodec(registry);
}
return null;
}
}
3) 具体使用
// ApplicationToMongoData.java
public class ApplicationToMongoData {
public static void main(String[] args) throws Exception {
MongoClient client = MongoClients.create("mongodb://gavin:123456@192.168.0.100:27017/crawlers");
MongoDatabase database = client.getDatabase("crawlers");
MongoCollection<AppTutorial> collection = database.getCollection("test_application", AppTutorial.class);
CodecRegistry registry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(
new AppTutorialCodecProvider()
)
);
collection = collection.withCodecRegistry(registry);
FindIterable<AppTutorial> results = collection.find().limit(100);
for (AppTutorial app : results) {
System.out.println(app);
}
}
}
此方法需要对各个结点逐一进行映射,比较繁琐。
3、使用PojoCodecProvider,通过自定义ClassModel来实现自定义映射
(1) 分析
对方法1进行深入分析,发现可以通过PojoCodecProvider.register(ClassModel...classModels)方法来实现自定义类型的映射。
1) 获取ClassModel对象
Class<T> clazz = ...; // 获取类对象
ClassModelBuilder<T> classModelBuilder = ClassModel.builder(clazz);
ClassMode<T> model = classModelBuilder.build();
2) ClassModelBuilder#build()方法如何设置属性名称
该方法中,通过为所有的PropertyModelBuilder对象的readName和writeName属性赋值,来指定映射关系。
// ClassModelBuilder#build():267-297
public ClassModel<T> build() {
...省略...
for (PropertyModelBuilder<?> propertyModelBuilder : propertyModelBuilders) {
boolean isIdProperty = propertyModelBuilder.getName().equals(idPropertyName);
if (isIdProperty) {
// 为readName何writeName属性赋值
propertyModelBuilder.readName(ID_PROPERTY_NAME).writeName(ID_PROPERTY_NAME);
}
...省略...
}
...省略...
}
3) 为PropertyModelBuilder重新赋值
通过上述两步的代码分析,只需在创建ClassModel对象时,为其所属的所有PropertyModelBuilder对象的readName和writeName重新赋值即可。
Class<T> clazz = ...;
ClassModelBuilder<T> classModelBuilder = ClassModel.builder(clazz);
List<PropertyModelBuilder<?>> propertyModelBuilders = classModelBuilder.getPropertyModelBuilders();
propertyModelBuilders.forEach(pmb -> {
String name = pmb.getName();
// 修改readName属性
pmb.readName(upperTheInitial(name));
// 修改writeName属性
pmb.writeName(upperTheInitial(name));
});
ClassModel<T> model = classModelBuilder.build();
4) 在PojoCodecProvider中使用ClassModel
...
MongoCollection<AppTutorial> collection = database.getCollection("test_application", AppTutorial.class);
ClassModel<AppTutorial> cm1 = getClassModel(AppTutorial.class);
ClassModel<Application> cm2 = getClassModel(Application.class);
ClassModel<ApplicationA> cm3 = getClassModel(ApplicationA.class);
ClassModel<ApplicationB> cm4 = getClassModel(ApplicationB.class);
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(
// 注册自定义ClassModel
PojoCodecProvider.builder().register(
cm1, cm2, cm3, cm4
).build()
)
);
collection = collection.withCodecRegistry(codecRegistry);
...
(2) 完整代码
// ApplicationBuilder.java
public class ApplicationBuilder {
public static void main(String[] args) {
MongoClient client = MongoClients.create("mongodb://gavin:123456@192.168.0.100:27017/crawlers");
MongoDatabase database = client.getDatabase("crawlers");
MongoCollection<AppTutorial> collection = database.getCollection("test_application", AppTutorial.class);
ClassModel<AppTutorial> cm1 = getClassModel(AppTutorial.class);
ClassModel<Application> cm2 = getClassModel(Application.class);
ClassModel<ApplicationA> cm3 = getClassModel(ApplicationA.class);
ClassModel<ApplicationB> cm4 = getClassModel(ApplicationB.class);
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(
// 注册自定义ClassModel
PojoCodecProvider.builder().register(
cm1, cm2, cm3, cm4
).build()
)
);
collection = collection.withCodecRegistry(codecRegistry);
read(collection);
write(collection);
}
private static void read(MongoCollection<AppTutorial> collection) {
collection.find().forEach(System.out::println);
}
private static void write(MongoCollection<AppTutorial> collection) {
AppTutorial tutorial = new AppTutorial(
new ObjectId(),
new Application(
new ApplicationA("fda1", "fda2"),
new ApplicationB("fdb1", "fdb2"),
"field1",
"field2"
)
);
collection.insertOne(tutorial);
}
/**
* 获取clazz类对应的ClassModel对象
* @param clazz 类对象
* @param <T> 相应类型
* @return ClassModel
*/
private static <T> ClassModel<T> getClassModel(Class<T> clazz) {
ClassModelBuilder<T> classModelBuilder = ClassModel.builder(clazz);
List<PropertyModelBuilder<?>> propertyModelBuilders = classModelBuilder.getPropertyModelBuilders();
propertyModelBuilders.forEach(pmb -> {
String name = pmb.getName();
// 修改readName属性
pmb.readName(upperTheInitial(name));
// 修改writeName属性
pmb.writeName(upperTheInitial(name));
});
return classModelBuilder.build();
}
/**
* 首字母变为大写
* @param src src
* @return new string
*/
private static String upperTheInitial(String src) {
char[] chars = src.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
return new String(chars);
}
}
运行结果如下:
AppTutorial{id=6050b5053e2e00006b000603, Application=Application{Application_A=ApplicationA{Field_1='1', Field_2='2'}, Application_B=ApplicationB{Field_11='3', Field_12='4'}, Field_00='000', Field_01='001'}}
四、后续
1、更新于2021.05.26
今天在使用jackson的时候,发现在类的属性上使用@JsonProperty(value="")可以指定该属性对应的json字段,顺便就联想到当初在使用mongo时也有类似的需求。由于mongo中有相应的数据结构bson,于是就想到是否也有BsonProperty之类的注解,经过一番尝试发现,还真有:@BsonProperty("")。在此简单记录。
参考:
1、MongoDB查询使用Codec的简单示例;
2、Codec;
3、QuickStartPojos;
4、PojoQuickTour。

本文介绍MongoDB与Java对象互相转换的三种方法:利用PojoCodecProvider自动映射、实现Codec接口手动映射及自定义ClassModel实现特定映射。
1万+

被折叠的 条评论
为什么被折叠?



