补习系列(16)-springboot mongodb 数据库应用技巧

https://mp.weixin.qq.com/s/0JmMdAGi9LFy6iKnSVEVWQ

一、关于 MongoDB

MongoDB 目前非常流行,在最近的DB-Engine排名中居第5位,仅次于传统的关系型数据库如 Oracle、Mysql。

 

然而在非关系型数据库领域,MongoDB已经持续成为佼佼者一段时间了,这与 MongoDB的一些优势存在一定关系:

  • 无模式(Schema),便于快速开发;

  • 面向文档化的数据,基于BSON格式(类JSON),灵活性强

  • 高性能,得益于其内存计算能力;

  • 副本集、自动分片特性,提供了高可用及水平扩展能力

 

MongoDB 的主要对象包括数据库(database)、集合(collection)、文档对象(document),与关系型数据库的对应关系如下:

MySqlMongoDB
schemadatabase
tablecollection
recorddocument
columnfield

与关系型数据库一样,MongoDB也支持索引(不支持外键),然而其没有定义固定的列(Column),字段可以是任何类型的值,比如数值、数组或嵌套文档等。 在最近发布的4.0版本中,MongoDB开始支持事务。可见,在未来这些数据库之间的差异只会越来越少。

 

二、Spring-Data-Mongo

Spring-Data-Mongo 是Spring框架对于MongoDB 数据读写的ORM 封装, 与 大家熟悉的 JPA一样,其在MongoDB-Java-Driver基础之上做了一些封装,令应用开发更加简便。

如下是SpringData 整体框架的一个概要:

从上图中可以看出,SpringData 是基于分层设计的。从下之上,分别是:

  • 数据库层;

  • 驱动层(JDBC/Driver);

  • ORM层(Repository);

     

 

三、整合 MongoDB CRUD

接下来的篇幅,主要针对如何在项目中使用框架进行MongoDB数据库的读写,部分代码可供参考。

A. 引入框架

 
  1.        <dependency>

  2.            <groupId>org.springframework.boot</groupId>

  3.            <artifactId>spring-boot-starter-data-mongodb</artifactId>

  4.            <version>${spring-boot.version}</version>

  5.        </dependency>

其中 spring-boot-starter-mongodb 是一个胶水组件,声明对它的依赖会令项目自动引入spring-data-mongomongodb-java-driver等基础组件。

 

B. 数据库配置

我们在 application.properties 中声明一段配置:

 
  1. spring.data.mongodb.host=127.0.0.1

  2. spring.data.mongodb.port=27017

  3. spring.data.mongodb.username=appuser

  4. spring.data.mongodb.password=appuser@2016

  5. spring.data.mongodb.database=appdb

不难理解,这里是数据库主机、端口、用户密码、数据库的设置。

 

C. 数据模型

