Spring JPA 从序列(SEQUENCE)中获取下一个值

1、简介

Sequence (序列)是用于生成唯一 ID 的数字生成器,可避免数据库中出现重复记录。Spring JPA 为大多数情况提供了自动处理序列的方法。不过,在某些特定情况下,我们可能需要在持久化实体之前手动检索下一个序列值。例如,在将订单(Order)详细信息保存到数据库之前,需要生成一个唯一的订单号。

本文将带你了解使用 Spring Data JPA 从数据库序列中获取下一个值的几种方法。

2、设置项目依赖

首先要在 Maven pom.xml 文件中添加 Spring Data JPAPostgreSQL 驱动依赖,并在数据库中创建序列。

2.1、Maven 依赖

首先,在 pom.xml 中添加必要的依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

2.2、测试数据

下面是我们在运行测试用例之前用来准备数据库的 SQL 脚本,可以将该脚本保存为 .sql 文件,并将其放在项目的 src/test/resources 目录中:

DROP SEQUENCE IF EXISTS my_sequence_name;
CREATE SEQUENCE my_sequence_name START 1;

该命令创建一个从 1 开始的序列,每调用一次 NEXTVAL 就递增一次。

然后,在测试类中使用 @Sql 注解,并将 executionPhase 属性设置为 BEFORE_TEST_METHOD,以便在每个测试方法执行之前将测试数据插入数据库:

@Sql(scripts = "/testsequence.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)

3、使用 @SequenceGenerator 注解

通常我们使用 JPA 中的 @SequenceGenerator 注解来配置序列生成器。该生成器可用于在实体类中自动生成主键。

它还经常与 @GeneratedValue 注解结合使用,以指定主键的生成策略。下面是使用 @SequenceGenerator 配置主键生成策略的示例:

@Entity
public class MyEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "mySeqGen")
    @SequenceGenerator(name = "mySeqGen", sequenceName = "my_sequence_name", allocationSize = 1)
    private Long id;

    // 其他实体字段和方法
}

strategyGenerationType.SEQUENCE 策略的 @GeneratedValue 注解表示将使用序列生成主键值。随后,generator 属性将此策略与名为 mySeqGen 的指定序列生成器关联起来。

@SequenceGenerator 注解配置了名为 mySeqGen 的序列生成器。它指定了数据库序列的名称 my_sequence_name 和一个可选参数 allocationSize(分配大小)。

allocationSize 是一个整数值,指定一次从数据库中预获取多少个序列号。例如,如果我们将 allocationSize 设置为 50,Persistence Provider 就会在一次调用中请求 50 个序列号,并将其存储在内部。然后,它就会使用这些预获取的序列号来生成实体 ID,减少了与数据库的交互,这非常适合并发写入高的应用。

通过这种配置,当我们持久化一个新的 MyEntity 实例时,Persistence Provider 会自动从名为 my_sequence_name 的序列中获取下一个值。检索到的序列号会分配给实体的 id 字段,然后再保存到数据库中。

下面的示例演示了如何在持久化实体后检索序列号和实体 ID:

MyEntity entity = new MyEntity();

myEntityRepository.save(entity);
long generatedId = entity.getId();  // 自动生成的序列 ID

assertNotNull(generatedId);
assertEquals(1L, generatedId);

保存实体后,可以使用实体对象上的 getId() 方法访问生成的 ID。

4、Spring Data JPA 自定义查询

在某些情况下,我们可能需要在保存到数据库之前获得下一个序列号或唯一 ID。为此可以在 Repository 中使用原生 SQL 查询来访问序列。

检索下一个序列值的具体语法取决于数据库系统。例如,在 PostgreSQLOracle 中,使用 NEXTVAL 函数从序列中获取下一个值。

下面是一个使用 @Query 注解实现的示例:

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
    @Query(value = "SELECT NEXTVAL('my_sequence_name')", nativeQuery = true)
    Long getNextSequenceValue();
}

如上,用 @QuerygetNextSequenceValue() 方法进行了注解。通过 @Query 注解,我们可以指定一个原生 SQL 查询,使用 NEXTVAL 函数从序列中检索下一个值。这样就能直接访问序列值:

@Autowired
private MyEntityRepository myEntityRepository;

long generatedId = myEntityRepository.getNextSequenceValue();

assertNotNull(generatedId);
assertEquals(1L, generatedId);

不同数据库获取序列的 SQL 不同 ,如果更换数据库,可能需要调整 SQL 查询。

5、使用 EntityManager

另外,Spring JPA 还提供了 EntityManager API,可以用于直接检索下一个序列值。这种方法提供了更精细的控制,但绕过了 JPA 的对象关系映射功能。

使用 Spring Data JPA 中的 EntityManager API 从序列中检索下一个值的示例如下:

@PersistenceContext
private EntityManager entityManager;

public Long getNextSequenceValue(String sequenceName) {
    BigInteger nextValue = (BigInteger) entityManager.createNativeQuery("SELECT NEXTVAL(:sequenceName)")
      .setParameter("sequenceName", sequenceName)
      .getSingleResult();
    return nextValue.longValue();
}

使用 createNativeQuery() 方法创建一个原生 SQL 查询。在查询中,调用 NEXTVAL 函数从序列中获取下一个值。可以看到,PostgreSQL 中的 NEXTVAL 函数返回 BigInteger 类型的值。因此,使用 longValue() 方法将 BigInteger 转换为 Long

getNextSequenceValue() 方法调用如下:

@Autowired
private MyEntityService myEntityService;

long generatedId = myEntityService.getNextSequenceValue("my_sequence_name");
assertNotNull(generatedId);
assertEquals(1L, generatedId);

6、总结

本文介绍了使用 Spring Data JPA 从数据库序列中获取下一个值的各种方法。

Spring JPA 通过 @SequenceGenerator@GeneratedValue 等注解提供了与数据库序列的无缝集成。在保存实体前需要下一个序列值的情况下,可以使用 Spring Data JPA 进行自定义的本地 SQL 查询。


Ref:https://www.baeldung.com/spring-jpa-sequence-nextval