OneToMany 关系不能在春天用 liquibase 保存

Posted

技术标签:

【中文标题】OneToMany 关系不能在春天用 liquibase 保存【英文标题】:OneToMany relation cannot be saved in spring with liquibase 【发布时间】:2022-01-19 19:07:03 【问题描述】:

我正在尝试在 spring 中构建一个数据模型,该模型通过一对多关系向下级联多达 3 个级别,但我无法让它与 liquibase 脚本一起使用。

我正在使用带有 Kotlin 的 Spring Boot 和带有 PostgreSQL 数据库的 liquibase。

到目前为止我做了什么:

将代码缩减为仅包含不起作用的部分(见下文) 我尝试了 @OneToMany 与 @JoinTable 以及 @JoinColumn,我还尝试了与 @ManyToMany 相同以排除 @OneToMany 的问题 我在没有 liquibase 的情况下运行相同的代码(如下),让 Hibernate/JPA 从模型创建表 这确实有效,因此我从这些表中生成了 liquibase 脚本,但它们看起来与我自己的完全相同(键名除外) 使用这些模型检索数据有效(如果我直接通过 SQL 插入数据)

老实说,我不确定问题出在模型、配置还是 liquibase 脚本中,所以我将发布所有这些。我缺少配置吗?我是否正确配置了级联?我的模型定义/liquibase 脚本有错吗?

我在保存父母时遇到的例外是:

Hibernate: insert into parent (name) values (?)
2021-12-15 23:29:16.797  WARN 14115 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 23502
2021-12-15 23:29:16.798 ERROR 14115 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: null value in column "id" of relation "parent" violates not-null constraint
  Detail: Failing row contains (null, Test 1).
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [id" of relation "parent]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
...
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
...
Caused by: org.postgresql.util.PSQLException: ERROR: null value in column "id" of relation "parent" violates not-null constraint
  Detail: Failing row contains (null, Test 2).

我尝试运行的代码:

val parent = Parent(
    id = 0,
    name = "Test 2"
).apply 
    children = mutableSetOf(
        Child(
            id = 0,
            name = "Test 21",
            parent = this
        ).apply 
            grandchildren =
                mutableSetOf(
                    Grandchild(
                        id = 0,
                        name = "Test 211",
                        child = this
                    )
                )
        ,
        Child(
            id = 0,
            name = "Test 22",
            parent = this
        )
    )


val saveParent: Parent = parentRepository.save(parent)

型号:

@Entity
class Parent(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,
    var name: String,
    @OneToMany(mappedBy = "parent", cascade = [CascadeType.ALL])
    var children: MutableSet<Child> = mutableSetOf()
)
@Entity
class Child(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,
    var name: String,
    @ManyToOne @JoinColumn(name = "child_id")
    var parent: Parent,
    @OneToMany(mappedBy = "child", cascade = [CascadeType.ALL])
    var grandchildren: MutableSet<Grandchild> = mutableSetOf()
)
@Entity
class Grandchild(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,
    var name: String,
    @ManyToOne @JoinColumn(name = "child_id")
    var child: Child
)

application.yml

spring:
  datasource:
    platform: postgres
    url: jdbc:postgresql://localhost:5432/onetomany?ssl=false
    driver-class-name: org.postgresql.Driver
    initialization-mode: always
  jpa:
    database: postgresql
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    generate-ddl: false
    hibernate:
      ddl-auto: none
  liquibase:
    enabled: true
    change-log: classpath:db/master.xml

liquibase 脚本:

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
  <changeSet author="bruce (generated)" id="data-1">
    <createTable tableName="parent">
      <column name="id" type="BIGINT">
        <constraints nullable="false" primaryKey="true" primaryKeyName="PK_PARENT"/>
      </column>
      <column name="name" type="VARCHAR(255)">
        <constraints nullable="false"/>
      </column>
    </createTable>
  </changeSet>
  <changeSet author="bruce (generated)" id="data-2">
    <createTable tableName="child">
      <column name="id" type="BIGINT">
        <constraints nullable="false" primaryKey="true" primaryKeyName="PK_CHILD"/>
      </column>
      <column name="name" type="VARCHAR(255)">
        <constraints nullable="false"/>
      </column>
      <column name="parent_id" type="BIGINT">
        <constraints nullable="false"/>
      </column>
    </createTable>
  </changeSet>
  <changeSet author="bruce (generated)" id="data-3">
    <createTable tableName="grandchild">
      <column name="id" type="BIGINT">
        <constraints nullable="false" primaryKey="true" primaryKeyName="PK_GRANDCHILD"/>
      </column>
      <column name="name" type="VARCHAR(255)">
        <constraints nullable="false"/>
      </column>
      <column name="child_id" type="BIGINT">
        <constraints nullable="false"/>
      </column>
    </createTable>
  </changeSet>
  <changeSet author="bruce (generated)" id="data-6">
    <addForeignKeyConstraint baseColumnNames="parent_id" baseTableName="child" constraintName="FK_CHILD_PARENT"
                             deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
                             referencedColumnNames="id" referencedTableName="parent" validate="true"/>
  </changeSet>
  <changeSet author="bruce (generated)" id="data-8">
    <addForeignKeyConstraint baseColumnNames="child_id" baseTableName="grandchild" constraintName="FK_CHILD_GRANDCHILD"
                             deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
                             referencedColumnNames="id" referencedTableName="child" validate="true"/>
  </changeSet>
</databaseChangeLog>

【问题讨论】:

我不知道 kotlin,但你为什么提供 id = 0 ? id 不应该为 null 吗? 【参考方案1】:

@GeneratedValue(strategy = GenerationType.IDENTITY) 通常可以通过在数据库中指定一个自动递增的默认值来工作,例如nextval('my_entity_sequence'::regclass)。插入时,数据库将生成标识符。

在 Postgres 中,有 serial/bigserial 伪类型来指定自动递增列(它将在内部创建序列以及列默认值),因此 DDL 可以例如看起来像这样:create table my_entity ( id bigserial not null, primary key (id) )https://www.postgresql.org/docs/current/datatype-numeric.html

在您的情况下,liquibase 错过了所有 ID 列的类型/默认值(现在只是“父”插入失败,但其他实体的插入也会失败)。

这是一个已知的 liquibase 问题:https://github.com/liquibase/liquibase/issues/1009 - 解决该问题的建议包括在变更集中手动指定 autoIncrement="true"

【讨论】:

谢谢!这解决了这个问题,但我永远不会自己发现这一点

以上是关于OneToMany 关系不能在春天用 liquibase 保存的主要内容,如果未能解决你的问题,请参考以下文章

onetomany / manytoone 的无限循环?春天/休息api

无法在休眠中删除 OneToMany 关系的实例

NHibernate OneToMany 代码映射

如何在 @OneToMany 关系映射的列上使用 JPA findBy 查询?

OneToMany 和 ManyToMany 单向关系的区别

Symfony2 OneToMany 关系