MongoDB 整合 Spring Boot

上一篇记录了 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的提供的关键字扩展一些新的查询方法。但是相比MongoTemplateJPA方式所提供的的功能就显得比较弱了,所以接下来我们重点讨论如何使用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);
}

三、添加文档

给集合中添加文档,可以使用MongoTemplateinsert系列方法来完成:

/**
 * 添加一条数据
 *
 * @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)等。CriteriaQuery这两个类在后边会经常用到的。

五、删除文档

要删除集合中的文档,可以使用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 操作不会提交到数据库而是回滚到原来的状态。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容