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