Accessing Non-Final Property Name in Constructor With JPA
Author
Marcus HeldThe implications of JPA always manage to surprise me. Yesterday a colleague of mine made me aware of a warning in IntelliJ. The conversation went like that: “Marcus, in your blog you explained that we should check constraints in the constructor instead of bean validation
. Me: “yeah”. “I wanted to make it right, but when I do it in this entity IntelliJ warns me with Accessing non-final property name in constructor
”. So I dug into it. Fearing my conclusion would change the recommendation that I gave in (k)lean JPA
two years ago.
But first, let’s look at an example how an Entity looks like when you follow my recommendations:
@Entity
class User(
var name: String
// BaseEntity implementation omitted for readability - check the (K)lean JPA talk to understand its purpose
) : BaseEntity() {
init {
check(name.isNotEmpty()) { "name must not be empty" }
}
}
The warning is on name.isNotEmpty()
. It states: Accessing non-final property name in constructor. But - this member is final by default in Kotlin, you correctly state.
Right. I reasoned that we must configure the all-open compiler plugin
, so JPA does not silently fetch all relations eager. And this is what I do in all my projects.
So the code the compiler sees actually looks like this:
@Entity
open class User(
open var name: String
// BaseEntity implementation omitted for readability - check the (K)lean JPA talk to understand its purpose
) : BaseEntity() {
init {
check(name.isNotEmpty()) { "name must not be empty" }
}
}
And that’s why IntelliJ warns that we access a non-final property in the constructor.
We can construct scenarios where the access to inherited properties might not be fully initialized and therefore result in an unexpected value.
Example:
abstract class Base {
val code = calculate()
abstract fun calculate(): Int
}
class Derived(private val x: Int) : Base() {
override fun calculate() = x
}
fun testIt() {
println(Derived(42).code) // Expected: 42, actual: 0
}
Fine, should we remove the all-open plugin?
Is it necessary to have the members open? Yes! We need all members to be open. Check this example:
@Entity
open class Person(
@Id
val id: UUID = UUID.randomUUID(),
@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
var items: MutableSet<Item> = mutableSetOf()
)
@Entity
open class Item(
@Id
val id: UUID = UUID.randomUUID(),
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
var brand: Brand
)
@Entity
open class Brand(
@Id
val id: UUID = UUID.randomUUID()
)
When we fetch the Person
we see the following logs:
Hibernate: select p1_0.id from person p1_0 where p1_0.id=?
Hibernate: select i1_0.person_id,i1_1.id,i1_1.brand_id from person_items i1_0 join item i1_1 on i1_1.id=i1_0.items_id where i1_0.person_id=?
Hibernate: select b1_0.id from brand b1_0 where b1_0.id=?
So, even when we specified that the relations should be fetched lazy - Hibernate did not do it. Instead, when we declare all properties open
we see the expected select - just the person
:
Hibernate: select p1_0.id from person p1_0 where p1_0.id=?
At least, this time Hibernate prints a warning when booting issuing that the members must be open.
HHH000305: Could not create proxy factory for:com.example.jpainitissue.Item
org.hibernate.HibernateException: Getter methods of lazy classes cannot be final: com.example.jpainitissue.Item#getBrand
Conclusion
The only right way to deal with the warning is:
*drumm roll*
Ignoring it.
Yes, this is frustrating. But there is no other alternative. At least constructing such an issue which the inspection warns us against is unlikely with entities. We would need to derive values from inherited properties. It’s hard for me to construct such a scenario.
And to close with an important note. This is not an issue with kotlin. You have the same issue with Java. You actually have the issue all the time because all members are non-final by default. You just don’t have an inspection warning you against it - since it would be super annoying ;-)