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
}
}
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:
id
does not need to be nullable. 0 as default value already means "not persisted".id
will be auto generated and should not be in the constructorid
is not part of the primary constructor equals
would yield the wrong results (because id would not be part of comparison) unless overriden in a correct manner, so I would not use a data class
lineItems
is property without backing field there is not need for a @Transient
annotationaddLineItem
since it returns Unit
, which also enables you to use the +=
operator instead of an explicit function call (plusAssign
).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
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With