2022-07-04 @이영훈
일대다, 일대일 관계에서 부모객체를 저장할 때 자식객체를 함께 저장하는 방법을 기록으로 남깁니다
도메인
LOL 프로게임팀과 프로게이머이 있습니다.
프로게이머들은 LOL 프로게임팀에 속할 수 있습니다.
LOL 프로게임팀 엔터티입니다.
일대다관계를 설정에서
•
GameTeam의 gamers 변수는 Gamer의 gameTeam 변수의 거울이기 때문에 mappedBy를 설정했습니다
•
OneToMany, ManyToMany에서는 기본적으로 fetch 전략이 LAZY이지만 개발자의 의도를 명시적으로 나타내기 위해서 FetchType.LAZY 를 설정합니다
•
️ 마지막으로 cascade를 CascadeType.ALL로 설정합니다. CascadeType.ALL 은 CascadeType.PERSIST 를 포함하기 때문에 이 설정으로 부모 엔터티를 저장할 때 자식 엔터티도 함께 저장이 됩니다.
// GameTeam.kt
@Entity
class GameTeam(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val name: String,
@OneToMany(mappedBy = "gameTeam", fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
val gamers: MutableList<Gamer> = ArrayList(),
) : BaseTimeEntity() {
companion object {
fun construct(name: String): GameTeam {
return GameTeam(name = name)
}
}
}
Kotlin
복사
프로게이머 엔터티입니다.
// Gamer.kt
@Entity
class Gamer(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val name: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "game_team_id")
var gameTeam: GameTeam,
) : BaseTimeEntity() {
companion object {
// 생성 메소드
// 생성시 연관관계 설정도 같이 합니다
// 객체를 생성하면서 연관관계도 설정해서 "원자적으로" 생성합니다
fun construct(name: String, gameTeam: GameTeam): Gamer {
val gamer = Gamer(name = name, gameTeam = gameTeam)
gamer.setGameTeam(gameTeam)
return gamer
}
}
// 연관관계 설정 메소드
// 객체를 생성할 때만 사용합니다
@JvmName("customSetGameTeam")
private fun setGameTeam(gameTeam: GameTeam) {
this.gameTeam = gameTeam
gameTeam.gamers.add(this)
}
}
Kotlin
복사
생성시간과 업데이트 시간은 공통으로 사용하기 위해서 따로 뺐습니다
// BaseTimeEntity.kt
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity {
lateinit var createdAt: OffsetDateTime
lateinit var updatedAt: OffsetDateTime
@PrePersist
fun prePersist() {
createdAt = OffsetDateTime.now()
updatedAt = OffsetDateTime.now()
}
@PreUpdate
fun preUpdate() {
updatedAt = OffsetDateTime.now()
}
}
Kotlin
복사
테스트 코드로 확인해보기
@SpringBootTest
@Transactional
internal class GameTeamTest {
@Autowired
private lateinit var gameTeamRepository: GameTeamRepository
@Test
fun `test cascade persist`() {
val gameTeam = GameTeam.construct("Damwon KIA")
val gamer1 = Gamer.construct("Showmaker", gameTeam)
val gamer2 = Gamer.construct("Canyon", gameTeam)
gameTeamRepository.save(gameTeam)
}
}
Kotlin
복사
위의 테스트 코드를 실행시켜보면, 아래와 같이 1번의 game_team insert 쿼리문과 2번의 gamer insert 쿼리문이 실행되는 것을 확인할 수 있습니다.
테스트코드에서는 gamer를 저장하는 코드가 따로 없는데도 gamer insert문이 실행됩니다.
cascade의 persist 속성 덕분에 함께 실행되는 것입니다.
insert into game_team (id, created_at, updated_at, name) values (…)
insert into gamer (id, created_at, upated_at, game_team_id, name) values (…)
insert into gamer (id, created_at, upated_at, game_team_id, name) values (…)
프로게이머의 gameTeam에도 cascade 설정해보기
Gamer의 gameTeam에도 cascade를 설정하였습니다
@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "game_team_id")
var gameTeam: GameTeam,
Kotlin
복사
Gamer 엔터티 전체 코드는 다음과 같습니다
@Entity
class Gamer(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val name: String,
@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "game_team_id")
var gameTeam: GameTeam,
) : BaseTimeEntity() {
companion object {
// 생성 메소드
// 생성시 연관관계 설정도 같이 합니다
// 객체를 생성하면서 연관관계도 설정해서 "원자적으로" 생성합니다
fun construct(name: String, gameTeam: GameTeam): Gamer {
val gamer = Gamer(name = name, gameTeam = gameTeam)
gamer.setGameTeam(gameTeam)
return gamer
}
}
// 연관관계 설정 메소드
// 객체를 생성할 때만 사용합니다
@JvmName("customSetGameTeam")
private fun setGameTeam(gameTeam: GameTeam) {
this.gameTeam = gameTeam
gameTeam.gamers.add(this)
}
}
Kotlin
복사
테스트 코드로 확인해보기 (2)
@SpringBootTest
@Transactional
internal class GamerTest {
@Autowired
private lateinit var gamerRepository: GamerRepository
@Test
fun `test cascade persist`() {
val gameTeam = GameTeam.construct("Damwon KIA")
val gamer1 = Gamer.construct("Showmaker", gameTeam)
val gamer2 = Gamer.construct("Canyon", gameTeam)
gamerRepository.save(gamer1)
}
}
Kotlin
복사
gamer1만 저장했는데, gameTeam과 gamer2도 함께 저장되는 것을 확인할 수 있습니다
JPA의 영속성 컨텍스트에 gameTeam, game1, game2가 모두 저장되어 있습니다. 이때 game1을 flush 하면 연관되어 있는 gameTeam도 flush되고 gameTeam과 연관되어 있는 game2 또한 함께 flush 되는 것입니다.
insert into game_team (id, created_at, updated_at, name) values (…)
insert into gamer (id, created_at, updated_at, game_team_id, name)
insert into gamer (id, created_at, updated_at, game_team_id, name)