Spring Boot MongoDB 入门

news2024/9/21 14:50:26
  • 1. 概述

  • 2. 快速入门

  • 3. 基于方法名查询

  • 4. 基于 Example 查询

  • 5. MongoTemplate

  • 6. 自增主键

  • 666. 彩蛋


1. 概述

可能有一些胖友对 MongoDB 不是很了解,这里我们引用一段介绍:

FROM 《分布式文档存储数据库 MongoDB》

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

他支持的数据结构非常松散,是类似 json 的 bjson 格式,因此可以存储比较复杂的数据类型。

Mongo 最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

MongoDB 中的许多概念在 MySQL 中具有相近的类比。本表概述了每个系统中的一些常见概念。

对于不熟悉的胖友,可以先看下该表,然后开始本文的旅程。

MySQLMongoDB
库 Database库 Database
表 Table集合 Collection
行 Row文档 Document
列 Column字段 Field
joins嵌入文档或者链接

在早期,在项目中 MongoDB 的 ORM 框架使用 Morphia 较多。随着 Spring Data MongoDB 的日趋完善,更为主流。目前,艿艿手头所有的项目,都从 Morphia 该用 Spring Data MongoDB 。

在 Spring Data MongoDB 中,有两种方式进行 MongoDB 操作:

  • Spring Data Repository 方式

  • MongoTemplate

艿艿:如果胖友还没安装 MongoDB ,可以参考下 《芋道 MongoDB 安装部署》 文章,先进行下安装。

2. 快速入门

示例代码对应仓库:lab-16-spring-data-mongodb 。

  • MongoDB 版本号:4.2.1

本小节,我们会使用 spring-boot-starter-data-mongodb 自动化配置 Spring Data MongoDB 主要配置。同时,使用 Spring Data Repository 实现的 MongoDB 的 CRUD 操作。

2.1 引入依赖

在 pom.xml 文件中,引入相关依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/><!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-16-spring-data-mongodb</artifactId>

    <dependencies>
        <!-- 自动化配置 Spring Data Mongodb -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <!-- 方便等会写单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

2.2 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication(exclude = {ElasticsearchAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class})
publicclass Application {
}

2.3 MongoDBConfig

在 cn.iocoder.springboot.lab16.springdatamongodb.config 包路径下,创建 MongoDBConfig 配置类。代码如下:

// MongoDBConfig.java

@Configuration
publicclass MongoDBConfig {

    @Bean// 目的,就是为了移除 _class field 。参考博客 https://blog.csdn.net/bigtree_3721/article/details/82787411
    public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory,
                                                       MongoMappingContext context,
                                                       BeanFactory beanFactory) {
        // 创建 DbRefResolver 对象
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        // 创建 MappingMongoConverter 对象
        MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
        // 设置 conversions 属性
        try {
            mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
        } catch (NoSuchBeanDefinitionException ignore) {
        }
        // 设置 typeMapper 属性,从而移除 _class field 。
        mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return mappingConverter;
    }

}
  • 通过在自定义 MappingMongoConverter Bean 对象,避免实体保存到 MongoDB 中时,会多一个 _class 字段,存储实体的全类名。

2.4 配置文件

在 application.yml 中,添加 MongoDB 配置,如下:

spring:
  data:
    # MongoDB 配置项,对应 MongoProperties 类
    mongodb:
      host:127.0.0.1
      port:27017
      database:yourdatabase
      username:test01
      password:password01
      # 上述属性,也可以只配置 uri

logging:
  level:
    org:
      springframework:
        data:
          mongodb:
            core:DEBUG# 打印 mongodb 操作的具体语句。生产环境下,不建议开启。

2.5 UserDO

在 cn.iocoder.springboot.lab16.springdatamongodb.dataobject 包路径下,创建 UserDO 类。代码如下:

@Document(collection = "User")
publicclass UserDO {

    /**
     * 用户信息
     */
    publicstaticclass Profile {

        /**
         * 昵称
         */
        private String nickname;
        /**
         * 性别
         */
        private Integer gender;

        // ... 省略 setting/getting 方法

    }

    @Id
    private Integer id;
    /**
     * 账号
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 用户信息
     */
    private Profile profile;

    // ... 省略 setting/getting 方法

}
  • 在 UserDO 类中,我们内嵌了一个 profile 属性,它是 Profile 类。这里仅仅作为示例,实际场景下,还是建议把 User 和 Profile 拆分开。

  • 推荐阅读 《你应该知道的 MongoDB 最佳实践》 文章。对于初用 MongoDB 的开发者,往往错误的使用内嵌属性,需要去理解一下。

