๐ŸŒง๏ธ ORM/JPA

JPA ๋ณตํ•ฉํ‚ค ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ (feat. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋กœ ์•Œ์•„๋ณด์ž)

iseunghan 2023. 7. 26. 23:54
๋ฐ˜์‘ํ˜•

Intro

๋‹ค๋Œ€๋‹ค ํ…Œ์ด๋ธ”์„ ๋ณดํ†ต ์ผ๋Œ€๋‹ค ↔ ๋‹ค๋Œ€์ผ ํ…Œ์ด๋ธ”๋กœ ํ’€์–ด์„œ ๊ตฌ์„ฑํ•˜๊ณค ํ•ฉ๋‹ˆ๋‹ค. ์ค‘๊ฐ„์— ์ด์–ด์ฃผ๋Š” ํ…Œ์ด๋ธ”์„ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์ด๋ผ๊ณ  ๋ถ€๋ฅด๋Š”๋ฐ ์ด ํ…Œ์ด๋ธ”์˜ PK๊ฐ€ ์–‘์ชฝ ํ…Œ์ด๋ธ”์˜ PK๋ฅผ ๊ฐ€์ง€๊ณ  ๋ณตํ•ฉํ‚ค๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

JPA์—์„œ ๋ณตํ•ฉํ‚ค๋กœ PK๋ฅผ ๊ตฌ์„ฑํ–ˆ์„ ๋•Œ, ์ €์žฅ ๋˜๋Š” PK๋ฅผ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด์•ผํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฐœ๋ฐœํ™˜๊ฒฝ

  • SpringBoot 2.5.11
  • Java 11
  • H2 memory db

ํ…Œ์ŠคํŠธ์šฉ ์—”ํ‹ฐํ‹ฐ ์†Œ๊ฐœ

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public Member(String name) {
        this.name = name;
    }

    public Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }

    public void updateTeam(Team team) {
        this.team = team;
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }

    public void addMember(Member member) {
        this.members.add(member);
        member.updateTeam(this);
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class MemberTeam {
    @EmbeddedId
    private MemberTeamId id;

    @MapsId("memberId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @MapsId("teamId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

		private String description;

    public MemberTeam(Member member, Team team) {
        this.id = MemberTeamId.of(team.getId(), member.getId());
        this.member = member;
        this.team = team;
				 this.description = "none";
    }

    public void changeMember(Member member) {
        this.member = member;
    }

		public void changeDescription(String description) {
			this.description = description;
		}
}

์—”ํ‹ฐํ‹ฐ ์ €์žฅ ํ…Œ์ŠคํŠธ

๋ณตํ•ฉํ‚ค๋ฅผ ๊ฐ€์ง„ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์ €์žฅํ•  ๋•Œ ์‹ค์ˆ˜ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ€์ด์Šค๋“ค์„ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด์„œ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๊ธฐ๋ณธ ๊ตฌ์กฐ

@DataJpaTest
class MemberTeamRepositoryTest {

    @Autowired private MemberRepository memberRepository;
    @Autowired private TeamRepository teamRepository;
    @Autowired private MemberTeamRepository memberTeamRepository;
    @Autowired private EntityManager em;
    private Long member1Id;
    private Long member2Id;
    private Long team1Id;
    private Long team2Id;

    @Commit
    @BeforeEach
    void setup() {
        Member member1 = new Member("member1");
        Member member2 = new Member("member2");
        Team team1 = new Team("team1");
        Team team2 = new Team("team2");
        team1.addMember(member1);
        team1.addMember(member2);
        teamRepository.saveAndFlush(team1);
        teamRepository.saveAndFlush(team2);

        MemberTeam memberTeam = new MemberTeam(member1, team1);
        memberTeamRepository.saveAndFlush(memberTeam);
        MemberTeam memberTeam2 = new MemberTeam(member1, team2);
        memberTeamRepository.saveAndFlush(memberTeam2);

        this.member1Id = member1.getId();
        this.member2Id = member2.getId();
        this.team1Id = team1.getId();
        this.team2Id = team2.getId();
        em.flush();
        em.clear();
    }
}

๋ณ€๊ฒฝํ•˜๋ ค๋Š” ์—”ํ‹ฐํ‹ฐ๋งŒ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ

@Test
void MemberTeam์„_๋ณ€๊ฒฝํ• ์ˆ˜์žˆ๋‹ค() throws Exception {
    // given
    MemberTeam memberTeam = memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id)).orElseThrow();

    // when
    Member member2 = memberRepository.findById(member2Id).orElseThrow();
    memberTeam.changeMember(member2);

    MemberTeam modifiedMemberTeam = memberTeamRepository.save(memberTeam);
    assertThat(modifiedMemberTeam.getId().getMemberId()).isEqualTo(member2Id);
}

