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

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 一、关于 MongoDB MongoDB 目前非常流行,在最近的DB-Engine排名中居第5位,仅次于传统的关系型数据库如 Oracle、Mysql。 然而在非关系型数据库领域,MongoDB已经持续成为佼佼者一段时间了,这与 MongoDB的一些优势存在一定关系: 无模式(Schema).

一、关于 MongoDB

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

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

  • 无模式(Schema),便于快速开发;
  • 面向文档化的数据,基于BSON格式(类JSON),灵活性强
  • 高性能,得益于其内存计算能力;
  • 副本集、自动分片特性,提供了高可用及水平扩展能力

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

MySql MongoDB
schema database
table collection
record document
column field

与关系型数据库一样,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. 引入框架

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

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

B. 数据库配置

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

spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.port=27017
spring.data.mongodb.username=appuser
spring.data.mongodb.password=appuser@2016
spring.data.mongodb.database=appdb

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

C. 数据模型

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

@Document(collection = "book")
@CompoundIndexes({ @CompoundIndex(name = "idx_category_voteCount", def = "{'category': 1, 'voteCount': 1}"),
        @CompoundIndex(name = "idx_category_createTime", def = "{'category': 1, 'createTime': 1}") })
public class Book {

    @Id
    private String id;

    @Indexed
    private String author;

    private String category;

    @Indexed
    private String title;

    private int voteCount;
    private int price;

    @Indexed
    private Date publishDate;

    private Date updateTime;
    private Date createTime;
...

这里,我们给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
public interface BookRepository extends MongoRepository<Book, String> {

    public List<Book> findByAuthor(String author);

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

    public Book findOneByTitle(String title);
}

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

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

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

@Service
public class BookService {

    @Autowired
    private BookRepository bookRepository;

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

    /**
     * 创建book
     * 
     * @param category
     * @param title
     * @param author
     * @param price
     * @param publishDate
     * @return
     */
    public Book createBook(String category, String title, String author, int price, Date publishDate) {
        if (StringUtils.isEmpty(category) || StringUtils.isEmpty(title) || StringUtils.isEmpty(author)) {
            return null;
        }

        Book book = new Book();
        book.setAuthor(author);
        book.setTitle(title);
        book.setCategory(category);
        book.setPrice(price);
        book.setPublishDate(publishDate);

        book.setVoteCount(0);
        book.setCreateTime(new Date());
        book.setUpdateTime(book.getCreateTime());

        return bookRepository.save(book);
    }