2.6 UserRepository

在 cn.iocoder.springboot.lab16.springdatamongodb.repository 包路径下,创建 UserRepository 接口。代码如下:

// ProductRepository.java

publicinterface UserRepository extends MongoRepository<UserDO, Integer> {
}
  • 继承 org.springframework.data.mongodb.repository.MongoRepository 接口,第一个泛型设置对应的实体是 UserDO ,第二个泛型设置对应的主键类型是 Integer 。

  • 因为实现了 MongoRepository 接口,Spring Data MongoDB 会自动生成对应的 CRUD 等等的代码。😈 是不是很方便。

  • MongoRepository 类图如下:

    • 每个接口定义的方法,胖友可以点击下面每个链接,自己瞅瞅,简单~

    • org.springframework.data.repository.CrudRepository

    • org.springframework.data.repository.PagingAndSortingRepository

    • org.springframework.data.repository.query.QueryByExampleExecutor :定义基于 org.springframework.data.domain.Example.Example 的查询操作。在 「4. 基于 Example 查询」 中,我们再来详细介绍。

    • org.springframework.data.mongodb.repository.MongoRepository :主要重写 PagingAndSortingRepository 和 CrudRepository 接口,将 findXXX 方法返回的结果从 Iterable 放大成 List ,同时增加 insert 插入方法。

艿艿:如果胖友看过艿艿写的 《芋道 Spring Boot JPA 入门》 文章,会发现和 Spring Data JPA 的使用方式,基本一致。这就是 Spring Data 带给我们的好处,使用相同的 API ,统一访问不同的数据源。o( ̄▽ ̄)d 点赞。

2.7 简单测试

创建 UserRepositoryTest 测试类,我们来测试一下简单的 UserRepositoryTest 的每个操作。代码如下:

// UserRepositoryTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
publicclass UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test// 插入一条记录
    public void testInsert() {
        // 创建 UserDO 对象
        UserDO user = new UserDO();
        user.setId(1); // 这里先临时写死一个 ID 编号,后面演示自增 ID 的时候,在修改这块
        user.setUsername("yudaoyuanma");
        user.setPassword("buzhidao");
        user.setCreateTime(new Date());
        // 创建 Profile 对象
        UserDO.Profile profile = new UserDO.Profile();
        profile.setNickname("芋道源码");
        profile.setGender(1);
        user.setProfile(profile);
        // 存储到 DB
        userRepository.insert(user);
    }

    // 这里要注意,如果使用 save 方法来更新的话,必须是全量字段,否则其它字段会被覆盖。
    // 所以,这里仅仅是作为一个示例。
    @Test// 更新一条记录
    public void testUpdate() {
        // 查询用户
        Optional<UserDO> userResult = userRepository.findById(1);
        Assert.isTrue(userResult.isPresent(), "用户一定要存在");
        // 更新
        UserDO updateUser = userResult.get();
        updateUser.setUsername("yutou");
        userRepository.save(updateUser);
    }

    @Test// 根据 ID 编号,删除一条记录
    public void testDelete() {
        userRepository.deleteById(1);
    }

    @Test// 根据 ID 编号,查询一条记录
    public void testSelectById() {
        Optional<UserDO> userDO = userRepository.findById(1);
        System.out.println(userDO.isPresent());
    }

    @Test// 根据 ID 编号数组,查询多条记录
    public void testSelectByIds() {
        Iterable<UserDO> users = userRepository.findAllById(Arrays.asList(1, 4));
        users.forEach(System.out::println);
    }

}
  • 每个测试单元方法,胖友自己看看方法上的注释。

具体的,胖友可以自己跑跑,妥妥的。

3. 基于方法名查询

示例代码对应仓库:lab-16-spring-data-mongodb 。

在 《芋道 Spring Boot JPA 入门》 文章的「4. 基于方法名查询」小节中,我们已经提到:

在 Spring Data 中,支持根据方法名作生成对应的查询(WHERE)条件,进一步进化我们使用 JPA ,具体是方法名以 findByexistsBycountBydeleteBy 开头,后面跟具体的条件。具体的规则,在 《Spring Data JPA —— Query Creation》 文档中,已经详细提供。如下:

