上一篇记录了 MongoDB 的一些基础知识,以及在 Mongo Shell 中操作 MongoDB,本文内容将更贴合实际的开发,主要介绍如何使用 SpringBoot 来操作 MongoDB,采用目前最新的 SpringBoot2.6.0 版本。
一、准备工作
这里通过 Spring Data 来集成 MongoDB,此时它对应 MongoDB4.4.0 版本的驱动:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
然后就是连接到 MongoDB,这里连接到我们本地搭建的副本集,这样也就可以支持事务了:
spring:
data:
mongodb:
auto-index-creation: true
# 连接副本集,slaveOk=true 表示开启副本节点的读支持,可实现读写分离,connect=replicaSet 表示自动到副本集中选择读写的主机,replicaSet=myrs 用来指定副本集的名称
uri: mongodb://localhost:27017,localhost:27018,localhost:27019/mydb?connect=replicaSet&slaveOk=true&replicaSet=myrs
目前需要的配置就这些了。
根据 SpringBoot 的自动装配机制,项目启动时会创建一个MongoTemplate
对象,MongoTemplate
提供了增、删、改、查、聚合等方法可以方便的操作 MongoDB。
除了直接使用MongoTemplate
,还可以采用JPA
的方式,即创建一个接口集成MongoRepository<T, ID>
,比如:
public interface BookRepository extends MongoRepository<Book, String> {
}
这样注入BookRepository
对象也就可以操作 MongoDB 了,也可以使用JPA
的提供的关键字扩展一些新的查询方法。但是相比MongoTemplate
,JPA
方式所提供的的功能就显得比较弱了,所以接下来我们重点讨论如何使用MongoTemplate
来操作 MongoDB。
接下来创建实体类Book
来对应数据库中集合的文档结构:
// 指定对应的集合名
@Document(collection = "book")
// 创建复合索引
@CompoundIndexes({@CompoundIndex(name = "author_name_index", def = "{author: 1, name: 1}")})
public class Book {
// 指定主键
@Id
private String id;
// 给 skuId 字段创建索引,并设置成唯一索引
@Indexed(unique = true, useGeneratedName = true)
private String skuId;
// bookName 是文档中的字段名
@Field("bookName")
private String name;
private String author;
// 指定文档中实际存储的类型,不指定则自推断
@Field(targetType = FieldType.DECIMAL128)
private BigDecimal price;
@Field(targetType = FieldType.INT64)
private Long commentCount;
private String shop;
private String publisher;
private String img;
...省略get\set方法...
}
在Book
类中指定了集合名、主键、创建了索引、建立了字段名称以及类型的映射关系。通过注解创建索引的配置默认是不生效的,需要在配置文件开启auto-index-creation: true
FieldType
是个枚举类,用来指定Book
中字段在 MongoDB 中的类型。
后边操作 MongoDB 的代码都定义在BookService
类里:
@Service
public class BookService {
@Autowired
private MongoTemplate mongoTemplate;
}
二、集合、索引
还需要创建一个book
集合来存储文档数据,可以隐式创建,也可以通过指定集合名来显式创建:
/**
* 创建集合
*/
public void createCollection() {
mongoTemplate.createCollection("book");
}
删除集合也类似:
/**
* 删除集合
*/
public void dropCollection() {
mongoTemplate.dropCollection("book");
}
前边我们在Book
类中用注解自动创建索引,也可以在后期根据需要手动创建:
/**
* 创建索引
*/
public void createIndex() {
IndexOptions options = new IndexOptions();
options.unique(true);
// options.name("");
mongoTemplate.getCollection("book").createIndex(Indexes.ascending("skuId"), options);
}
/**
* 同时创建创建多个索引
*/
public void createIndexes() {
IndexModel index1 = new IndexModel(Indexes.ascending("skuId"));
IndexModel index2 = new IndexModel(Indexes.ascending("publisher"));
List<IndexModel> indexes = Arrays.asList(index1, index2);
mongoTemplate.getCollection("book").createIndexes(indexes);
}
但我目前没找到创建复合索引的方法......
索引也可以删除:
/**
* 删除索引
*/
public void dropIndex(String indexName) {
mongoTemplate.getCollection("book").dropIndex(indexName);
}
三、添加文档
给集合中添加文档,可以使用MongoTemplate
的insert
系列方法来完成:
/**
* 添加一条数据
*
* @param book
*/
public void addBook(Book book) {
mongoTemplate.insert(book, "book");
}
/**
* 批量添加数据
*
* @param books
*/
public void addBook(List<Book> books) {
mongoTemplate.insert(books, "book");
}
这里添加一些提前准备好的数据:
public void writeBookDataToMongoDB() {
String filePath = System.getProperty("user.dir") + File.separator + "jd_book2.txt";
FileReader fileReader = null;
BufferedReader bufferedReader = null;
try {
fileReader = new FileReader(filePath);
bufferedReader = new BufferedReader(fileReader);
String line;
ArrayList<Book> books = new ArrayList<>();
while ((line = bufferedReader.readLine()) != null) {
books.add(JSON.parseObject(line, Book.class));
if (books.size() >= 1000) {
bookService.addBook(books);
books.clear();
}
}
bookService.addBook(books);
} catch (Exception e) {
e.printStackTrace();
} finally {
......
}
}
由于我们使用了副本集,则添加的数据会通过主节点写入,然后同步到副本节点,可以在 Mongo Shell 中确认是否成功:
也可以通过 NoSQLBooster for MongoDB 查看具体的数据,Book
类中配置的索引、字段类型:
四、查询文档
查询操作可以使用find
系列方法:
/**
* 根据 id 查询
*/
public Book queryBook(String id) {
return mongoTemplate.findById(id, Book.class);
}
/**
* 复杂条件查询
*/
public List<Book> queryBook(String bookName, Integer pageNum, Integer pageSize) {
// 由于使用了 regex,这里的 bookName 支持正则表达式
Criteria criteria = Criteria.where("bookName").regex(bookName).and("commentCount").gte(100000);
// 设置分页,pageNum 从 0 开始(也可以使用 skip、limit 来设置)、设置排序
Query query = Query.query(criteria)
.with(PageRequest.of(pageNum, pageSize))
.with(Sort.by("commentCount").descending());
List<Book> books = mongoTemplate.find(query, Book.class);
return books;
}
find
相关的重载方法比较多,除了例子中的还有findOne()
、findDistinct()
、findAndRemove()
等。
Criteria
是用来构造具体的查询条件的,Query
是最终封装的查询对象,可以整合Criteria
,以及设置排序(PageRequest
)、分页(Sort
)等。Criteria
、Query
这两个类在后边会经常用到的。
五、删除文档
要删除集合中的文档,可以使用remove
系列的方法,一般情况下,都是根据指定条件删除的:
/**
* 根据条件删除
*/
public long deleteBook(String skuId) {
// 构造查询条件
Criteria criteria = Criteria.where("skuId").is(skuId);
Query query = Query.query(criteria);
// 执行删除操作
DeleteResult deleteResult = mongoTemplate.remove(query, Book.class);
return deleteResult.getDeletedCount();
}
六、更新文档
更新操作可以使用update
系列方法,比如:
/**
* 修改
*/
public long updateBook(String skuId, BigDecimal price, String img) {
// 构造查询条件
Criteria criteria = Criteria.where("skuId").is(skuId);
Query query = Query.query(criteria);
// 设置要更新的字段
Update update = Update.update("price", price).set("img", img);
// 执行更新操作
UpdateResult updateResult = mongoTemplate.updateFirst(query, update, Book.class);
return updateResult.getModifiedCount();
}
Update
用来设置更新的字段以及新值,updateFirst()
方法只会更新符合条件的第一条数据,要更新符合条件的全部数据可以使用updateMulti()
方法。
七、聚合操作
Spring Data 中对 MongoDB 的聚合操作主要是通过聚合管道
的方式实现的,上一篇已经介绍了聚合管道中常用的操作符,每个操作符在 Spring Data MongoDB 中都有对应的实现,下边通过几个例子来看看如何将这些操作符组合起来完成聚合操作。
public void aggregateBook() {
// 过滤
MatchOperation matchOperation = Aggregation.match(Criteria.where("commentCount").gte(100000));
// 分组,指定字段的值相同文档会被分为一组,也可以指定多个分组字段,多个字段共同确定一个唯一的组
// 每一组会生成一个新文档,默认会有一个 _id 字段,bookCount、avgPrice、skuIds 是组内经过统计分析生成的新字段
GroupOperation groupOperation = Aggregation.group("author")
.count().as("bookCount")
.avg("price").as("avgPrice")
.push("skuId").as("skuIds");
// 排序,按照上一个阶段输出的文档字段排序
SortOperation sortOperation = Aggregation.sort(Sort.by("bookCount").descending());
// 最大返回的数据量
LimitOperation limitOperation = Aggregation.limit(100);
// 添加时间字段
AddFieldsOperation addFieldsOperation = Aggregation.addFields().addFieldWithValue("addTime", new Date()).build();
// 将处理结果写入指定集合,只能出现在管道的最后一个阶段
OutOperation outOperation = Aggregation.out("hotBook");
// 按顺序组合每一个聚合步骤
TypedAggregation<Book> typedAggregation = Aggregation.newAggregation(Book.class,
matchOperation, groupOperation, sortOperation, limitOperation, addFieldsOperation, outOperation);
// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据
AggregationResults<Map> aggregationResults = mongoTemplate.aggregate(typedAggregation, Map.class);
// 取出最终结果
List<Map> mappedResults = aggregationResults.getMappedResults();
}
上边例子中,对book
中的文档依次进行了过滤($match
)、分组($group
)、排序(sort
)、数量限制($limit
)、添加新字段($addFields
)、输出到新集合($out
)的操作。
用到的核心类就是Aggregation
,它提供了各种操作符的静态方法,以及将每一步的操作组合起来。最终的结果可以在 NoSQLBooster 查看:
$bucket
也可以用来分组,但和$group
有些差别,简单的理解就是将文档按指定字段的不同区间值分组后,再统计分析,来看一个例子:
public void aggregateBook2() {
// 过滤
MatchOperation matchOperation = Aggregation.match(Criteria.where("commentCount").gte(200000));
// 统计不同价格区间的相关数据
// withBoundaries 用来指定每一组的区间值,每相邻的两个数据组成一个区间,包含区间的开始值、不包含结束值,比如[30, 50)、[50, 70)
// 不在 withBoundaries 所指定的所有组的文档会被分到 withDefaultBucket 指定的组中。
// andOutput 用来指定组内输出文档的字段与值 可以对组内文档字段值进行求平均、求和、计算等操作,还可以将字段值添加到集合中
// 同样每组输出一个文档,文档中默认会包含一个 _id 字段
BucketOperation bucketOperation = Aggregation.bucket("price")
.withBoundaries(30, 50, 70, 90, 110, 150, 200)
.withDefaultBucket("other")
.andOutput("price").avg().as("avgPrice")
.andOutput("skuId").count().as("count")
.andOutput("skuId").push().as("skuIds");
// 将处理结果写入指定集合,只能出现在管道的最后一个阶段
OutOperation outOperation = Aggregation.out("hotBook2");
// 按顺序组合每一个聚合步骤
TypedAggregation<Book> typedAggregation = Aggregation.newAggregation(Book.class,
matchOperation, bucketOperation, outOperation);
// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据
AggregationResults<Map> aggregationResults = mongoTemplate.aggregate(typedAggregation, Map.class);
// 取出最终结果
List<Map> mappedResults = aggregationResults.getMappedResults();
}
八、事务
事务功能也是非常有用的,目前 MongoDB 对事务的支持已经比较完善了,但是单节点下无法使用事务,需要搭建副本集或者分片集群,关于副本集搭建可参考上一篇。
在 SpringBoot 中使用 MongoDB 的事务功能还是很简单的,前边我们已经在配置文件中连接到了副本集。
接下来创建事务的配置类:
@Configuration
public class MongoTransactionConfig {
@Bean
MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory factory) {
return new MongoTransactionManager(factory);
}
}
最后在方法上添加@Transactional
注解:
/**
* 测试事务
*/
@Transactional
public void testTransaction() {
// 更新
Criteria criteria = Criteria.where("skuId").is("12482639");
Query query = Query.query(criteria);
Update update = Update.update("price", new BigDecimal("22.2"));
UpdateResult updateResult = mongoTemplate.updateFirst(query, update, Book.class);
// 删除
Criteria criteria2 = Criteria.where("skuId").is("12500708");
Query query2 = Query.query(criteria2);
DeleteResult deleteResult = mongoTemplate.remove(query2, Book.class);
// 异常
int i = 1 / 0;
}
虽然上边的方法会报错,但有事务的支持 ,则已经执行的 Mongo 操作不会提交到数据库而是回滚到原来的状态。