接下来,要定义数据集合(collection) 的一个结构,以 Book实体为例:

 
  1. @Document(collection = "book")

  2. @CompoundIndexes({ @CompoundIndex(name = "idx_category_voteCount", def = "{'category': 1, 'voteCount': 1}"),

  3.        @CompoundIndex(name = "idx_category_createTime", def = "{'category': 1, 'createTime': 1}") })

  4. public class Book {

  5.  

  6.    @Id

  7.    private String id;

  8.  

  9.    @Indexed

  10.    private String author;

  11.  

  12.    private String category;

  13.  

  14.    @Indexed

  15.    private String title;

  16.  

  17.    private int voteCount;

  18.    private int price;

  19.  

  20.    @Indexed

  21.    private Date publishDate;

  22.  

  23.    private Date updateTime;

  24.    private Date createTime;

  25. ...

这里,我们给Book 实体定义了一些属性:

属性名描述
id书籍ID
author作者
category书籍分类
title书籍标题
voteCount投票数量
price价格
publishDate发布日期
updateTime更新时间
createTime创建时间

 

除此以外,我们还会用到几个注解:

注解描述
@Document声明实体为MongoDB文档
@Id标记ID属性
@Indexed单键索引
@CompoundIndexes复合索引集
@CompoundIndex复合索引

关于MongoDB索引形态,可以参考官方文档做一个详细了解。

 

D. 数据操作

ORM 框架可以让你通过操作对象来直接影响数据,这样一来,可以大大减少上手的难度,你不再需要熟悉大量驱动层的API了。 

Spring-Data-Mongo 实现了类JPA的接口,通过预定义好的Repository可实现代码方法到数据库操作语句DML的映射。 

下面是一些例子:

  • BookRepository

 
  1. public interface BookRepository extends MongoRepository<Book, String> {

  2.  

  3.    public List<Book> findByAuthor(String author);

  4.  

  5.    public List<Book> findByCategory(String category, Pageable pageable);

  6.  

  7.    public Book findOneByTitle(String title);

  8. }

 

我们所看到的 findByAttribute 将会直接被转换成对应的条件查询,

如 findByAuthor 等价于

 
  1. db.book.find({author:'Lilei'})

 

接下来,我们可以方便的在业务逻辑层(service层) 对Repository 进行调用,如下:

 
  1. @Service

  2. public class BookService {

  3.  

  4.    @Autowired

  5.    private BookRepository bookRepository;

  6.  

  7.    private static final Logger logger = LoggerFactory.getLogger(BookService.class);

  8.  

  9.    /**

  10.     * 创建book

  11.     *

  12.     * @param category

  13.     * @param title

  14.     * @param author

  15.     * @param price

  16.     * @param publishDate

  17.     * @return

  18.     */

  19.    public Book createBook(String category, String title, String author, int price, Date publishDate) {

  20.        if (StringUtils.isEmpty(category) || StringUtils.isEmpty(title) || StringUtils.isEmpty(author)) {

  21.            return null;

  22.        }

  23.  

  24.        Book book = new Book();

  25.        book.setAuthor(author);

  26.        book.setTitle(title);

  27.        book.setCategory(category);

  28.        book.setPrice(price);

  29.        book.setPublishDate(publishDate);

  30.  

  31.        book.setVoteCount(0);

  32.        book.setCreateTime(new Date());

  33.        book.setUpdateTime(book.getCreateTime());

  34.  

  35.        return bookRepository.save(book);

  36.    }

  37.  

  38.    /**

  39.     * 更新价格

  40.     *

  41.     * @param id

  42.     * @param price

  43.     * @return

  44.     */

  45.    public boolean updatePrice(String id, int price) {

  46.        if (StringUtils.isEmpty(id)) {

  47.            return false;

  48.        }

  49.  

  50.        Book book = bookRepository.findOne(id);

  51.        if (book == null) {

  52.  

  53.            logger.info("the book '{}' is not exist", id);

  54.            return false;

  55.        }

  56.  

  57.        book.setPrice(price);

  58.        book.setUpdateTime(new Date());

  59.        if (bookRepository.save(book) != null) {

  60.            return true;

  61.        }

  62.        return false;

  63.    }

  64.  

  65.    /**

  66.     * 根据获取book

  67.     *

  68.     * @param title

  69.     * @return

  70.     */

  71.    public Book getBookByTitle(String title) {

  72.        if (StringUtils.isEmpty(title)) {

  73.            return null;

  74.        }

  75.        return bookRepository.findOneByTitle(title);

  76.    }

  77.  

  78.    /**

  79.     * 获取投票排行列表

  80.     *

  81.     * @param category

  82.     * @param max

  83.     * @return

  84.     */

  85.    public List<Book> listTopVoted(String category, int max) {

  86.  

  87.        if (StringUtils.isEmpty(category) || max <= 0) {

  88.            return Collections.emptyList();

  89.        }

  90.  

  91.        // 按投票数倒序排序

  92.        Sort sort = new Sort(Direction.DESC, Book.COL_VOTE_COUNT);

  93.        PageRequest request = new PageRequest(0, max, sort);

  94.  

  95.        return bookRepository.findByCategory(category, request);

  96.  

  97.    }

  98.  

  99.    /**

  100.     * 删除书籍

  101.     *

  102.     * @param id

  103.     * @return

  104.     */

  105.    public boolean deleteBook(String id) {

  106.        Book book = bookRepository.findOne(id);

  107.        if (book == null) {

  108.  

  109.            logger.info("the book '{}' is not exist", id);

  110.            return false;

  111.        }

  112.  

  113.        bookRepository.delete(book);

  114.        return true;

  115.    }

  116.  

  117. }

关于Repository 映射规则,可以从这里找到详细介绍。

 

E. 自定义操作

有时候,Repository的方法映射无法较好的满足一些特定场景,比如高级检索、局部更新、覆盖索引查询等等, 此时可以使用框架提供的 MongoTemplate 工具类来完成这些定制。

MongoTemplate 提供了大量的 Criteria API 来封装 Mongo-Java-Driver的实现。 

我们一方面可以选择直接使用该API,另一方面,则可以更加"优雅"的整合到Repository 接口,如下面的代码:

 

  • 声明 Custom 接口

     

 
  1. public interface BookRepositoryCustom {

  2.  

  3.    public PageResult<Book> search(String category,

  4.        
       String title, String author, Date publishDataStart,

  5.            Date publishDataEnd, Pageable pageable);

  6.  

  7.    public boolean incrVoteCount(String id, int voteIncr);

  8. }

 

  • 声明接口继承关系

 
  1. public interface BookRepository extends

  2.      MongoRepository<Book, String>, BookRepositoryCustom{

 

  • 实现类

 
  1. public class BookRepositoryImpl implements BookRepositoryCustom {

  2.  

  3.    @Autowired

  4.    private MongoTemplate mongoTemplate;

  5.  

  6.    public boolean incrVoteCount(String id, int voteIncr) {

  7.        if (StringUtils.isEmpty(id)) {

  8.            return false;

  9.        }

  10.  

  11.        Query query = new Query();

  12.        query.addCriteria(Criteria.where("id").is(id));

  13.  

  14.        Update update = new Update();

  15.  

  16.        update.inc(Book.COL_VOTE_COUNT, voteIncr);

  17.        update.set(Book.COL_UPDATE_TIME, new Date());

  18.  

  19.        WriteResult result = mongoTemplate.updateFirst(query, update, Book.class);

  20.        return result != null && result.getN() > 0;

  21.    }

  22.  

  23.    @Override

  24.    public PageResult<Book> search(String category, String title, String author, Date publishDataStart,

  25.            Date publishDataEnd, Pageable pageable) {

  26.        Query query = new Query();

  27.  

  28.        if (!StringUtils.isEmpty(category)) {

  29.            query.addCriteria(Criteria.where(Book.COL_CATEGORY).is(category));

  30.        }

  31.  

  32.        if (!StringUtils.isEmpty(author)) {

  33.            query.addCriteria(Criteria.where(Book.COL_AUTHOR).is(author));

  34.        }

  35.  

  36.        if (!StringUtils.isEmpty(title)) {

  37.            query.addCriteria(Criteria.where(Book.COL_TITLE).regex(title));

  38.        }

  39.  

  40.        if (publishDataStart != null || publishDataEnd != null) {

  41.            Criteria publishDateCond = Criteria.where(Book.COL_PUBLISH_DATE);

  42.  

  43.            if (publishDataStart != null) {

  44.                publishDateCond.gte(publishDataStart);

  45.            }

  46.            if (publishDataEnd != null) {

  47.                publishDateCond.lt(publishDataEnd);

  48.            }

  49.            query.addCriteria(publishDateCond);

  50.        }

  51.  

  52.        long totalCount = mongoTemplate.count(query, Book.class);

  53.        if (totalCount <= 0) {

  54.            return new PageResult<Book>();

  55.        }

  56.  

  57.        if (pageable != null) {

  58.            query.with(pageable);

  59.        }

  60.  

  61.        List<Book> books = mongoTemplate.find(query, Book.class);

  62.        return PageResult.of(totalCount, books);

  63.    }

  64. }

 

利用 AOP的魔法 ,Spring 框架会自动将我们这段代码实现 织入 到Bean对象中, 这样一来,我们原先对Repository的依赖引用方式就不需要改变了。

 

四、高级技巧

SpringBoot中完成Mongodb的自动化配置,是通过MongoAutoConfiguration、MongoDataAutoConfiguration完成的。

其中MongoAutoConfiguration的实现如下:

 
  1. @Configuration

  2. @ConditionalOnClass(MongoClient.class)

  3. @EnableConfigurationProperties(MongoProperties.class)

  4. @ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")

  5. public class MongoAutoConfiguration {

  6.  

  7.    private final MongoProperties properties;

  8.  

  9.    private final MongoClientOptions options;

  10.  

  11.    private final Environment environment;

  12.  

  13.    private MongoClient mongo;

  14.  

  15.    public MongoAutoConfiguration(MongoProperties properties,

  16.            ObjectProvider<MongoClientOptions> options, Environment environment) {

  17.        this.properties = properties;

  18.        this.options = options.getIfAvailable();

  19.        this.environment = environment;

  20.    }

  21.  

  22.    @PreDestroy

  23.    public void close() {

  24.        if (this.mongo != null) {

  25.            this.mongo.close();

  26.        }

  27.    }

  28.  

  29.    @Bean

  30.    @ConditionalOnMissingBean

  31.    public MongoClient mongo() throws UnknownHostException {

  32.        this.mongo = this.properties.createMongoClient(this.options, this.environment);

  33.        return this.mongo;

  34.    }

  35.  

  36. }

从上面的代码可见,如果应用代码中未声明 MongoClient、MongoDbFactory,那么框架会根据配置文件自动做客户端的初始化。 

通过声明,可以取消这些自动化配置:

 
  1. @SpringBootApplication

  2. @EnableAutoConfiguration(exclude = { EmbeddedMongoAutoConfiguration.class, MongoDataAutoConfiguration.class,

  3.        MongoAutoConfiguration.class })

  4. public class DemoBoot {

  5. ...

 

真实线上的项目中,会对MongoDB 客户端做一些定制,下面介绍几个常见用法

1. 连接池配置

 
  1. @Configuration

  2. public class MongoConfig {

  3.  

  4.    @Bean

  5.    public MongoDbFactory mongoFactory(MongoProperties mongo) throws Exception {

  6.  

  7.        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();

  8.        // 连接池配置

  9.        builder.maxWaitTime(1000 * 60 * 1).socketTimeout(30 * 1000).connectTimeout(10 * 1000).connectionsPerHost(60)

  10.                .minConnectionsPerHost(60).socketKeepAlive(true);

  11.  

  12.        // 设置鉴权信息

  13.        MongoCredential credential = null;

  14.        if (!StringUtils.isEmpty(mongo.getUsername())) {

  15.            credential = MongoCredential.createCredential(mongo.getUsername(), mongo.getDatabase(),

  16.                    mongo.getPassword());

  17.        }

  18.  

  19.        MongoClientOptions mongoOptions = builder.build();

  20.  

  21.        List<ServerAddress> addrs = Arrays.asList(new ServerAddress(mongo.getHost(), mongo.getPort()));

  22.        MongoClient mongoClient = null;

  23.        if (credential != null) {

  24.            mongoClient = new MongoClient(addrs, Arrays.asList(credential), mongoOptions);

  25.        } else {

  26.            mongoClient = new MongoClient(addrs, mongoOptions);

  27.        }

  28.        return new SimpleMongoDbFactory(mongoClient, mongo.getDatabase());

  29.    }

这里我们所关心的,往往是连接池大小、超时参数阈值、队列这几个,

如下:

 
  1. //连接池最小值

  2. private int minConnectionsPerHost;

  3. //连接池最大值

  4. private int maxConnectionsPerHost = 100;

  5. //线程等待连接阻塞系数

  6. private int threadsAllowedToBlockForConnectionMultiplier = 5;

  7. //选择主机超时

  8. private int serverSelectionTimeout = 1000 * 30;

  9. //最大等待

  10. private int maxWaitTime = 1000 * 60 * 2;

  11. //最大连接闲时

  12. private int maxConnectionIdleTime;

  13. //最大连接存活

  14. private int maxConnectionLifeTime;

  15. //TCP建立连接超时

  16. private int connectTimeout = 1000 * 10;

  17. //TCP读取超时

  18. private int socketTimeout = 0;

  19. //TCP.keepAlive是否启用

  20. private boolean socketKeepAlive = true;

  21. //心跳频率

  22. private int heartbeatFrequency = 10000;

  23. //最小心跳间隔

  24. private int minHeartbeatFrequency = 500;

  25. //心跳TCP建立连接超时

  26. private int heartbeatConnectTimeout = 20000;

  27. //心跳TCP读取超时

  28. private int heartbeatSocketTimeout = 20000;

 

2. 去掉_class属性

通过 SpringDataMongo 定义的实体,会自动写入一个_class属性,大多数情况下这个不是必须的,可以通过配置去掉:

 
  1. @Bean

   public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext context) {

  1.    DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);

  2.    MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);

  3.    converter.setTypeMapper(new DefaultMongoTypeMapper(null));

  4.  

  5.    converter.afterPropertiesSet();        

       MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);

       return mongoTemplate;

  1. }

 

