使用Azure SQL的Spring Data JDBC:保存后的标识符不得为null

问题描述

您能帮我解决以下错误吗?我正在将spring-data-jdbc与Liquibase和Azure sql数据库一起使用。我想通过 UserRepository.save

将对象保存到数据库时得到此信息

java.lang.IllegalArgumentException:保存后,标识符不能为null! 在org.springframework.util.Assert.notNull(Assert.java:201) 在org.springframework.data.jdbc.core.JdbcAggregateTemplate.store(JdbcAggregateTemplate.java:343) 在org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:149) 在org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:55)

实体:

@Table("user_details")
public class UserDetails {

    @Id
    Long id;

    @Column("email_address")
    String emailAddress;

    @Column("first_name")
    String firstName;

    @Column("last_name")
    String lastName;
}

存储库:

@Repository
public interface UserRepository extends CrudRepository<UserDetails,Long> {
}

Liquibase:

  <changeSet id="0001_user_details">
        <createSequence sequenceName="user_details_seq" startValue="1"/>

        <createTable tableName="user_details">
            <column name="id" type="BIGINT" defaultValueSequenceNext="user_details_seq">
                <constraints nullable="false"/>
            </column>

            <column name="email_address" type="VARCHAR(1024)">
                <constraints nullable="true"/>
            </column>       

            <column name="first_name" type="VARCHAR(255)">
                <constraints nullable="true"/>
            </column>

            <column name="last_name" type="VARCHAR(255)">
                <constraints nullable="true"/>
            </column>  

        </createTable>

        <addPrimaryKey tableName="user_details" columnNames="id"
                       constraintName="user_details_pk"/>

    </changeSet>

生成的架构:

create sequence user_details_seq
go

create table user_details
(
    id               bigint
        constraint DF_user_details_id default NEXT VALUE FOR [user_details_seq] not null
        constraint user_details_pk
            primary key,email_address    varchar(1024),first_name       varchar(255),last_name        varchar(255),)
go

行家:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>8.4.1.jre14</version>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
            <version>3.8.9</version>
        </dependency>

解决方法

如果我理解您的Liquibase配置正确,它将设置一个序列并将id的默认值设置为该序列的next值。

这在原则上应该可行,因为Spring Data JDBC在插入中不包含null id,并假定它将由数据库生成并由JDBC驱动程序返回。

最后一部分可能是哪里出了问题。 异常基本上说,在将数据插入数据库中并使用返回的JDBC驱动程序更新ID后,该ID仍为null(如果使用原始数字类型,则为0)。 因此,驱动程序可能不会返回该序列生成的ID。

为了解决该问题,您可以使用以下选项:

  1. 不使用序列,而是在客户端(例如,使用UUID)创建ID。这样做的好处是易于实施,可以很好地扩展。如果您是在实体实例化时创建ID的,则还可以在持久保存实体之前引用它。

  2. 使用单独的查询从序列中选择值,并使用其设置ID。这可能最好在BeforeSaveCallback中完成,但是也可以在创建实体的工厂方法中发生。这涉及更多,并且需要额外的数据库往返。可以通过实施一种算法来缓解这种情况,该算法是从序列中选择值乘以50,然后从本地生成50个ID。基本上是Hibernate的HiLo算法。

  3. 教Spring Data JDBC如何从数据库获取ID。这可能采用多种形式,并且取决于我所没有的Azure知识。如果Azure有一个SERIALAUTOINCREMENT字段,则可以使用它代替序列,并且应该由JDBC驱动程序返回。

    也许在您使用的IdGeneration中使用其他Dialect时有效。也许可以实施新的IdGeneration。显然,这很复杂,并且需要Spring Data JDBC的全新发行版(或修补版本)。