Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin JPA Encapsulate OneToMany

Tags:

jpa

kotlin

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
    }
}
like image 551
greyfox Avatar asked Aug 11 '17 19:08

greyfox


2 Answers

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 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
  • 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 returns Unit, which also enables you to use the += operator instead of an explicit function call (plusAssign).
like image 171
Willi Mentzel Avatar answered Sep 20 '22 14:09

Willi Mentzel


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
)
like image 29
Johan Vergeer Avatar answered Sep 17 '22 14:09

Johan Vergeer