5.9 KiB
Hibernate / JPA Conversion Guide
When This Applies
Detected when imports match any of:
javax.persistence.*jakarta.persistence.*org.hibernate.*
Critical Rules
-
Do NOT use data classes for JPA entities. Data classes generate
equals/hashCodebased on all properties, which breaks Hibernate's identity semantics and proxy creation. -
Keep entity classes
open. Hibernate creates proxies via subclassing. Kotlin classes arefinalby default, so you must useopenexplicitly (or use theallopencompiler plugin with JPA annotation support). -
Provide a no-argument constructor if Hibernate requires one for proxy creation. Use a secondary constructor or default values for all primary constructor parameters.
-
Annotation site targets matter:
@Id,@Column,@GeneratedValueon fields → use@field:Id,@field:Column, etc. in Kotlin, OR place annotations on constructor parameters with@field:site target.@ManyToOne,@OneToMany,@JoinColumn→ same@field:targeting.
-
Lazy loading considerations:
@ManyToOne(fetch = FetchType.LAZY)requires the entity class to be open for proxy creation.@OneToManywith lazy collections work with Kotlin'sMutableList. -
@Embeddableclasses: Can be data classes (they don't need proxies). -
@MappedSuperclass: Must beopen abstract classin Kotlin.
Examples
Example 1: JPA Entity with @Id, @Column, and Relationships
Java:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String username;
@Column(name = "email")
private String email;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
protected User() {}
public User(String username, String email, Department department) {
this.username = username;
this.email = email;
this.department = department;
}
public Long getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Department getDepartment() { return department; }
public void setDepartment(Department department) { this.department = department; }
}
Kotlin:
@Entity
@Table(name = "users")
open class User(
@field:Column(name = "username", nullable = false, unique = true)
open val username: String,
@field:Column(name = "email")
open var email: String? = null,
@field:ManyToOne(fetch = FetchType.LAZY)
@field:JoinColumn(name = "department_id")
open var department: Department? = null
) {
@field:Id
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
open var id: Long? = null
protected set
protected constructor() : this(username = "")
}
Example 2: @Embeddable Value Object
Java:
@Embeddable
public class Address {
@Column(name = "street")
private String street;
@Column(name = "city")
private String city;
@Column(name = "zip_code")
private String zipCode;
protected Address() {}
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
public String getStreet() { return street; }
public String getCity() { return city; }
public String getZipCode() { return zipCode; }
}
Kotlin:
@Embeddable
data class Address(
@field:Column(name = "street")
val street: String = "",
@field:Column(name = "city")
val city: String = "",
@field:Column(name = "zip_code")
val zipCode: String = ""
)
@Embeddable classes can safely be data classes because Hibernate does not proxy them.
Default values satisfy the no-arg constructor requirement.
Example 3: Entity with @ManyToOne and @OneToMany
Java:
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<User> users = new ArrayList<>();
protected Department() {}
public Department(String name) {
this.name = name;
}
public Long getId() { return id; }
public String getName() { return name; }
public List<User> getUsers() { return users; }
public void addUser(User user) {
users.add(user);
user.setDepartment(this);
}
public void removeUser(User user) {
users.remove(user);
user.setDepartment(null);
}
}
Kotlin:
@Entity
@Table(name = "departments")
open class Department(
@field:Column(name = "name", nullable = false)
open val name: String = ""
) {
@field:Id
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
open var id: Long? = null
protected set
@field:OneToMany(mappedBy = "department", cascade = [CascadeType.ALL], orphanRemoval = true)
open val users: MutableList<User> = mutableListOf()
protected constructor() : this(name = "")
fun addUser(user: User) {
users.add(user)
user.department = this
}
fun removeUser(user: User) {
users.remove(user)
user.department = null
}
}
Key points in this example:
cascadearray syntax uses Kotlin's[CascadeType.ALL]instead of Java's{CascadeType.ALL}.- The collection is typed as
MutableListto allow Hibernate to manage the relationship. - The class and its properties are
openso Hibernate can create proxies. - The no-arg constructor delegates to the primary constructor with default values.