springboot mongodb数据库应用技巧
一、关于 MongoDB
MongoDB 目前非常流行,在最近的DB-Engine排名中居第5位,仅次于传统的关系型数据库如 Oracle、Mysql。
vcCp1bnE3MGmPC9saT4NCjwvdWw+DQo8cD5Nb25nb0RCILXE1vfSqrbUz/Ow/MCoyv2+3b/io6hkYXRhYmFzZaOpoaK8r7rPo6hjb2xsZWN0aW9uo6mhos7EtbW21M/zo6hkb2N1bWVudKOpo6zT67nYz7XQzcr9vt2/4rXEttTTprnYz7XI58/Co7o8L3A+DQo8dGFibGU+DQo8dGhlYWQ+DQoJPHRyPg0KCTx0aD4NCgkJTXlTcWw8L3RoPg0KCTx0aD4NCgkJTW9uZ29EQjwvdGg+DQoJPC90cj4NCjwvdGhlYWQ+DQo8dGJvZHk+DQoJPHRyPg0KCTx0ZD5zY2hlbWE8L3RkPg0KCTx0ZD5kYXRhYmFzZTwvdGQ+DQoJPC90cj4NCgk8dHI+DQoJPHRkPnRhYmxlPC90ZD4NCgk8dGQ+Y29sbGVjdGlvbjwvdGQ+DQoJPC90cj4NCgk8dHI+DQoJPHRkPnJlY29yZDwvdGQ+DQoJPHRkPmRvY3VtZW50PC90ZD4NCgk8L3RyPg0KCTx0cj4NCgk8dGQ+Y29sdW1uPC90ZD4NCgk8dGQ+ZmllbGQ8L3RkPg0KCTwvdHI+DQo8L3Rib2R5Pg0KPC90YWJsZT4NCjxwPtPrudjPtdDNyv2+3b/i0rvR+aOsTW9uZ29EQtKy1qez1sv30v0osrvWp7PWzeK8/CmjrMi7tvjG5MO709C2qNLlucy2qLXEwdAoQ29sdW1uKaOs19a2zr/J0tTKx8jOus7A4NDNtcTWtaOsscjI58r91rWhosr91+m78se2zNfOxLW1tcihozxiciAvPg0K1NrX7r38t6KyvLXENC4wsOaxvtbQo6xNb25nb0RCv6rKvNans9bKws7xoaO/ybz7o6zU2s60wLTV4tCpyv2+3b/i1q685LXEsu7S7Na7u+HUvcC01L3J2aGjPC9wPg0KPGgyPrb+oaJTcHJpbmctRGF0YS1Nb25nbzwvaDI+DQo8cD5TcHJpbmctRGF0YS1Nb25nbyDKx1NwcmluZ7/yvNy21NPaTW9uZ29EQiDK/b7dtsHQtLXET1JNILfi17CjrDxiciAvPg0K0+sgtPO80srsz6S1xCBKUEHSu9H5o6zG5NTaTW9uZ29EQi1KYXZhLURyaXZlcrv5tKHWrsnP1/bBy9K70Km34tewo6zB7tOm08O/qreiuPy807zyseOhozwvcD4NCjxwPsjnz8LKxzxzdHJvbmc+U3ByaW5nRGF0YTwvc3Ryb25nPiDV+8zlv/K83LXE0ru49rjF0qqjujwvcD4NCjxwPjxpbWcgYWx0PQ=="" src="http://file.laike.net/d/img/20191109025812082.png" title="" />
从上图中可以看出,SpringData 是基于分层设计的。从下之上,分别是:
- 数据库层;
- 驱动层(JDBC/Driver);
- ORM层(Repository);
三、整合 MongoDB CRUD
接下来的篇幅,主要针对如何在项目中使用框架进行MongoDB数据库的读写,部分代码可供参考。
A. 引入框架
org.springframework.boot spring-boot-starter-data-mongodb${spring-boot.version}
其中 spring-boot-starter-mongodb 是一个胶水组件,声明对它的依赖会令项目自动引入spring-data-mongo、mongodb-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{ public List findByAuthor(String author); public List 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 ListlistTopVoted(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 PageResultsearch(String category, String title, String author, Date publishDataStart, Date publishDataEnd, Pageable pageable); public boolean incrVoteCount(String id, int voteIncr); }
- 声明接口继承关系
public interface BookRepository extends MongoRepository, 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 PageResultsearch(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 (); } if (pageable != null) { query.with(pageable); } List 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, ObjectProvideroptions, 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(); Listaddrs = 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> converters = new ArrayList >(); converters.add(new BasicDBObjectWriteConverter()); converters.add(new BasicDBObjectReadConverter()); return new CustomConversions(converters); } /** * 写入序列化 */ @WritingConverter public static class BasicDBObjectWriteConverter implements Converter { public String convert(BasicDBObject source) { if (source == null) { return null; } return source.toJson(); } } /** * 读取反序列化 */ @ReadingConverter public static class BasicDBObjectReadConverter implements Converter { 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篇" ,期待更多精彩内容^-^