Home > Software engineering >  OneToMany relation cannot be saved in spring with liquibase
OneToMany relation cannot be saved in spring with liquibase

Time:12-17

I am trying to build a data model in spring that cascades up to 3 levels down with one-to-many relationships, but I cannot get it to work with liquibase scripts.

I am using spring boot with Kotlin and liquibase with a PostgreSQL database.

What I did so far:

  • cutting down the code to only contain the part that does not work (see below)
  • I tried both @OneToMany with @JoinTable as well as with @JoinColumn, and I also tried the same with @ManyToMany to rule out an issue with @OneToMany
  • I ran the same code (below) without liquibase to have Hibernate/JPA create the tables from the models
  • this actually worked, so I generated liquibase scripts out of these tables, but they look exactly the same as my own (except key names)
  • retrieving data with these models work (if I insert data via SQL directly)

To be honest I am not sure, if the issue is in the model, in the configuration, or in the liquibase scripts, so I'll just post all of those. Am I missing a configuration? Did I configure the cascading correct? Are my model definitions/liquibase scripts wrong?

The exception I get on saving a parent is:

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).

The code I am trying to run:

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)

models:

@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 script:

<?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>

CodePudding user response:

@GeneratedValue(strategy = GenerationType.IDENTITY) would normally work by specifying an auto-incrementing default value at the database, such as nextval('my_entity_sequence'::regclass). Upon insert, the DB will generate the identifier.

In Postgres, there are serial/bigserial pseudo-types to specify an auto-increment column (which will internally create the sequence as well as the column default value), so the DDL could e.g. look like this:
create table my_entity ( id bigserial not null, primary key (id) )
https://www.postgresql.org/docs/current/datatype-numeric.html

In your case, liquibase missed the type/defaults for all of the ID columns (right now just a "parent"-insert fails, but inserts for the other entities will fail as well).

This is a known liquibase issue: https://github.com/liquibase/liquibase/issues/1009 - suggestions to work around it include manually specifying autoIncrement="true" in the changeset.

  • Related