Kotlin JPA Encapsulate OneToMany
Solution 1
Your basic idea is correct but, I would propose some slight modifications:
@Entity
class OrderEntity(
var firstName: String,
var lastName: String
) {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long = 0
@OneToMany(cascade = [(CascadeType.ALL)], fetch = FetchType.LAZY, mappedBy = "order")
private val _lineItems = mutableListOf<LineItem>()
val lineItems get() = _lineItems.toList()
fun addLineItem(newItem: LineItem) {
_lineItems += newItem // ".this" can be omitted too
}
}
@Entity
class LineItem(
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "order_id")
val order: OrderEntity? = null
){
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long = 0
}
Notes:
- the
id
does not need to be nullable. 0 as default value already means "not persisted". - use a primary constructor, there is no need for a protected empty primary constructor (see no-arg compiler plug-in)
- The
id
will be auto generated and should not be in the constructor - since the
id
is not part of the primary constructorequals
would yield the wrong results (because id would not be part of comparison) unless overriden in a correct manner, so I would not use adata class
- if
lineItems
is property without backing field there is not need for a@Transient
annotation - it would be better to use a block body for
addLineItem
since it returnsUnit
, which also enables you to use the+=
operator instead of an explicit function call (plusAssign
).
Solution 2
First of all I like to use data classes this way you get equals
, hashCode
and toString
for free.
I put all the properties that are not collections into the primary constructor. I put the collections into the class body.
There you can create a private val _lineItems
which is a backing property (which can be generated by IntelliJ after you have created the val lineItems
property.
Your private backing field has a mutable collection (I prefer to use a Set
whenever possible), which can be changed using the addNewLineItem
method. And when you get the property lineItems
you get an immutable collection. (Which is done by using .toList()
on a mutable list.
This way the collection is encapsulated, and it is still pretty concise.
import javax.persistence.*
@Entity
data class OrderEntity(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long? = -1,
var firstName: String,
var lastName: String
) {
@OneToMany(cascade = [(CascadeType.ALL)], fetch = FetchType.LAZY, mappedBy = "order")
private val _lineItems = mutableListOf<LineItem>()
@Transient
val lineItems = _lineItems.toList()
fun addLineItem(newItem: LineItem) = this._lineItems.plusAssign(newItem)
}
@Entity
data class LineItem(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long? = -1,
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "order_id")
val order: OrderEntity? = null
)
Related videos on Youtube
greyfox
I'm a web developer from Columbus, Ohio. I studied Computer Science at Capital University in Columbus, where I received my bachelor's degree. In college I was heavy into C++ and Python. I dabbled my hands in Objective-C/Cocoa as well. After college I began doing web development using PHP/MySQL. I really fell in love with web development. Now I'm transitioning into Java/Spring MVC. At some point I would like to get more into ASP.NET MVC.
Updated on September 14, 2022Comments
-
greyfox over 1 year
I am using JPA with Kotlin and coming against an issue trying to encapsulate a OneToMany relationship. This is something I can easily achieve in Java but having some issues due to Kotlin only having properties and no fields in classes.
I have an order, and an order has one to many line items. The order object has a MutableList of LineItem BUT the get method SHOULD NOT return a mutable list, or anything that the caller could potentially modify, as this breaks encapsulation. The order class should be responsible for managing collection of line items and ensuring all business rules / validations are met.
This is the code I've come up with thus far. Basically I'm using a backing property which is the MutableList which Order class will mutate, and then there is a transient property which returns Iterable, and
Collections.unmodifiableList(_lineItems)
ensure that even if caller gets the list, and cast it to MutableList they won't be able to modify it.Is there a better way to enforce encapsulation and integrity. Perhaps I'm just being too defensive with my design and approach. Ideally no one should be using the getter to get and modify the list, but hey it happens.
import java.util.* import javax.persistence.* @Entity @Table(name = "order") open class Order { @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null @Column(name = "first_name") lateinit var firstName: String @Column(name = "last_name") lateinit var lastName: String @OneToMany(cascade = arrayOf(CascadeType.ALL), fetch = FetchType.LAZY, mappedBy = "order") private val _lineItems: MutableList<LineItem> = ArrayList() val lineItems: Iterable<LineItem> @Transient get() = Collections.unmodifiableList(_lineItems) protected constructor() constructor(firstName: String, lastName: String) { this.firstName = firstName this.lastName = lastName } fun addLineItem(newItem: LineItem) { // do some validation and ensure all business rules are met here this._lineItems.add(newItem) } } @Entity @Table(name = "line_item") open class LineItem { @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "order_id", referencedColumnName = "id") lateinit var order: Order private set // whatever properties might be here protected constructor() constructor(order: Order) { this.order = order } }
-
Johan Vergeer about 6 yearsI took the liberty of creating a gist from your example. gist.github.com/johanvergeer/eb0827595c490f742ce3dc9d4ef306c3
-
Michael Piefel over 4 yearsThese classes are problematic as entities. They will change their hash code when they are persisted.
toString()
for bidirectional associations will recurse forever. Kotlin data classes are not perfect entities. -
TomPlum over 2 yearsIntelliJ provides a warning now when annotating a data class with
@Entity
- The data class implementations of equals(), hashCode() and toString() are not recommended for JPA entities. They can cause severe performance and memory consumption issues.