3. 自定义序列化

一些基础的字段类型,如 int 、long、string,通过JDK 装箱类就可以完成, 对于内嵌的对象类型,SpringDataMongo框架会将其转换为 DBObject对象(java driver 实体)。 

一般情况下这已经足够了,但某些场景下你不得不实现自己的序列化方式,比如通过文档存储某些特殊格式的内容。

这需要用到 Converter 接口,如下面的代码:

 
  1.   @Bean

  2.    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,

  3.                         MongoMappingContext context) {

  4.  

  5.        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);

  6.        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);

  7.        converter.setTypeMapper(new DefaultMongoTypeMapper(null));

  8.  

  9.        // 自定义转换

  10.        converter.setCustomConversions(customConversions());

  11.        converter.afterPropertiesSet();

  12.  

  13.        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);

  14.        return mongoTemplate;

  15.    }

  16.  

  17.    private CustomConversions customConversions() {

  18.        List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();

  19.        converters.add(new BasicDBObjectWriteConverter());

  20.        converters.add(new BasicDBObjectReadConverter());

  21.        return new CustomConversions(converters);

  22.    }

  23.  

  24.    /**

  25.     * 写入序列化

  26.     */

  27.    @WritingConverter

  28.    public static class BasicDBObjectWriteConverter

  29.                   implements Converter<BasicDBObject, String> {

  30.  

  31.        public String convert(BasicDBObject source) {

  32.            if (source == null) {

  33.                return null;

  34.            }

  35.            return source.toJson();

  36.        }

  37.    }

  38.  

  39.    /**

  40.     * 读取反序列化

  41.     */

  42.    @ReadingConverter

  43.    public static class BasicDBObjectReadConverter

  44.                     implements Converter<String, BasicDBObject> {

     

  45.        public BasicDBObject convert(String source) {

  46.            if (source == null || source.length() <= 0) {

  47.                return null;

  48.            }

  49.            return BasicDBObject.parse(source);

  50.        }

  51.    }

 