์œ„์— ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•˜๋‚˜์˜ ํŒ€์— ์†ํ•œ 2๋ช…์˜ Member๋ฅผ ์ €์žฅํ•˜๊ณ , member1๊ณผ team1์˜ MemberTeam์„ ํ•˜๋‚˜ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  memberTeam์— ์žˆ๋Š” member1์„ member2๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

  • member1๊ณผ team1์— ๋Œ€ํ•œ memberTeam์„ ์กฐํšŒํ•œ๋‹ค.
  • memberTeam ๋‚ด๋ถ€์— ์žˆ๋Š” changeMember๋ฅผ ํ†ตํ•ด member๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค.
  • ๋ณ€๊ฒฝ๋œ memberTeam์„ ๋‹ค์‹œ ์ €์žฅํ•œ๋‹ค.

์œ„ ํ…Œ์ŠคํŠธ๋Š” ํ†ต๊ณผ์ผ๊นŒ์š”? ์‹คํŒจ์ผ๊นŒ์š”?

์‹คํ–‰๊ฒฐ๊ณผ

org.opentest4j.AssertionFailedError: 
expected: 3L
but was : 2L
Expected :3L
Actual   :2L

์‹คํ–‰๊ฒฐ๊ณผ๋Š” ๋‹น์—ฐํžˆ ์‹คํŒจ์ž…๋‹ˆ๋‹ค. ์™œ๊ทธ๋Ÿด๊นŒ์š”?

๋ณตํ•ฉํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ ๊ฐ€์žฅ ํ—ท๊ฐˆ๋ฆฌ๋Š” ๋ถ€๋ถ„์ธ๋ฐ, MemberTeam์€ ๋ณตํ•ฉํ‚ค๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทผ๋ฐ member๋งŒ ๋ฐ”๊พผ๋‹ค๊ณ  ๋ฐ˜์˜์ด ๋ ๊นŒ์š”?

์šฐ๋ฆฌ๋Š” MemberTeam์˜ ์•„์ด๋””์ธ MemberTeamId์„ ํ•จ๊ป˜ ๋ฐ”๊ฟ”์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Test
void MemberTeam์„_๋ณ€๊ฒฝํ• ์ˆ˜์žˆ๋‹ค() throws Exception {
    // given
    MemberTeam memberTeam = memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id)).orElseThrow();

    // when
    Member member2 = memberRepository.findById(member2Id).orElseThrow();
    memberTeam.changeMember(member2);

    MemberTeam modifiedMemberTeam = memberTeamRepository.save(memberTeam);
    assertThat(modifiedMemberTeam.getId().getMemberId()).isEqualTo(member2Id);
}

๋ณตํ•ฉํ‚ค ์ „์ฒด๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ

์œ„์—์„œ ์‚ดํŽด๋ณธ ์˜ˆ์ œ์—์„œ ๋ณตํ•ฉํ‚ค๋ฅผ ๋ณ€๊ฒฝํ•ด๋ณด๋„๋ก ํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?

์•„๋ž˜๋Š” DeployLogId๋ฅผ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class MemberTeam {
	...

	public void changeMember(Member member) {
	    this.id = MemberTeamId.of(this.team.getId(), member.getId());
	    this.member = member;
	}
}

์‹คํ–‰๊ฒฐ๊ณผ๋Š”?

org.springframework.orm.jpa.JpaSystemException: identifier of an instance of com.in2wise.in8opsbe.entity.deployLog.DeployLog was altered from com.in2wise.in8opsbe.entity.deployLog.DeployLogId@90404d71 to com.in2wise.in8opsbe.entity.deployLog.DeployLogId@90404d90; nested exception is org.hibernate.HibernateException: identifier of an instance of com.in2wise.in8opsbe.entity.deployLog.DeployLog was altered from com.in2wise.in8opsbe.entity.deployLog.DeployLogId@90404d71 to com.in2wise.in8opsbe.entity.deployLog.DeployLogId@90404d90

