13 June 2017

spring data mongo 初级教程

mongodb是一个比较常见的document based的nosql数据库,我们目前主要存储大量的服务器心跳信息,所以要用到它。java调用mongodb的时候,会有多种database client的选型问题。
我来说说我遇到的一些问题。最早我采用的是原生的mongo db driver v3,这种方式调用和查询很方便,但是当文档类型一致,文档结构却很复杂的时候,通过手动的解析各个字段的时候,会变得很枯燥和很痛苦。
后来转型到spring data mongodb,这个可以直接映射到java object,缺点就是文档不是很人性化,特别是那些api,简直不知道在说什么。于是,我花了不少时间,学习了下通过MongoTemplate(spring data mongodb的核心类)来直接操作mongodb查询的测试用例。主要用来解决负责查询和映射的问题。

好了,直接进入主题
1 预备条件,一个简单的可以登录创建db,collection的mongo实例
2 配置下maven的依赖

    <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-mongodb</artifactId>
                <version>1.10.3.RELEASE</version>
    </dependency>

3, 配置下mongodb的config,如果是使用spring boot,会有一个默认的spring mongodb的配置,用户只需要在application.properties or application.yaml中修改链接信息就行,当然,我们还是要知其然,也知其所以然。

这里采用的是手工配置

package com.ly.openapi.api.configs;

import com.ly.openapi.api.db.mongo.converters.DateToZonedDateTimeConverter;
import com.ly.openapi.api.db.mongo.converters.ZonedDateTimeToDateConverter;
import com.mongodb.MongoClient;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.util.Assert;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by xlm45038 on 2017/6/1.
 */
@Configuration
public class MongoConfig {
    @Value("${mongo.hosts}")
    private String[] hosts;
    @Value("${mongo.ports}")
    private int[] ports;
    @Value("${mongo.username}")
    private String username;
    @Value("${mongo.password}")
    private String password;
    @Value("${mongo.authdb}")
    private String authenticationDatabase;
    @Value("${mongo.db}")
    private String database;


    @Bean
    MongoClient mongoClient() throws UnknownHostException {
        String authDb = this.authenticationDatabase == null
                ? this.database : this.authenticationDatabase;

        Assert.isTrue(hosts.length == ports.length, "length is not equal for hosts and ports");
        List<ServerAddress> serverAddresses = new ArrayList<>();
        List<MongoCredential> credentials = new ArrayList<>();
        ServerAddress address;
        MongoCredential credential;
        for (int i = 0; i < hosts.length; i++) {
            address = new ServerAddress(hosts[0], (ports[0]));
            serverAddresses.add(address);
        }
        credential = MongoCredential.createCredential(this.username,
                authDb, this.password.toCharArray());
        credentials.add(credential);
        return new MongoClient(
                serverAddresses,
                credentials);
    }


    @Bean
    public MongoDbFactory mongoDbFactory() throws Exception {
        return new SimpleMongoDbFactory(mongoClient(), this.database);
    }


    @Bean
    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MappingMongoConverter converter) throws UnknownHostException {
        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);
        return mongoTemplate;
    }

    @Bean
    public MappingMongoConverter mappingMongoConverter(MongoDbFactory mongoDbFactory) {
        MappingMongoConverter converter = new MappingMongoConverter(
                new DefaultDbRefResolver(mongoDbFactory), new MongoMappingContext());
        converter.setCustomConversions(mongoCustomConversions());
        return converter;
    }


    @Bean
    public CustomConversions mongoCustomConversions() {
        return new CustomConversions(Arrays.asList(
                new DateToZonedDateTimeConverter(),
                new ZonedDateTimeToDateConverter()
        ));
    }
}

这里的dependency就是MongoTemplate -> MongoDbFactory -> MongoClient

还有就是,mongodb默认不支持zoneddateTime,需要自己添加conversion

public class DateToZonedDateTimeConverter implements Converter<Date, ZonedDateTime> {
    @Override
    public ZonedDateTime convert(Date source) {
        if (source == null) {
            return null;
        } else {
            return ZonedDateTime.ofInstant(source.toInstant(), ZoneOffset.ofHours(8));
        }
    }
}
package com.ly.openapi.api.db.mongo.converters;

import org.springframework.core.convert.converter.Converter;

import java.time.ZonedDateTime;
import java.util.Date;

/**
 * Created by xlm45038 on 2017/6/5.
 */
public class ZonedDateTimeToDateConverter implements Converter<ZonedDateTime, Date> {
    @Override
    public Date convert(ZonedDateTime source) {
        if (source == null)
            return null;
        else
            return Date.from(source.toInstant());
    }
}

用到的配置文件 application.properties

mongo.hosts=10.100.189.30
mongo.ports=27017
mongo.username=admin
mongo.password=o123
mongo.authdb=admin
mongo.db=studyDB

最后就是一长段testcase,看方法名就知道我在测试什么,感觉还是example代码是最好的说明文档。
后期如果有需求,我会不定期更新。

