美文网首页
MongoDB 整合 Spring Boot

MongoDB 整合 Spring Boot

作者: SheHuan | 来源:发表于2021-11-28 21:21 被阅读0次

    上一篇记录了 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 操作不会提交到数据库而是回滚到原来的状态。

    相关文章

      网友评论

          本文标题:MongoDB 整合 Spring Boot

          本文链接:https://www.haomeiwen.com/subject/ibytzltx.html