    /**
     * 更新价格
     * 
     * @param id
     * @param price
     * @return
     */
    public boolean updatePrice(String id, int price) {
        if (StringUtils.isEmpty(id)) {
            return false;
        }

        Book book = bookRepository.findOne(id);
        if (book == null) {

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

        book.setPrice(price);
        book.setUpdateTime(new Date());
        if (bookRepository.save(book) != null) {
            return true;
        }
        return false;
    }

    /**
     * 根据获取book
     * 
     * @param title
     * @return
     */
    public Book getBookByTitle(String title) {
        if (StringUtils.isEmpty(title)) {
            return null;
        }
        return bookRepository.findOneByTitle(title);
    }

    /**
     * 获取投票排行列表
     * 
     * @param category
     * @param max
     * @return
     */
    public List<Book> listTopVoted(String category, int max) {

        if (StringUtils.isEmpty(category) || max <= 0) {
            return Collections.emptyList();
        }

        // 按投票数倒序排序
        Sort sort = new Sort(Direction.DESC, Book.COL_VOTE_COUNT);
        PageRequest request = new PageRequest(0, max, sort);

        return bookRepository.findByCategory(category, request);

    }

    /**
     * 删除书籍
     * 
     * @param id
     * @return
     */
    public boolean deleteBook(String id) {
        Book book = bookRepository.findOne(id);
        if (book == null) {

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

        bookRepository.delete(book);
        return true;
    }

}

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

E. 自定义操作

有时候,Repository的方法映射无法较好的满足一些特定场景,比如高级检索、局部更新、覆盖索引查询等等,
此时可以使用框架提供的 MongoTemplate 工具类来完成这些定制,MongoTemplate 提供了大量的 Criteria API 来封装 Mongo-Java-Driver的实现。
我们一方面可以选择直接使用该API,另一方面,则可以更加"优雅"的整合到Repository 接口,如下面的代码:

  • 声明 Custom 接口
public interface BookRepositoryCustom {

    public PageResult<Book> search(String category, String title, String author, Date publishDataStart,
            Date publishDataEnd, Pageable pageable);

    public boolean incrVoteCount(String id, int voteIncr);
}
  • 声明接口继承关系
public interface BookRepository extends MongoRepository<Book, String>, BookRepositoryCustom{
  • 实现类
public class BookRepositoryImpl implements BookRepositoryCustom {

    @Autowired
    private MongoTemplate mongoTemplate;

    public boolean incrVoteCount(String id, int voteIncr) {
        if (StringUtils.isEmpty(id)) {
            return false;
        }

        Query query = new Query();
        query.addCriteria(Criteria.where("id").is(id));

        Update update = new Update();

        update.inc(Book.COL_VOTE_COUNT, voteIncr);
        update.set(Book.COL_UPDATE_TIME, new Date());

        WriteResult result = mongoTemplate.updateFirst(query, update, Book.class);
        return result != null && result.getN() > 0;
    }

    @Override
    public PageResult<Book> search(String category, String title, String author, Date publishDataStart,
            Date publishDataEnd, Pageable pageable) {
        Query query = new Query();

        if (!StringUtils.isEmpty(category)) {
            query.addCriteria(Criteria.where(Book.COL_CATEGORY).is(category));
        }

        if (!StringUtils.isEmpty(author)) {
            query.addCriteria(Criteria.where(Book.COL_AUTHOR).is(author));
        }

        if (!StringUtils.isEmpty(title)) {
            query.addCriteria(Criteria.where(Book.COL_TITLE).regex(title));
        }

        if (publishDataStart != null || publishDataEnd != null) {
            Criteria publishDateCond = Criteria.where(Book.COL_PUBLISH_DATE);

            if (publishDataStart != null) {
                publishDateCond.gte(publishDataStart);
            }
            if (publishDataEnd != null) {
                publishDateCond.lt(publishDataEnd);
            }
            query.addCriteria(publishDateCond);
        }

        long totalCount = mongoTemplate.count(query, Book.class);
        if (totalCount <= 0) {
            return new PageResult<Book>();
        }

        if (pageable != null) {
            query.with(pageable);
        }

        List<Book> books = mongoTemplate.find(query, Book.class);
        return PageResult.of(totalCount, books);
    }
}

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

四、高级技巧

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

其中MongoAutoConfiguration的实现如下:

@Configuration
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {

    private final MongoProperties properties;

    private final MongoClientOptions options;

    private final Environment environment;

    private MongoClient mongo;

    public MongoAutoConfiguration(MongoProperties properties,
            ObjectProvider<MongoClientOptions> options, Environment environment) {
        this.properties = properties;
        this.options = options.getIfAvailable();
        this.environment = environment;
    }

    @PreDestroy
    public void close() {
        if (this.mongo != null) {
            this.mongo.close();
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public MongoClient mongo() throws UnknownHostException {
        this.mongo = this.properties.createMongoClient(this.options, this.environment);
        return this.mongo;
    }

}

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

@SpringBootApplication
@EnableAutoConfiguration(exclude = { EmbeddedMongoAutoConfiguration.class, MongoDataAutoConfiguration.class,
        MongoAutoConfiguration.class })
public class DemoBoot {
...

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

1. 连接池配置

@Configuration
public class MongoConfig {

    @Bean
    public MongoDbFactory mongoFactory(MongoProperties mongo) throws Exception {

        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
        // 连接池配置
        builder.maxWaitTime(1000 * 60 * 1).socketTimeout(30 * 1000).connectTimeout(10 * 1000).connectionsPerHost(60)
                .minConnectionsPerHost(60).socketKeepAlive(true);

        // 设置鉴权信息
        MongoCredential credential = null;
        if (!StringUtils.isEmpty(mongo.getUsername())) {
            credential = MongoCredential.createCredential(mongo.getUsername(), mongo.getDatabase(),
                    mongo.getPassword());
        }

        MongoClientOptions mongoOptions = builder.build();

        List<ServerAddress> addrs = Arrays.asList(new ServerAddress(mongo.getHost(), mongo.getPort()));
        MongoClient mongoClient = null;
        if (credential != null) {
            mongoClient = new MongoClient(addrs, Arrays.asList(credential), mongoOptions);
        } else {
            mongoClient = new MongoClient(addrs, mongoOptions);
        }
        return new SimpleMongoDbFactory(mongoClient, mongo.getDatabase());
    }

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

//连接池最小值
private int minConnectionsPerHost;
//连接池最大值
private int maxConnectionsPerHost = 100;
//线程等待连接阻塞系数
private int threadsAllowedToBlockForConnectionMultiplier = 5;
//选择主机超时
private int serverSelectionTimeout = 1000 * 30;
//最大等待
private int maxWaitTime = 1000 * 60 * 2;
//最大连接闲时
private int maxConnectionIdleTime;
//最大连接存活
private int maxConnectionLifeTime;
//TCP建立连接超时
private int connectTimeout = 1000 * 10;
//TCP读取超时
private int socketTimeout = 0;
//TCP.keepAlive是否启用
private boolean socketKeepAlive = true;
//心跳频率
private int heartbeatFrequency = 10000;
//最小心跳间隔
private int minHeartbeatFrequency = 500;
//心跳TCP建立连接超时
private int heartbeatConnectTimeout = 20000;
//心跳TCP读取超时
private int heartbeatSocketTimeout = 20000;

2. 去掉_class属性

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

@Bean
    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext context) {

        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));

        converter.afterPropertiesSet();

        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);

        return mongoTemplate;
    }

3. 自定义序列化

一些基础的字段类型,如 int 、long、string,通过JDK 装箱类就可以完成,
对于内嵌的对象类型,SpringDataMongo框架会将其转换为 DBObject对象(java driver 实体)。
一般情况下这已经足够了,但某些场景下你不得不实现自己的序列化方式,比如通过文档存储某些特殊格式的内容。

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

   @Bean
    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext context) {

        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));