JPA์—์„œ๋Š” ๊ฐ์ฒด์˜ ๋™์ผ์„ฑ ๋น„๊ต๋ฅผ ์ง€์›ํ•˜๋Š”๋ฐ ์œ„ ์ฝ”๋“œ๋กœ ์ธํ•ด์„œ DeployLogId์˜ ๊ฐ์ฒด์˜ ์ฃผ์†Œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์•„๋ž˜์™€ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค.

์ ˆ๋Œ€ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ง๊ณ  ๊ธฐ์กด Id ๊ฐ์ฒด์—์„œ ๋ณ€๊ฒฝํ•˜๋ ค๋Š” ํ‚ค๊ฐ’๋งŒ ์ˆ˜์ •ํ•˜๋„๋ก ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๋ณตํ•ฉํ‚ค ๋‚ด๋ถ€ ๋ณ€์ˆ˜๋ฅผ ๋ฐ”๊ฟ”๋ณด์ž

์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ ์ „์ฒด ํ†ต์œผ๋กœ ๋ฐ”๊พธ์ง€๋ง๊ณ  ID ํด๋ž˜์Šค ๋‚ด๋ถ€ ๋ณ€์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ๋„๋ก ํ•ฉ์‹œ๋‹ค.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class MemberTeam {
	...

	public void changeTrainedModel(TrainedModel trainedModel) {
        this.member = member;
        this.id.changeMemberId(member.getId()); // **
	}
}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(staticName = "of")
@Embeddable
public class MemberTeamId implements Serializable {
    private Long teamId;
    private Long memberId;

    public void changeMemberId(Long memberId) {
        this.memberId = memberId;
    }	
		...
}

์ด์ œ ์•„๋ž˜ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?

@Test
@Transactional
void MemberTeam์„_๋ณ€๊ฒฝํ• ์ˆ˜์žˆ๋‹ค() throws Exception {
    // given
    MemberTeam memberTeam = memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id)).orElseThrow();

    // when
    Member member2 = memberRepository.findById(member2Id).orElseThrow();
    memberTeam.changeMember(member2);
    MemberTeam modifiedMemberTeam = memberTeamRepository.saveAndFlush(memberTeam);

    // then
(1) assertThat(modifiedMemberTeam.getId().getMemberId()).isEqualTo(member2Id);
(2) assertThat(memberTeamRepository.count()).isEqualTo(2);
(3) assertThat(memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id))).isNotPresent();
}

์‹คํ–‰๊ฒฐ๊ณผ

[1] o.h.p.entity.AbstractEntityPersister     : HHH000502: The [member] property of the [me.iseunghan.testjpa.domain.MemberTeam] entity was modified, but it won't be updated because the property is immutable.
...
Expecting an empty Optional but was containing value: me.iseunghan.testjpa.domain.MemberTeam@5f01361e, mergedContextConfiguration = [WebMergedContextConfiguration@d3957fe testClass = MemberTeamRepositoryTest, locations = '{}', classes = '{class me.iseunghan.testjpa.TestJpaApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4d14b6c2, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@25df00a0, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@1be2019a, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@28cda624, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@14bdbc74, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@11dc3715], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]

java.lang.AssertionError: 
[2] Expecting an empty Optional but was containing value: me.iseunghan.testjpa.domain.MemberTeam@5f01361e
  • [1]: MemberTeam์˜ member๋Š” Immutable์ด๋ผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒฝ๊ณ ์™€ ํ•จ๊ป˜ Update ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
  • [2]: ๊ฐ์ฒด์ƒ์œผ๋กœ๋Š” ๋ณ€๊ฒฝ๋์ง€๋งŒ, JPA์ชฝ์—์„œ ๋ณ€๊ฒฝ์„ ๋ง‰์•„ ์‹ค์ œ DB์—๋Š” ๋ฐ˜์˜์ด ๋˜์งˆ ์•Š์•„์„œ (1)์€ ํ†ต๊ณผํ•˜๊ณ  (3)์€ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ์—ฌ๊ธฐ์„œ ์šฐ๋ฆฌ๋Š” ๋‹ค์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ณตํ•ฉํ‚ค๋Š” Immutable์ด๋ผ ์ˆ˜์ •์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • ๊ฐ์ฒด ์ƒ์œผ๋กœ ์ˆ˜์ •์ด ๋์ง€๋งŒ JPA์—์„œ๋Š” Update๋ฅผ ๋ง‰๋Š”๋‹ค.

๋งŒ์•ฝ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ์„ ํ•จ๊ป˜ ๋ณ€๊ฒฝ ์‹œ๋„ํ•œ๋‹ค๋ฉด?

@Commit
@Transactional
@Test
void MemberTeam์„_๋ณ€๊ฒฝํ• ์ˆ˜์žˆ๋‹ค() throws Exception {
    System.out.println("-----------------------------------");
    // given
    MemberTeam memberTeam = memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id)).orElseThrow();
    Member member2 = memberRepository.findById(member2Id).orElseThrow();

    // when
    memberTeam.changeText("edit");
    memberTeam.changeMember(member2);
    MemberTeam modifiedMemberTeam = memberTeamRepository.saveAndFlush(memberTeam);

    // then
    assertThat(modifiedMemberTeam.getId().getMemberId()).isEqualTo(member2Id);
    assertThat(memberTeamRepository.count()).isEqualTo(2);
    assertThat(memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id))).isNotPresent();
}

