개발
home
🧵

[JPA] 일대다, 일대일 연관관계 함께 저장 cascade

Created
2022/07/04
Tags
SpringBoot
JPA
Cascade
Relation
2022-07-04 @이영훈
일대다, 일대일 관계에서 부모객체를 저장할 때 자식객체를 함께 저장하는 방법을 기록으로 남깁니다
전체코드는 깃허브에서 확인할 수 있습니다

도메인

LOL 프로게임팀과 프로게이머이 있습니다.
프로게이머들은 LOL 프로게임팀에 속할 수 있습니다.
LOL 프로게임팀 엔터티입니다.
일대다관계를 설정에서
GameTeam의 gamers 변수는 Gamer의 gameTeam 변수의 거울이기 때문에 mappedBy를 설정했습니다
OneToMany, ManyToMany에서는 기본적으로 fetch 전략이 LAZY이지만 개발자의 의도를 명시적으로 나타내기 위해서 FetchType.LAZY 를 설정합니다
️ 마지막으로 cascade를 CascadeType.ALL로 설정합니다. CascadeType.ALLCascadeType.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)