关键字方法示例JPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
IsEqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullNullfindByAge(Is)Null… where x.age is null
IsNotNullNotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)
  • 注意,如果我们有排序需求,可以使用 OrderBy 关键字。

下面,我们来编写一个简单的示例。

艿艿:IDEA 牛逼,提供的插件已经能够自动提示上述关键字。太强了~

3.1 UserRepository02

在 cn.iocoder.springboot.lab16.springdatamongodb.repository 包路径下,创建 UserRepository02 接口。代码如下:

// UserRepository02.java

publicinterface UserRepository02 extends MongoRepository<UserDO, Integer> {

    UserDO findByUsername(String username);

    Page<UserDO> findByUsernameLike(String username, Pageable pageable);

}
  • 对于分页操作,需要使用到 Pageable 参数,需要作为方法的最后一个参数。

  • 注意噢,基于方法名查询,不支持内嵌对象的属性。(⊙o⊙)… 至少在网上翻了一波资料,没有提及这块。

3.2 简单测试

创建 UserRepository02Test 测试类,我们来测试一下简单的 UserRepository02Test 的每个操作。代码如下:

// UserRepository02Test.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
publicclass UserRepository02Test {

    @Autowired
    private UserRepository02 userRepository;

    @Test// 根据名字获得一条记录
    public void testFindByName() {
        UserDO user = userRepository.findByUsername("yutou");
        System.out.println(user);
    }

    @Test// 使用 username 模糊查询,分页返回结果
    public void testFindByNameLike() {
        // 创建排序条件
        Sort sort = new Sort(Sort.Direction.DESC, "id"); // ID 倒序
        // 创建分页条件。
        Pageable pageable = PageRequest.of(0, 10, sort);
        // 执行分页操作
        Page<UserDO> page = userRepository.findByUsernameLike("yu", pageable);
        // 打印
        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
    }

}
  • 每个测试单元方法,胖友自己看看方法上的注释。

具体的,胖友可以自己跑跑,妥妥的。

4. 基于 Example 查询

示例代码对应仓库:lab-16-spring-data-mongodb 。

实际场景下,我们并不会基于 Example 查询。所以本小节,胖友可以选择性看看即可。

对于大多数胖友,可能不了解 Spring Data Example 。我们先来一起看看官方文档的介绍:

Query by Example (QBE) is a user-friendly querying technique with a simple interface. 使用 Example 进行查询,是一种友好的查询方式,可以使用便捷的 API 方法。

It allows dynamic query creation and does not require to write queries containing field names. 它允许创建动态查询,而无需编写包含字段名的查询。

In fact, Query by Example does not require to write queries using store-specific query languages at all. 事实上,在使用 Example 进行查询的时候,我们无需使用特定的存储器(数据库)的查询语言。

  • 请原谅艿艿蹩脚的翻译。简单来说,我们可以通过 Example 进行编写动态的查询条件,而无需使用每个不同的 Spring Data 实现类的 Query 对象。例如说:

    • Spring Data JPA 提供的 javax.persistence.criteria.Predicate

    • Spring Data MongoDB 提供的 org.springframework.data.mongodb.core.query.Query 。

  • 相当于说,不同 Spring Data 实现框架,会将 Spring Data Example 条件,翻译成对应的查询对象。例如说:

    • Spring Data JPA 将 Example 转换成 Predicate 。

    • Spring Data MongoDB 将 Example 转换成 Query 。

  • 当然,并不是所有 Spring Data 实现都支持 Spring Data Example 。例如说,Spring Data Elasticsearch 并不支持。一般情况下,支持 Spring Data Example 的 Spring Data 实现框架,它们的 Repository 都继承了 org.springframework.data.repository.query.QueryByExampleExecutor 接口。例如说:

    • Spring Data JPA 的 JpaRepository 接口。

    • Spring Data MongoDB 的 MongoRepository 接口。

Example API 一共包含三部分:

  • Probe :含有对应字段的实体对象。通过设置该实体对象的字段,作为查询字段。

    注意,Probe 并不是一个类,而是实体对象的泛指。

  • ExampleMatcher :ExampleMatcher 可以定义特定字段的匹配模式。例如说,全模糊匹配、前缀模糊匹配等等。

    简单来说,通过实体对象的字段作为查询条件,只能满足相等的情况,对于 != 、LIKE 等等情况,需要通过 ExampleMatcher 特殊指定。如果不理解,没事,看了示例会更容易明白。

  • Example :是 Probe 和 ExampleMatcher 的组合,构成查询对象。

