[MongoDB] 记一次Mongo对象与Java对象的互转

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

一、事件起因

最近的项目中使用到了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.PojoBuilderHelperconfigureClassModelBuilder方法。

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对象的readNamewriteName属性赋值,来指定映射关系。

// 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对象的readNamewriteName重新赋值即可。

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值