package com.ly.openapi.api.db.mongo;

import com.ly.openapi.api.configs.RootConfig;
import com.mongodb.*;
import lombok.Data;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Created by hunter.xue on 2017/6/13.
 */
@ContextConfiguration(classes = {RootConfig.class})
@RunWith(value = SpringJUnit4ClassRunner.class)
public class MongoSpringDataTest {
    private static DBCollection collection = null;
    private static DB db = null;
    @Autowired
    private MongoClient mongoClient;
    @Autowired
    private MongoTemplate mongoTemplate;

    @Before
    public void insertDummyDocuments() throws UnknownHostException {
        db = mongoClient.getDB("studyDB");
        // get a single collection
        collection = db.getCollection("dummyColl");
        System.out.println(mongoTemplate.getDb().getMongo().getAddress());
        System.out.println(mongoTemplate.getDb().getName());
        System.out.println(mongoTemplate.getCollectionNames());

        List<DBObject> list = new ArrayList<DBObject>();

        Calendar cal = Calendar.getInstance();

        for (int i = 1; i <= 5; i++) {

            BasicDBObject data = new BasicDBObject();
            data.append("number", i);
            data.append("name", "mkyong-" + i);
            cal.add(Calendar.DATE, 1);
            list.add(data);

            data = new BasicDBObject();
            data.append("number", i);
            data.append("name", "hunter-" + i);
            cal.add(Calendar.DATE, 1);
            list.add(data);

        }
        collection.insert(list);
    }

    @After
    public void tearDown() {
        collection.drop();
        db.dropDatabase();
    }

    @Test
    public void testFindAll1() {
        Query query = new Query();
//        query.addCriteria(
//                Criteria.where("HeartbeatTime").exists(true).andOperator(
//                        Criteria.where("HeartbeatTime").gte(Date.from(start.toInstant())),
//                        Criteria.where("HeartbeatTime").lt(Date.from(end.toInstant()))
//                )
//        );
        System.out.println(query.toString());
        List<MyObject> datas = mongoTemplate.findAll(MyObject.class, "dummyColl");
        datas.stream().forEach(data -> System.out.println(data));
    }

    @Test
    public void testFindAll_implicitCollectionName() {
        Query query = new Query();
        System.out.println(query.toString());
        List<MyObject> datas = mongoTemplate.findAll(MyObject.class);
        datas.stream().forEach(data -> System.out.println(data));
    }

    @Test
    public void testFindNameOnly() {
        Query query = new Query();
        query.fields().exclude("_id");
        query.fields().exclude("name");
        System.out.println(query.toString());
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(data -> System.out.println(data));
    }

    @Test
    public void testFindNumberEqualsAndWithOrder() {
        Query query = new Query();
        query.addCriteria(
                Criteria.where("number").is(5)
        );
//        query.with(new Sort(Sort.Direction.ASC, "number").and(new Sort(Sort.Direction.DESC,"name")));
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println(query);
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

    @Test
    public void testFindNumberIn() {
        Query query = new Query();
        query.addCriteria(
                Criteria.where("number").in(2, 4, 5)
        );
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println(query);
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

    @Test
    public void testBetweenAnd() {
        Query query = new Query();
        query.addCriteria(
                Criteria.where("number").gt(2).lt(5)
        );
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println("query>" + query.toString());
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

    @Test
    public void testBetweenAnd2() {
        Query query = new Query();
        query.addCriteria(
                new Criteria("number").lt(5).andOperator(
                        Criteria.where("number").gt(2),
                        new Criteria("number").lt(5))
        );
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println("query>" + query.toString());
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

    @Test
    public void testNe() {
        Query query = new Query();
        query.addCriteria(
                Criteria.where("number").ne(4)
        );
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println("query>" + query.toString());
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

    /**
        测试基本的and查询
    */
    @Test
    public void testAnd() {
        Query query = new Query();
        query.addCriteria(
                new Criteria().andOperator(
                        Criteria.where("number").is(2),
                        Criteria.where("name").is("hunter-3")
                )
        );
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println("query>" + query.toString());
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

    @Test
    public void testRegex(){
        Query query = new Query();
        query.addCriteria(
                Criteria.where("name").regex(Pattern.compile("HUN.*-[1-3]",Pattern.CASE_INSENSITIVE))
        );
        query.with(new Sort(Sort.Direction.ASC, "number", "name"));
        System.out.println("query>" + query.toString());
        List<MyObject> datas = mongoTemplate.find(query, MyObject.class);
        datas.stream().forEach(System.out::println);
    }

}

@Data
@Document(collection = "dummyColl")
class MyObject {
    private int number;
    private String name;
}

目前,spring data mongodb后台其实操作的是mongo java driver v2的代码。后期到了spring5.x时代,会迁移到mongo Java driver v3,到时候如果有api变动,我再写个new blog 追踪下。