当然,Example 有它的限制条件:

No support for nested/grouped property constraints like firstname = ?0 or (firstname = ?1 and lastname = ?2) 不支持嵌套或分组约束。例如说,firstname = ?0 or (firstname = ?1 and lastname = ?2) 。

Only supports starts/contains/ends/regex matching for strings and exact matching for other property types 对于字符串,只支持 tarts/contains/ends/regex 匹配方式;对于其它类型,只支持精确匹配。

4.1 UserRepository03

在 cn.iocoder.springboot.lab16.springdatamongodb.repository 包路径下,创建 UserRepository02 接口。代码如下:

// UserRepository03.java

publicinterface UserRepository03 extends MongoRepository<UserDO, Integer> {

    // 使用 username 精准匹配
    default UserDO findByUsername01(String username) {
        // 创建 Example 对象,使用 username 查询
        UserDO probe = new UserDO();
        probe.setUsername(username); // 精准匹配 username 查询
        Example<UserDO> example = Example.of(probe);
        // 执行查询
        return findOne(example)
                .orElse(null); // 如果为空,则返回 null
    }

    // 使用 username 模糊匹配
    default UserDO findByUsernameLike01(String username) {
        // 创建 Example 对象,使用 username 查询
        UserDO probe = new UserDO();
        probe.setUsername(username); // 这里还需要设置
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withMatcher("username", ExampleMatcher.GenericPropertyMatchers.contains()); // 模糊匹配 username 查询
        Example<UserDO> example = Example.of(probe, matcher);
        // 执行查询
        return findOne(example)
                .orElse(null); // 如果为空,则返回 null
    }

}

4.2 简单测试

创建 UserRepository03Test 测试类,我们来测试一下简单的 UserRepository03Test 的每个操作。代码如下:

// UserRepository03Test.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
publicclass UserRepository03Test {

    @Autowired
    private UserRepository03 userRepository;

    @Test
    public void testFindByUsername01() {
        UserDO user = userRepository.findByUsername01("yutou");
        System.out.println(user);
    }

    @Test
    public void testFindByUsernameLike01() {
        UserDO user = userRepository.findByUsernameLike01("yu");
        System.out.println(user);
    }

}
  • 每个测试单元方法,胖友自己看看方法上的注释。

具体的,胖友可以自己跑跑,妥妥的。更多示例,可以看看如下文章:

  • 《Spring Data JPA Query by Example》

  • 《Spring Data JPA 使用 Example 快速实现动态查询》

5. MongoTemplate

示例代码对应仓库:lab-16-spring-data-mongodb 。

在 Spring Data MongoDB 中,有一个 MongoTemplate 类,提供了 MongoDB 操作模板,方便我们操作 MongoDB 。

5.1 UserDao

在 cn.iocoder.springboot.lab16.springdatamongodb.dao 包路径下,创建 UserDao 类。代码如下:

// UserDao.java

@Repository
publicclass UserDao {

    @Autowired
    private MongoTemplate mongoTemplate;

    public void insert(UserDO entity) {
        mongoTemplate.insert(entity);
    }

    public void updateById(UserDO entity) {
        // 生成 Update 条件
        final Update update = new Update();
        // 反射遍历 entity 对象,将非空字段设置到 Update 中
        ReflectionUtils.doWithFields(entity.getClass(), new ReflectionUtils.FieldCallback() {

            @Override
            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                // 排除指定条件
                if ("id".equals(field.getName()) // 排除 id 字段,因为作为查询主键
                        || field.getAnnotation(Transient.class) != null// 排除 @Transient 注解的字段,因为非存储字段
                        || Modifier.isStatic(field.getModifiers())) { // 排除静态字段
                    return;
                }
                // 设置字段可反射
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                // 排除字段为空的情况
                if (field.get(entity) == null) {
                    return;
                }
                // 设置更新条件
                update.set(field.getName(), field.get(entity));
            }

        });
        // 防御,避免有业务传递空的 Update 对象
        if (update.getUpdateObject().isEmpty()) {
            return;
        }
        // 执行更新
        mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(entity.getId())), update, UserDO.class);
    }

    public void deleteById(Integer id) {
        mongoTemplate.remove(new Query(Criteria.where("_id").is(id)), UserDO.class);
    }

    public UserDO findById(Integer id) {
        return mongoTemplate.findOne(new Query(Criteria.where("_id").is(id)), UserDO.class);
    }

    public UserDO findByUsername(String username) {
        return mongoTemplate.findOne(new Query(Criteria.where("username").is(username)), UserDO.class);
    }

    public List<UserDO> findAllById(List<Integer> ids) {
        return mongoTemplate.find(new Query(Criteria.where("_id").in(ids)), UserDO.class);
    }

}
  • 使用 MongoTemplate 实现了 CRUD 的功能。其它 findAndModify/findAndRemove/count/upsert 等操作,胖友可以自己尝试下。

  • 对于 #updateById(UserDO entity) 方法,胖友可以自己抽象成一个通用的 #updateEntityFieldsById(Object id, Object entity) 方法,封装在一个 BaseMongoDao 抽象类中。

    友情提示:此处暂时有个问题,对于 UserDO 内嵌的 profile 对象,一旦设置了值,是整个 Profile 对象覆盖更新。所以,使用时需要注意下。

    目前艿艿自己项目里,大多数内嵌对象,全量更新不存在问题。如果存在问题的,提供了另外的方法解决。

    当然,也可以进一步封装 #updateEntityFieldsById(Object id, Object entity, String... fields) 方法,只更新指定字段。

