Kotlin JPA Encapsulate OneToMany

11,359

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 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).

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
)
Share:
11,359

Related videos on Youtube

greyfox
Author by

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, 2022

Comments

  • greyfox
    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
    Johan Vergeer about 6 years
    I took the liberty of creating a gist from your example. gist.github.com/johanvergeer/eb0827595c490f742ce3dc9d4ef306c‌​3
  • Michael Piefel
    Michael Piefel over 4 years
    These 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
    TomPlum over 2 years
    IntelliJ 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.