        // 自定义转换
        converter.setCustomConversions(customConversions());
        converter.afterPropertiesSet();

        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);
        return mongoTemplate;
    }

    private CustomConversions customConversions() {
        List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
        converters.add(new BasicDBObjectWriteConverter());
        converters.add(new BasicDBObjectReadConverter());
        return new CustomConversions(converters);
    }

    /**
     * 写入序列化
     */
    @WritingConverter
    public static class BasicDBObjectWriteConverter implements Converter<BasicDBObject, String> {

        public String convert(BasicDBObject source) {
            if (source == null) {
                return null;
            }
            return source.toJson();
        }
    }

    /**
     * 读取反序列化
     */
    @ReadingConverter
    public static class BasicDBObjectReadConverter implements Converter<String, BasicDBObject> {

        public BasicDBObject convert(String source) {
            if (source == null || source.length() <= 0) {
                return null;
            }
            return BasicDBObject.parse(source);
        }
    }

4. 读写分离

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

@Configuration
public class MongoConfig {

    @Bean(name="secondary")
    public MongoDbFactory mongoFactory(MongoProperties mongo) throws Exception {

        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
        // 连接池配置
        builder.maxWaitTime(1000 * 60 * 1).socketTimeout(30 * 1000).connectTimeout(10 * 1000).connectionsPerHost(60)
                .minConnectionsPerHost(60).socketKeepAlive(true);
        // 优先读备节点
        builder.readPreference(ReadPreference.secondaryPreferred());
        ...

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

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

Gitee同步代码

小结

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

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

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
目录
相关文章
|
10天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
32 4
SpringBoot入门(4) - 添加内存数据库H2
|
13天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
25 2
SpringBoot入门(4) - 添加内存数据库H2
|
5天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
40 13
|
11天前
|
NoSQL Cloud Native atlas
探索云原生数据库:MongoDB Atlas 的实践与思考
【10月更文挑战第21天】本文探讨了MongoDB Atlas的核心特性、实践应用及对云原生数据库未来的思考。MongoDB Atlas作为MongoDB的云原生版本,提供全球分布式、完全托管、弹性伸缩和安全合规等优势,支持快速部署、数据全球化、自动化运维和灵活定价。文章还讨论了云原生数据库的未来趋势,如架构灵活性、智能化运维和混合云支持,并分享了实施MongoDB Atlas的最佳实践。
|
12天前
|
NoSQL Cloud Native atlas
探索云原生数据库:MongoDB Atlas 的实践与思考
【10月更文挑战第20天】本文探讨了MongoDB Atlas的核心特性、实践应用及对未来云原生数据库的思考。MongoDB Atlas作为云原生数据库服务,具备全球分布、完全托管、弹性伸缩和安全合规等优势,支持快速部署、数据全球化、自动化运维和灵活定价。文章还讨论了实施MongoDB Atlas的最佳实践和职业心得,展望了云原生数据库的发展趋势。
|
14天前
|
存储 NoSQL MongoDB
MongoDB 数据库引用
10月更文挑战第20天
9 1
|
14天前
|
SQL Java 数据库
Spring Boot与Flyway:数据库版本控制的自动化实践
【10月更文挑战第19天】 在软件开发中,数据库的版本控制是一个至关重要的环节,它确保了数据库结构的一致性和项目的顺利迭代。Spring Boot结合Flyway提供了一种自动化的数据库版本控制解决方案,极大地简化了数据库迁移管理。本文将详细介绍如何使用Spring Boot和Flyway实现数据库版本的自动化控制。
14 2
|
20天前
|
存储 NoSQL MongoDB
基于阿里云数据库MongoDB版,微财数科“又快又稳”服务超7000万客户
选择MongoDB主要基于其灵活的数据模型、高性能、高可用性、可扩展性、安全性和强大的分析能力。
|
2天前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
6 0
|
19天前
|
存储 NoSQL MongoDB
mongodb的数据库表怎么创建
在此过程中,理解并掌握这些基本操作,是深入探索MongoDB魅力,乃至构建高效数据解决方案的关键所在。通过实践,您将更加深刻地体会到这种随需应变的数据管理模式带来的便利与效率提升。
22 0