5.2 简单测试

创建 UserDaoTest 测试类,我们来测试一下简单的 UserDaoTest 的每个操作。代码如下:

// UserDaoTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
publicclass UserDaoTest {

    @Autowired
    private UserDao userDao;

    @Test// 插入一条记录
    public void testInsert() {
        // 创建 UserDO 对象
        UserDO user = new UserDO();
        user.setId(1); // 这里先临时写死一个 ID 编号,后面演示自增 ID 的时候,在修改这块
        user.setUsername("yudaoyuanma");
        user.setPassword("buzhidao");
        user.setCreateTime(new Date());
        // 创建 Profile 对象
        UserDO.Profile profile = new UserDO.Profile();
        profile.setNickname("芋道源码");
        profile.setGender(1);
        user.setProfile(profile);
        // 存储到 DB
        userDao.insert(user);
    }

    // 这里要注意,如果使用 save 方法来更新的话,必须是全量字段,否则其它字段会被覆盖。
    // 所以,这里仅仅是作为一个示例。
    @Test// 更新一条记录
    public void testUpdate() {
        // 创建 UserDO 对象
        UserDO updateUser = new UserDO();
        updateUser.setId(1);
        updateUser.setUsername("nicai");

        // 执行更新
        userDao.updateById(updateUser);
    }

    @Test// 根据 ID 编号,删除一条记录
    public void testDelete() {
        userDao.deleteById(1);
    }

    @Test// 根据 ID 编号,查询一条记录
    public void testSelectById() {
        UserDO userDO = userDao.findById(1);
        System.out.println(userDO);
    }

    @Test// 根据 ID 编号数组,查询多条记录
    public void testSelectByIds() {
        List<UserDO> users = userDao.findAllById(Arrays.asList(1, 4));
        users.forEach(System.out::println);
    }

}
  • 每个测试单元方法,胖友自己看看方法上的注释。

具体的,胖友可以自己跑跑,妥妥的。

6. 自增主键

示例代码对应仓库:lab-16-spring-data-mongodb 。

MongoDB 自带的主键选择是 ObjectId 类型,需要占用 12 字节。相比 Int 4 字节或者 Long 8 字节占用更多的内存空间。而绝大多数业务场景下,Int 或 Long 足够使用,所以我们更加偏向使用 Int 或 Long 作为自增 ID 主键。

当然,我们在日志记录上,我们还是采用 ObjectId 为主。

我们在实现 MongoDB 自增主键时,会创建一个名为 "sequence" 的集合。字段如下:

"sequence" 集合名,也可以取其它名字,看自己喜好。

  • _id :String 类型,集合的实体类名。

  • value :Long 类型,存储每个集合的自增序列。

在程序中,每次插入实体对象到 MongoDB 之前,通过 $inc 操作,从 "sequence" 自增获得最新的 ID ,然后将该 ID 赋值给实体对象,最终在插入到 MongoDB 之中。

当然,考虑到并不是所有实体都需要使用自增 ID ,所以我们要有方式去标记:

  • 方式一:创建自定义 @AutoIncKey 注解,添加到 ID 属性上。

  • 方式二:创建 IncIdEntity 抽象基类,这样需要自增的实体继承它。

