Home > other >  One to Many / Many to one relationship isn't saved in Kotlin, while it is in Java
One to Many / Many to one relationship isn't saved in Kotlin, while it is in Java

Time:08-25

I have a Kotlin project..

I added the complete code to github, so you can download it to play around.

there are 2 Entities:
Band

@Entity
data class Band(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,

    var name: @NotEmpty(message = "name must not be empty") String? = null,

    @OneToMany(fetch = FetchType.EAGER,
                mappedBy = "band",
                cascade = [CascadeType.ALL])
    val links: Set<Link> = mutableSetOf()
)

Links

@Entity
data class Link(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,

    var url: String? = null,

    @ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
    @JoinColumn(name = "band_id", nullable = true)
    var band: Band? = null

)

SQL looks like this:

create table band
(
    id serial PRIMARY KEY not null,
    name varchar(100) not null
);

create table link
(
    id serial PRIMARY KEY not null,
    band_id int,
    url varchar(100) not null,

    CONSTRAINT "fk_band_links" FOREIGN KEY ("band_id") REFERENCES "band" ("id")    
);

I create a band like this for my test (in BandUtils.kt)

fun createBandWithLinks(): Band {
        val link1 = Link()
        link1.url = "https://fb.com/"

        val link2 = Link()
        link2.url = "https://twitter.com/"

        return Band(
            name = "Band with links",
            links = mutableSetOf(link1,link2),
        )
    }

When I save the data:

bandRepository!!.save(BandUtils.createBandWithLinks())

I expect, that the links and the bands are saved! This works.

But I can't see the bands in the link table.

I made the same thing before in Java, and I also see the examples from Baeldung work like this.

Is there any difference between Kotlin and Java?

CodePudding user response:

Problem 1. Persisting bidirectional relationship Band=Link
In the case of a bidirectional relationship Hibernate (or JPA implementations) cares only about the owning side of the association. The owning side is the side that doesn't have the mappedBy attribute!
So if we only call band.links = LinkUtils.createListOfLinks(), the Band will not be linked to the new Link entity, because this is not the owning /tracked side of the relation.

You need to explicitly set Band for Link, call link.band = band, because that is the owning side of the relation.

When using mappedBy, it is the responsibility of the developer to know what is the owning side, and update the correct side of the relation in order to trigger the persistence of the new relationship in the database.

There are no differences between Java and Kotlin. It is JPA specification.

Solution 1: set relation from both sides
Correct Utils to set relation from both sides

object BandUtils {
    fun createBandWithLinks(): Band {
         val band = Band(
            name = "MEA with links",
            description = "merch em all"
            )
        band.links = LinkUtils.createListOfLinks(band)
        return band
    }
}

object LinkUtils {
    fun createListOfLinks(band: Band): MutableSet<Link> {
        val link1 = Link()
        link1.url = "https://fb.com/joergi"
        link1.band = band

        val link2 = Link()
        link2.url = "https://twitter.com/joergi"
        link2.band = band

        var linkSets = mutableSetOf<Link>()
        linkSets.add(link1)
        linkSets.add(link2)
        return linkSets
    }
}

Solution 2: change the owning side of the relation (NOT recommended)
No need to change your utils.
Remove mappedBy for OneToMany, add JoinColumn

    @OneToMany(
        fetch = FetchType.EAGER,
       // mappedBy = "band",
        cascade = [CascadeType.ALL]
    )
    @JoinColumn(name = "band_id")
    var links: MutableSet<Link> = mutableSetOf()

Add JoinColumn(insertable = false, updatable = false) for ManyToOne

    @ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
    @JoinColumn(name = "band_id", insertable = false, updatable = false)
    var band: Band? = null

Entities definition

@Entity
data class Band(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,

    var name: @NotEmpty(message = "name must not be empty") String? = null,

    var description: String? = null,

    @JsonBackReference(value = "links")
    @OneToMany(
        fetch = FetchType.EAGER,
        cascade = [CascadeType.ALL]
    )
    @JoinColumn(name = "band_id")
    var links: MutableSet<Link> = mutableSetOf()
}

@Entity
data class Link(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,


    var url: String? = null

) {
    @ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
    @JoinColumn(name = "band_id", insertable = false, updatable = false)
    var band: Band? = null
}

In your Baeldung article this problem is described in item 6.

Problem 2. Bidirectional relationship data class circular dependency for toString and hashCode
When you perform the correct creation of your entities you will receive StackOverflow error during persisting because you have bidirectional relations and entities defined like data class. Kotling will generate incorrect hashCode implementation which moves into an infinity loop during execution for bidirectional entities.

Solution 1: Lombok
Use Lombok instead of data class and ignore one relation side

@Entity
@Data
@EqualsAndHashCode(exclude=["band"])
@ToString(exclude = ["band"])
class Link
@Entity
@Data
class Band

Solution 2: move the properties that cause de circular dependency to the data class body
See details Kotlin - Data class entity throws StackOverflowError

@Entity
data class Link(

    @Id
    @Setter(AccessLevel.NONE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Schema(hidden = true)
    val id: Int? = null,


    var url: String? = null

) {
    @ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
    @JoinColumn(name = "band_id", nullable = true)
    var band: Band? = null
}

UPDATE
Why changing owning side is not a good solution?
Its influences on the count of generated queries and as a result on performance.
3 Queries generated in case owning side is ManyToOne:

Hibernate:
    insert 
    into
        band
        (id, description, name) 
    values
        (default, ?, ?)
Hibernate: 
    insert 
    into
        link
        (id, band_id, url) 
    values
        (default, ?, ?)
Hibernate: 
    insert 
    into
        link
        (id, band_id, url) 
    values
        (default, ?, ?)

5 Queries generated in case owning side is OneToMany:

Hibernate:
  insert 
    into
        band
        (id, description, name) 
    values
        (default, ?, ?)
Hibernate: 
    insert 
    into
        link
        (id, url) 
    values
        (default, ?)
Hibernate: 
    insert 
    into
        link
        (id, url) 
    values
        (default, ?)
Hibernate: 
    update
        link 
    set
        band_id=? 
    where
        id=?
Hibernate: 
    update
        link 
    set
        band_id=? 
    where
        id=?
  • Related