์‹คํ–‰๊ฒฐ๊ณผ

2023-07-28 14:44:30.248  WARN 90834 --- [1]         main] o.h.p.entity.AbstractEntityPersister     : HHH000502: The [member] property of the [me.iseunghan.testjpa.domain.MemberTeam] entity was modified, but it won't be updated because the property is immutable.
2023-07-28 14:44:30.248 DEBUG 90834 --- [2]         main] org.hibernate.SQL                        : 
    update
        member_team 
    set
        text=? 
    where
        member_id=? 
        and team_id=?
2023-07-28 14:44:30.248 TRACE 90834 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [edit]
2023-07-28 14:44:30.249 TRACE 90834 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [3]
2023-07-28 14:44:30.249 TRACE 90834 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
2023-07-28 14:44:30.250 DEBUG 90834 --- [3]         main] org.hibernate.SQL                        : 
    select
        memberteam0_.member_id as member_i1_1_0_,
        memberteam0_.team_id as team_id2_1_0_,
        memberteam0_.text as text3_1_0_ 
    from
        member_team memberteam0_ 
    where
        memberteam0_.member_id=? 
        and memberteam0_.team_id=?
2023-07-28 14:44:30.251 TRACE 90834 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [3]
2023-07-28 14:44:30.252 TRACE 90834 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2023-07-28 14:44:30.253 DEBUG 90834 --- [           main] .l.e.p.AbstractLoadPlanBasedEntityLoader : Done entity load : me.iseunghan.testjpa.domain.MemberTeam#me.iseunghan.testjpa.domain.MemberTeamId@3e3
2023-07-28 14:44:30.259 DEBUG 90834 --- [           main] cResourceLocalTransactionCoordinatorImpl : JDBC transaction marked for rollback-only (exception provided for stack trace)
...
[4] org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [me.iseunghan.testjpa.domain.MemberTeam] with identifier [me.iseunghan.testjpa.domain.MemberTeamId@3e3]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [me.iseunghan.testjpa.domain.MemberTeam#me.iseunghan.testjpa.domain.MemberTeamId@3e3]
  • [1]: ์œ„์—์„œ ๋ดค๋˜ Id๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค๊ณ  ๊ฒฝ๊ณ ํ•˜๋Š” ๋กœ๊ทธ. ๊ฐ์ฒด์˜ memberId๋Š” ๋ณ€๊ฒฝ๋˜์—ˆ์ง€๋งŒ ์‹ค์ œ DB์—๋Š” ๋ฐ˜์˜ ์•ˆ๋จ.
  • [2]: text๋ฅผ ์ˆ˜์ •ํ•œ update ์ฟผ๋ฆฌ ๋ฐœ์ƒ.
  • [3]: save ์ „ MemberTeamId๋กœ ๋จผ์ € ์กฐํšŒ
  • [4]: update๋ฌธ์—๋Š” member๋ฅผ ๋ณ€๊ฒฝํ•œ ๋ถ€๋ถ„์€ JPA์— ์˜ํ•ด ์ œ์™ธ๋์ง€๋งŒ, ์ด๋ฏธ MemberTeamId๋Š” ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ณด๋‹ˆ JPA๋Š” 1์ฐจ์บ์‹œ์— ์žˆ๋Š” ์Šค๋ƒ…์ƒท๊ณผ ๋น„๊ตํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ  ์˜ˆ์™ธ๋ฅผ ํ„ฐ๋œจ๋ฆฌ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์€?

  • ๊ฐ€์žฅ ํ˜„๋ช…ํ•œ ๋ฐฉ๋ฒ•์€ ๋ณตํ•ฉํ‚ค๋Š” ์ ˆ๋Œ€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด ๊ธฐ์กด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • AutoIncrement๋ฅผ ๊ธฐ๋ณธํ‚ค๋กœ ์„ค์ •ํ•˜๊ณ  ๋ณตํ•ฉํ‚ค๋ฅผ ๋Œ€์ฒดํ‚ค๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

1๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ ๊ทธ๋ƒฅ ์‚ญ์ œํ•˜๊ณ  ๋‹ค์‹œ ์ƒ์„ฑํ•˜๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์‹ค์Šต์€ ๊ฑด๋„ˆ๋›ฐ๊ณ , 2๋ฒˆ์งธ ๋ฐฉ๋ฒ•์„ ์‹ค์Šต์„ ํ†ตํ•ด ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

MemberTeamId๋ฅผ ๋Œ€์ฒดํ‚ค๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ

AutoIncrement ๊ฐ’์„ PK๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , member์™€ team์„ ๊ฐ™์ด ๋ฌถ์–ด์„œ unique ์ œ์•ฝ์„ ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์ด์ œ PK๊ฐ€ ๋ณ€๊ฒฝ๋  ์ผ์€ ์—†์„ ๊ฒƒ ์ž…๋‹ˆ๋‹ค.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"MEMBER_ID", "TEAM_ID"})})
public class MemberTeam {
    @Id @GeneratedValue
    private Long candidateId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID", nullable = false)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID", nullable = false)
    private Team team;

    private String text;

    public MemberTeam(Member member, Team team) {
        this.member = member;
        this.team = team;
    }

    public void changeMember(Member member) {
        this.member = member;
    }

    public void changeText(String text) {
        this.text = text;
    }
}