对于方式一,网上博客比较多,胖友可以看看 《Java 中实现 MongoDB 主键自增》 文章。

对于方式二,目前艿艿项目采用这种方式,主要是我们自己的 BaseMongoDao 提供的很多方法,是基于 IncIdEntity 抽象类的。例如说,#updateEntityFieldsById(final IncIdEntity entity, String... fields) 方法,这样可以方便的直接获取到 ID 属性。

当然,无论方式一还是方式二,实现原理是一致的,差异仅仅在于获取 ID 属性不同。下面,本小节我们就来实现下方式二。

6.1 IncIdEntity

在 cn.iocoder.springboot.lab16.springdatamongodb.mongo 包路径下,创建 IncIdEntity 类。代码如下:

// IncIdEntity.java

/**
 * 自增主键实体
 *
 * @param <T> 主键泛型
 */
publicabstractclass IncIdEntity<T extends Number> {

    @Id
    private T id;

    public T getId() {
        return id;
    }

    public void setId(T id) {
        this.id = id;
    }

}

6.2 ProductDO

在 cn.iocoder.springboot.lab16.springdatamongodb.dataobject 包路径下,创建 ProductDO 类。代码如下:

// ProductDO.java

@Document(collection = "Product")
publicclass ProductDO extends IncIdEntity<Integer> {

    /**
     * 商品名
     */
    private String name;

    public String getName() {
        return name;
    }

    public ProductDO setName(String name) {
        this.name = name;
        returnthis;
    }

}
  • 继承 IncIdEntity 抽象类,使用 Integer 自增主键。

6.3 MongoInsertEventListener

在 cn.iocoder.springboot.lab16.springdatamongodb.dataobject 包路径下,创建 MongoInsertEventListener 类。代码如下:

// MongoInsertEventListener.java

@Component
publicclass MongoInsertEventListener extends AbstractMongoEventListener<IncIdEntity> {

    /**
     * sequence - 集合名
     */
    privatestaticfinal String SEQUENCE_COLLECTION_NAME = "sequence";
    /**
     * sequence - 自增值的字段名
     */
    privatestaticfinal String SEQUENCE_FIELD_VALUE = "value";

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public void onBeforeConvert(BeforeConvertEvent<IncIdEntity> event) {
        IncIdEntity entity = event.getSource();
        // 判断 id 为空
        if (entity.getId() == null) {
            // 获得下一个编号
            Number id = this.getNextId(entity);
            // 设置到实体中
            // noinspection unchecked
            entity.setId(id);
        }
    }

    /**
     * 获得实体的下一个主键 ID 编号
     *
     * @param entity 实体对象
     * @return ID 编号
     */
    private Number getNextId(IncIdEntity entity) {
        // 使用实体名的简单类名,作为 ID 编号
        String id = entity.getClass().getSimpleName();
        // 创建 Query 对象
        Query query = new Query(Criteria.where("_id").is(id));
        // 创建 Update 对象
        Update update = new Update();
        update.inc(SEQUENCE_FIELD_VALUE, 1); // 自增值
        // 创建 FindAndModifyOptions 对象
        FindAndModifyOptions options = new FindAndModifyOptions();
        options.upsert(true); // 如果不存在时,则进行插入
        options.returnNew(true); // 返回新值
        // 执行操作
        @SuppressWarnings("unchecked")
        HashMap<String, Object> result = mongoTemplate.findAndModify(query, update, options,
                HashMap.class, SEQUENCE_COLLECTION_NAME);
        // 返回主键
        return (Number) result.get(SEQUENCE_FIELD_VALUE);
    }

}
  • MongoDB 实体对象在插入之前,会发布 BeforeConvertEvent 事件。所以,我们可以通过创建 MongoInsertEventListener 监听器,监听该事件,生成自增主键 ID 主键,设置到实体对象中。

  • 如果胖友想要使用集合名作为 "sequence" 集合的 "id" ,可以使用 BeforeConvertEvent.collectionName 属性。

6.4 ProductRepository

在 cn.iocoder.springboot.lab16.springdatamongodb.repository 包路径下,创建 ProductRepository 接口。代码如下:

// ProductRepository.java

publicinterface ProductRepository extends MongoRepository<ProductDO, Integer> {
}

6.5 简单测试

创建 ProductRepositoryTest 测试类,我们来测试一下简单的 ProductRepositoryTest 的每个操作。代码如下:

// ProductRepositoryTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
publicclass ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testInsert() {
        // 创建 ProductDO 对象
        ProductDO product = new ProductDO();
        product.setName("芋头");
        // 插入
        productRepository.insert(product);
        // 打印 ID
        System.out.println(product.getId());
    }

}

项目中,只采用 MongoTemplate 。主要是我们封装了 BaseMongoDao ,提供了一些我们日常开发需要的功能,例如说:

  • #insertToMongo(Object objectToSave) 方法:插入实体,使用自增 ID 主键。

  • #updateEntityFieldsById(final IncIdEntity entity, String... fields) 方法:只更新实体指定字段。

推荐阅读:

  • 《性能测试 —— MongoDB 基准测试》

     

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/134810.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《计算机视觉》:角点检测与图像匹配

文章目录 任务一:基本处理-Harris角点检测原理代码结果与分析任务二:SIFT算法原理代码结果与分析任务一:基本处理-Harris角点检测 数据:棋盘图片 要求:自己写函数实现Harris角点检测子,设置不同参数,比较检测结果 边缘检测子:sobel检测子 响应函数参数alpha:0.05 参数…

【JavaScript】BOM 概念及相关操作

文章目录【JavaScript】BOM 概念及相关操作一. BOM概念BOM可以操作的内容二.window内置对象和属性(1) 获取浏览器窗口的尺寸(2) 获取文档窗口的尺寸(3) 浏览器的常见事件(4) 浏览器的历史记录(5) 浏览器的标签页(6) 浏览器卷去的尺寸(7) 浏览器滚动到的位置浏览器滚动到的位置案…

ARM 按键轮询编程实战

一、什么是按键 1、按键的物理特性 平时没人按的时候&#xff0c;弹簧把按键按钮弹开。此时内部断开的。有人按下的时候&#xff0c;手的力量克服弹簧的弹力&#xff0c;将按钮按下&#xff0c;此时内部保持接通&#xff08;闭合&#xff09;状态&#xff1b;如果手拿开&…

【应急响应】 - Windows 排查分析

Windows 分析排查1. 文件分析1.1 开机启动文件1.2 temp 临时异常文件1.3 浏览器信息分析1.4 文件时间属性分析1.5 最近打开文件分析2. 进程分析2.1 可疑进程发现与关闭3. 系统信息3.1 windows 计划任务3.2 隐藏账户与发现3.2.1 隐藏账号的建立3.2.2 隐藏账号的删除3.3 补丁查看…

Java开发的党员管理系统党员会议系统党务管理系统

简介 Java开发的大学生党员管理系统&#xff0c;主要功能会议&#xff0c;会议记录&#xff0c;会议主持&#xff0c;设置参会人员&#xff0c;请假申请&#xff0c;会议内容附件上传下载&#xff0c;党费管理&#xff0c;入党积极分子预备党员管理&#xff0c;人员变动&#…

hcip实验

1.搭建拓扑 2.配置IP R14&#xff1a; [r14]ip route-static 0.0.0.0 0 145.1.1.2 [r14]acl 2000 [r14-acl-basic-2000]rule permit source any [r14]int GigabitEthernet 0/0/1 [r14-GigabitEthernet0/0/1]nat outbound 2000 [r14]int Tunnel 0/0/0 [r14-Tunnel0/0/0…

【2 - 随机森林 - 原理部分】菜菜sklearn机器学习

课程地址&#xff1a;《菜菜的机器学习sklearn课堂》_哔哩哔哩_bilibili 第一期&#xff1a;sklearn入门 & 决策树在sklearn中的实现第二期&#xff1a;随机森林在sklearn中的实现第三期&#xff1a;sklearn中的数据预处理和特征工程第四期&#xff1a;sklearn中的降维算法…

DDR3 数据传输 (四)

目录 引言 AXI从侧接口参数 AXI从侧接口信号 参考说明 引言 前文链接

【数学思维】数理经济中一些基本概念

【数学思维】数理经济中一些基本概念开集 open set 与闭集 closed set紧集 compact set集合有界 bounded set度量空间 metric space欧式空间 euclidean space闭包 closure上包络 upper envelope、下包络 lower envelope上极限 limit superior、下极限 limit inferior左连续、右…

RabbitMQ第五个实操小案例——主题交换机(TopicExchange)

文章目录RabbitMQ第五个实操小案例——主题交换机&#xff08;TopicExchange&#xff09;RabbitMQ第五个实操小案例——主题交换机&#xff08;TopicExchange&#xff09; TopicExchange 和 DirectExchange 这两种交换机非常相似&#xff0c;Topic类型的Exchange与Direct相比&…

