JPA has several dependencies on equals() and hashCode(), see JBoss: Equals and HashCode.
I was looking for a way to marry the practically usable features of data classes (copy method, toString, componentN methods) with the equals() and hashCode() demands from JPA. After several rounds of de-compiling Kotlin to Java I wonder if JPA had any consideration in designing data classes, because one has to be quite cautious when using them.
In the end the only practical solution I could come up with, was to create base classes that provide final implementations of equals() and hashCode() for entity data classes in a parent class.
To prevent duplicate Java field generation of version and id in DbEntity the JPA annotations must be placed at the parent class getters (Kotlin has no fields), whereas the JPA annotations of the data classes are created on Java field level (AccessType.FIELD on the base class).
Is this still correct usage of data classes or should one abstain to use them with JPA in general?
JPA Base Classes for Data Classes
/**
* Base class for all JPA entities with an id and a version property.
*/
@Access(AccessType.FIELD)
@MappedSuperclass
abstract class DbEntity : Serializable {
abstract var id: Long?
@Access(AccessType.PROPERTY)
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
get
abstract var version: LocalDateTime?
@Access(AccessType.PROPERTY)
@Version
get
}
/**
* Final equals(), hashCode() from java.lang.Object for sub classes that are data classes.
*/
abstract class IdentityDbEntity : DbEntity() {
override final fun hashCode() = super.hashCode()
override final fun equals(other: Any?) = super.equals(other)
}
/**
* Equality is checked via a business key that may be comprised of several elements.
* Each of these elements must implement equals() and hashCode() correctly for it to work.
*/
abstract class BusinessKeyDbEntity : DbEntity() {
protected abstract val businessKey: List<Any>
override final fun equals(other: Any?): Boolean {
if (this === other) return true
if (other === null || other !is BusinessKeyDbEntity) return false
if (this::class !== other::class) return false
return businessKey == other.businessKey
}
override final fun hashCode(): Int {
return businessKey.hashCode()
}
}
Example
/**
* Customers can have identical names and multiple credit cards.
*/
@Entity
data class Customer(
@Column(length = 30, nullable = false) var firstName: String,
@Column(length = 30, nullable = false) var lastName: String,
@OneToMany(cascade = arrayOf(CascadeType.ALL)) val creditCards: MutableSet<CreditCard> = mutableSetOf(),
override var id: Long? = null,
override var version: LocalDateTime? = null
) : IdentityDbEntity()
/**
* A credit card is identified by per credit card issuer and number.
*/
@Entity
@Table(uniqueConstraints = arrayOf(UniqueConstraint(columnNames = arrayOf("cardIssuer", "number"))))
data class CreditCard(
@Column(length = 30, nullable = false) @Enumerated(EnumType.STRING) var cardIssuer: CreditCardCompany,
@Size(min = 16, max = 16) @Column(length = 16, nullable = false) var number: String,
@Size(min = 3, max = 3) @Column(length = 3, nullable = false) var verificationNumber: String,
override var id: Long? = null,
override var version: LocalDateTime? = null
) : BusinessKeyDbEntity() {
override val businessKey: List<Any>
get() = listOf(cardIssuer, number)
}
enum class CreditCardCompany {
VISA, MASTERCARD, AMERICAN_EXPRESS
}