๋ณ€๊ฒฝ๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

@Test
void MemberTeam์„_๋ณ€๊ฒฝํ• ์ˆ˜์žˆ๋‹ค() throws Exception {
    System.out.println("-----------------------------------");
    // given
    MemberTeam memberTeam = memberTeamRepository.findById(memberTeam1CandidateId).orElseThrow();
    Member member2 = memberRepository.findById(member2Id).orElseThrow();

    // when
    memberTeam.changeText("edit");
    memberTeam.changeMember(member2);
    MemberTeam modifiedMemberTeam = memberTeamRepository.saveAndFlush(memberTeam);

    // then
    assertThat(modifiedMemberTeam.getMember().getId()).isEqualTo(member2Id);
    assertThat(memberTeamRepository.count()).isEqualTo(2);
    assertThat(memberTeamRepository.findById(MemberTeamId.of(team1Id, member1Id))).isNotPresent();
}

์‹คํ–‰๊ฒฐ๊ณผ

2023-07-28 15:50:22.333 DEBUG 95256 --- [1]         main] org.hibernate.SQL                        : 
    update
        member_team 
    set
        member_id=?,
        team_id=?,
        text=? 
    where
        candidate_id=?
2023-07-28 15:50:22.335 TRACE 95256 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [3]
2023-07-28 15:50:22.335 TRACE 95256 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2023-07-28 15:50:22.335 TRACE 95256 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [edit]
2023-07-28 15:50:22.335 TRACE 95256 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [5]
  • [1]: ์ •์ƒ์ ์œผ๋กœ update Query๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Outro

JPA๋Š” ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ฐ์ฒด์ง€ํ–ฅ์ ์œผ๋กœ DB๋ฅผ ํ•ธ๋“ค๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ด์ง€๋งŒ ๊ทธ๋งŒํผ ์ถ”์ƒํ™”๋˜์–ด์žˆ๊ณ  ๋‚ด๋ถ€๋™์ž‘์›๋ฆฌ๋ฅผ ์ž˜ ์•Œ์•„์•ผ ์‹ค๋ฌด์—์„œ ์žฅ์• ๋กœ ์ด์–ด์ง€์ง€ ์•Š๊ณ  ์ข‹์€ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. JPA์— ๋Œ€ํ•ด์„œ ๊นŠ๊ฒŒ ํ•™์Šตํ•˜๋Š”๋ฐ ์ง‘์ค‘์„ ํ•ด์•ผํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

(ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๋ง์”€ํ•ด์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค)

๋ฐ˜์‘ํ˜•