JavaScript 面向对象的编程 (Code with mosh学习笔记)

JavaScript OOP Getting Start - 1- What is OOP 面向对象的编程是一种编程范例围绕对象而不是函数一些OOP语言 C#JavaRubyPythonJavaScript Getting Start - 2- Four Pillars of OOP OOP的4个概念&#xff1a; 封装 使用封装重新组合相关的变量和函数减少复杂性增加代码…

jrtt 某头条网页版 _signature参数逆向

本文仅供参考学习&#xff0c;如有侵权可联系本人 目标网站 aHR0cHM6Ly93d3cudG91dGlhby5jb20vYy91c2VyL3Rva2VuL01TNHdMakFCQUFBQWE0alpUdzhvRlZnaUJIREprMTA1NDdBVFBUb050aHlsVDRqWndZMmlrMXcvPw接口分析 token&#xff1a;需要采集用户 _signature&#xff1a;加密参数 ai…

【每日一leetcode】Day2 链表(简单)

一、剑指 Offer 06. 从尾到头打印链表 输入一个链表的头节点&#xff0c;从尾到头反过来返回每个节点的值&#xff08;用数组返回&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,3,2] 输出&#xff1a;[2,3,1] 限制&#xff1a; 0 < 链表长度 < 10000…

SCI论文解读复现【NO.3】MSFT-YOLO:基于变压器的改进YOLOv5钢表面缺陷检测(代码已复现)

此前出了目标检测算法改进专栏&#xff0c;但是对于应用于什么场景&#xff0c;需要什么改进方法对应与自己的应用场景有效果&#xff0c;并且多少改进点能发什么水平的文章&#xff0c;为解决大家的困惑&#xff0c;此系列文章旨在给大家解读发表高水平学术期刊中的SCI论文&am…

HCIP第三天ospf星型和全连实验

HCIP文章目录 文章目录HCIP文章目录ospf实验实验要求拓扑图R1/4/5为全连的MGRE结构所有私有网段可以互相通讯ospf实验 实验要求 1、R6为ISP只能配置ip地址&#xff0c;R1-5的环回为私有网段 2、R1/4/5为全连的MGRE结构&#xff0c;R1/2/3为星型的拓扑结构&#xff0c;R1为中心…

stm32平衡小车(1)---蓝牙模块及其bug处理

基于stm32c8t6开发板 一&#xff0c;蓝牙模块HC-05 1.外观 2.接线方式 TX----->PB10 RX----->PB11 VCC----->3.3V GND---->GND 3.AT模式 不用烧录代码&#xff0c;直接将c8t6和HC-05相连接&#xff0c;通过XCOM或者SSCOM软件便可以进入调试模式&a…

MySQL常见深入优化

一、分页查询优化 1. SQL语句准备 CREATE TABLE employees (id INT ( 11 ) NOT NULL AUTO_INCREMENT,name VARCHAR ( 24 ) NOT NULL DEFAULT COMMENT 姓名,age INT ( 11 ) NOT NULL DEFAULT 0 COMMENT 年龄,position VARCHAR ( 20 ) NOT NULL DEFAULT COMMENT 职位,hire_ti…

(Django+redis双机配置)ubuntu虚拟机配置redis,window中django访问

目录 Ubuntu虚拟机配置redis 进入root用户 配置redis服务 开启端口 1.设置密码 2.关闭只允许本机访问 3.关闭保护模式 双向ping测试 ubuntu开启SSH服务 Django中 Django中settings配置redis Ubuntu虚拟机配置redis 进入root用户 首先要进入root用户 后续一定保证要…

Spring Boot内存泄露,排查

背景 为了更好地实现对项目的管理&#xff0c;我们将组内一个项目迁移到MDP框架&#xff08;基于Spring Boot&#xff09;&#xff0c;随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因&#xff0c;发现配置了4G堆内内存&#xff0c;但是实际使…

利用kafka发送系统

kafka是一种消息队列框架。 如果不用消息队列框架&#xff0c;就需要用阻塞队列来实现发送系统消息和系统通知 1.阻塞队列 阻塞队列是一种用来解决进程间通信的方式 阻塞队列依靠自带的两个方法put(往队列里面存数据)和take(从队列里面取数据) 2.Kafka kafka最早只是用来发…