Kotlin으로 JPA Entity 정의하기
JPA spec
JPA 엔티티와 프로퍼티의 제약 사항에 대해 먼저 알고 가야한다.
https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1.pdf
The entity class must
- be annotated with the Entity annotation.
- have a public or protected no-arg constructor.
- be a top-level class. (An enum or interface must not be designated as an entity.)
- not be final.
- implement the Serializable interface, if an entity instance is to be passed by value as a detached object.
The instance variables of a class must
- be private, protected, or package visibility.
- have public or protected property accessor methods. (when property access is used)
Field Access (필드 접근)
- 객체의 필드에 직접 접근
- object.fieldName
Property Access (프로퍼티 접근)
- 접근자 메서드를 통해 필드에 접근
- object.getFieldName(), object.setFieldName(value)
The default access type of an entity hierarchy is determined by the placement of mapping annotations on the attributes of the entity classes.
@Id를 필드에 붙일 경우 Field Access, 게터에 붙일 경우 Property Access가 사용된다. 근데 Field Access가 낫다! 왜냐하면 불필요한 게터, 세터를 만들어서 데이터를 노출시키지 않으니까.
Kotlin으로 JPA Entity 정의하기
Entity는 상속이 가능해야 한다.
코틀린의 class는 기본적으로 final이다. 그러나 JPA 엔티티는 상속이 가능해야 하며, 기본 생성자를 가져야 한다.
jpa 플러그인 사용을 통해 클래스에 기본 생성자를 추가하지 않아도 된다.
spring 플러그인 사용을 통해 클래스에 open 키워드를 추가하지 않아도 된다.
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.9.25'
id 'org.jetbrains.kotlin.plugin.spring' version '1.9.25'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.jetbrains.kotlin.plugin.jpa' version '1.9.25'
}
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}
Property의 접근을 최대한 막는다.
open property의 경우에 private setter는 허용되지 않기에 그 다음으로 제한된 protected를 사용한다.
class Member(
name: String
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
protected set
var name: String = name
protected set
}
공통 Property들을 묶는다.
class BaseTimeEntity {
@Column(name = "created_at", nullable = false, insertable = false, updatable = false)
var createdAt: LocalDateTime? = null
protected set
@Column(name = "updated_at", nullable = false, insertable = false, updatable = false)
var modifiedAt: LocalDateTime? = null
protected set
}
db 설정에 의해 created_at 데이터가 입력되는 게 아니라면 애플리케이션 상에서 입력해준다.
이 때 이 값은 한 번 생성되면 바뀌지 않기 때문에 not nullable val로 선언할 수 있다.
@Column(name = "created_at", nullable = false, updatable = false)
val createdAt: LocalDateTime = LocalDateTime.now()
protected set
JPA Auditing을 사용하는 경우,
@SpringBootApplicaiton
@EnableJpaAuditing
class MyApplication {
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
}
@EntityListeners(AuditingEntityListener::class)
@MappedSuperClass
abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
var createdAt: LocalDateTime? = null
protected set
@LastModifiedDate
@Column(nullable = false)
var modifiedAt: LocalDateTime? = null
protected set
@CreatedBy
@Column(nullable = false, updatable = false)
var createdBy: String? = null
protected set
@LastModifiedBy
@Column(nullable = false)
var modifiedBy: String? = null
protected set
}
Property를 바디에 정의한다.
코틀린은 Property를 생성자에서 선언할 수 있다. 그러나 이렇게 선언 시 Property들이 파편화 되기 때문에 나는 모든 Property를 바디에 정의하는 것을 선호한다.
엔티티 양방향 매핑일 경우, 객체를 양쪽에 세팅해준다.
연관 관계의 주인 쪽에만 객체를 주입해주면 디비에 저장은 되지만 객체는 완전하지 않다.
(이게 적절한 예제는 아닌 것 같지만 이런 느낌으로 양쪽에 매핑 ㅎㅎ)
@Entity
class Order {
@OneToOne(mappedBy = "order", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
val usedCoupon: UsedCoupon?
fun useCoupon(coupon: UsedCoupon) {
this.usedCoupon = coupon
}
}
@Entity
class UsedCoupon(
order: Order
) {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false, updatable = false)
var order: Order = order
init {
order.useCoupon(this)
}
}