4. 读写分离

MongoDB 本身支持读写分离的实现,前提是采用副本集、分片副本集的架构, 通过声明客户端的 ReadPreference 级别可以达到优先读主、优先读备的控制。

 
  1. @Configuration

  2. public class MongoConfig {

  3.  

  4.    @Bean(name="secondary")

  5.    public MongoDbFactory mongoFactory(MongoProperties mongo) throws Exception {

  6.  

  7.        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();

  8.        // 连接池配置

  9.        builder.maxWaitTime(1000 * 60 * 1).socketTimeout(30 * 1000).connectTimeout(10 * 1000).connectionsPerHost(60)

  10.                .minConnectionsPerHost(60).socketKeepAlive(true);

  11.        // 优先读备节点

  12.        builder.readPreference(ReadPreference.secondaryPreferred());

  13.        ...

上面的代码中,将会为MongoClient 设置 secondaryPreferred 的读级别。 ReadPreference 级别包括以下几种:

级别描述
primary默认值,只从主节点读,主节点不可用时报错
primaryPreferred优先主节点(primary)读,主节点不可用时到从节点(secondary)读
secondary仅从备节点(secondary)读取数据
secondaryPreferred优先从备节点读,从节点不可用时到主节点读取
nearest到网络延迟最低的节点读取数据,不管是主节点还是从节点

 

小结

MongoDB 是当下 NoSQL 数据库的首选,也有不少服务化架构采用了 MongoDB作为主要数据库, 其在 4.x版本中即将推出事务功能,在未来该文档数据库相对于RDBMS的差距将会大大缩小。 也正由于MongoDB 具备 简单、易扩展、高性能等特性,其社区活跃度非常高,是非常值得关注和学习的。

欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容^-^

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值