JPA에서 복합키를 사용하는 방법 - 사전작업
먼저, 예제를 포스팅 하기 전에 다음 코드를 미리 작성합니다.
@Entity
@Getter
@Setter
@Table(name = "t_user")
public class User {
@Id
private String userId;
private String userName;
}
@Entity
@Getter
@Setter
@Table(name = "t_user_group")
public class UserGroup {
@Id
private String groupId;
private String groupName;
}
그다음 UserGroupMember라는 관계 테이블을 나타내는 Entity class를 생성합니다.
@Data
@Entity
@NoArgsConstructor
public class UserGroupMember {
@Id
private String groupId;
@Id
private String userId;
}
그다음 JpaRepository의 구현체인 UserGroupMemberRepository를 생성해야 하는데, 무엇인가 이상합니다.

ID에 하나밖에 입력할 수 없는데 UserGroupMember는 복합키이기 대문에 입력해야 할 것이 2개입니다.
이럴 때는 어떻게 해야 할까요?
JPA Entity에서 복합키 사용방법
복합키를 사용하기 위해서는 먼저 ID class를 생성해야 합니다.
ID class는 반드시 Serializable의 구현체이어야 합니다.
@Data
public class UserGroupMemberKey implements Serializable {
private String groupId;
private String userId;
}
JPA Entity에서 복합키(composite key)를 사용하는 방법은 두 가지 방법이 있습니다.
1) JPA에서 복합키를 사용하는 방법 - @IdClass annotation 사용
먼저, UserGroupMember class에 @IdClass를 사용합니다.
@Data
@Entity
@NoArgsConstructor
@IdClass(UserGroupMemberKey.class)
public class UserGroupMember {
@Id
private String groupId;
@Id
private String userId;
}
그다음 UserGroupMemberRepository에 UserGroupMemberKey class를 지정합니다.
public interface UserGroupMemberRepository
extends JpaRepository<UserGroupMember, UserGroupMemberKey> {
}
그 다음 Controller에 다음과 같은 코드를 작성합니다.
package com.example.demo;
import com.example.demo.domain.UserGroupMember;
import com.example.demo.domain.UserGroupMemberKey;
import com.example.demo.domain.UserGroupMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
private final UserGroupMemberRepository userGroupMemberRepository;
@RequestMapping("/addUserToGroup")
public void addUserToGroup() {
UserGroupMember userGroupMember = new UserGroupMember();
userGroupMember.setUserId("hong");
userGroupMember.setGroupId("1");
userGroupMemberRepository.save(userGroupMember);
}
@RequestMapping("/get")
public UserGroupMember get() {
UserGroupMemberKey key = new UserGroupMemberKey("1", "hong");
Optional<UserGroupMember> opt =
userGroupMemberRepository.findById(key);
return opt.orElseGet(UserGroupMember::new);
}
}
이제 /addUserToGroup을 호출한 후, /get을 호출하면 정상적으로 결과값이 출력되는 것을 볼 수 있습니다.
이때 이런 의문이 생깁니다.
UserGroupMemberKey 클래스는 과연 필요한가?
의문을 해소하기 위해 다음과 같이 코드를 변경합니다.
먼저, UserGroupMember의 @IdClass를 자신으로 변경합니다.
@IdClass로 지정된 클래스는 Serializable의 구현체 이어야 하기 때문에 추가합니다.
package com.example.demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import java.io.Serializable;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@IdClass(UserGroupMember.class)
public class UserGroupMember implements Serializable {
@Id
private String groupId;
@Id
private String userId;
}
UserGroupMemberRepository의 key 부분도 자기 자신으로 변경합니다.
package com.example.demo.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserGroupMemberRepository
extends JpaRepository<UserGroupMember, UserGroupMember> {
}
Controller의 findById 부분도 변경합니다.
@RequestMapping("/get")
public UserGroupMember get() {
Optional<UserGroupMember> opt =
userGroupMemberRepository.findById(new UserGroupMember("1", "hong"));
return opt.orElseGet(UserGroupMember::new);
}
여기까지 하고 실행하면 정상적으로 결과가 똑같이 출력되는 것을 볼 수 있습니다.
단, 여기서 주의사항이 있습니다.
자기 자신을 @IdClass로 지정했을 경우 @Id 외의 다른 변수가 존재하면 안 됩니다.
예를 들어 다음과 같이 @Id가 아닌 일반 Column인 desc를 추가합니다.
package com.example.demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import java.io.Serializable;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@IdClass(UserGroupMember.class)
public class UserGroupMember implements Serializable {
@Id
private String groupId;
@Id
private String userId;
@Column
private String desc;
}
package com.example.demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import java.io.Serializable;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@IdClass(UserGroupMember.class)
public class UserGroupMember implements Serializable {
@Id
private String groupId;
@Id
private String userId;
@Column
private String memberDesc;
public UserGroupMember(String groupId, String userId) {
this.groupId = groupId;
this.userId = userId;
}
}
그다음 /get을 호출하면 어떻게 될까요?
다음과 같이 결과값이 모두 null이 표시됩니다.
{
"groupId": null,
"userId": null,
"memberDesc": null
}
그 이유를 확인해보기 위해 query를 확인해보면 다음과 같습니다.
select
usergroupm0_.user_id as user_id1_2_0_,
usergroupm0_.group_id as group_id2_2_0_,
usergroupm0_.member_desc as member_d3_2_0_
from user_group_member usergroupm0_
where usergroupm0_.user_id=?
and usergroupm0_.group_id=?
and usergroupm0_.member_desc=?
이와 같이 코드에서는 user_id, group_id만 입력했고, Entity class에서도 user_id, group_id만 @Id로 지정했는데 조회할 때 member_desc까지 PK로 취급하여 조회하는 것을 볼 수 있습니다.
2) JPA에서 복합키를 사용하는 방법 - @Embeddable, @EmbeddedId 사용
먼저, UserGroupMemberKey에 @Embeddable을 사용합니다.
package com.example.demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class UserGroupMemberKey implements Serializable {
private String groupId;
private String userId;
}
그다음 UserGroupMember에 @EmbeddedId를 사용합니다.
package com.example.demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class UserGroupMember {
@EmbeddedId
private UserGroupMemberKey id;
private String memberDesc;
}
이어서 Repository와 Controller도 다음과 같이 변경합니다.
package com.example.demo.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserGroupMemberRepository
extends JpaRepository<UserGroupMember, UserGroupMemberKey> {
}
package com.example.demo;
import com.example.demo.domain.UserGroupMember;
import com.example.demo.domain.UserGroupMemberKey;
import com.example.demo.domain.UserGroupMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
private final UserGroupMemberRepository userGroupMemberRepository;
@RequestMapping("/addUserToGroup")
public void addUserToGroup() {
UserGroupMember userGroupMember = new UserGroupMember();
UserGroupMemberKey id = new UserGroupMemberKey("1", "hong");
userGroupMember.setId(id);
userGroupMember.setMemberDesc("test");
userGroupMemberRepository.save(userGroupMember);
}
@RequestMapping("/get")
public UserGroupMember get() {
Optional<UserGroupMember> opt =
userGroupMemberRepository.findById(new UserGroupMemberKey("1", "hong"));
return opt.orElseGet(UserGroupMember::new);
}
}
다시 /addUserToGroup를 호출한 후 /get을 호출하면 다음과 같이 정상적으로 결과가 표시되는 것을 확인할 수 있습니다.
{
"id": {
"groupId": "1",
"userId": "hong"
},
"memberDesc": "test"
}
눈치 채신 분도 있으시겠지만 @IdClass를 사용했을 때와 결과값이 다르게 표시됩니다.
@IdClass를 사용했을 때의 결과 값은 다음과 같습니다.
{
"groupId": "1",
"userId": "hong",
"memberDesc": "test"
}
개인의 선호도에 따라 다르겠지만, 저는 @IdClass를 사용했을 때의 결과를 선호합니다.
여러분들은 어떠신가요?
'back end > java' 카테고리의 다른 글
[java] JSON string을 이쁘게 출력하는 방법 - json string pretty print (0) | 2022.10.23 |
---|---|
[Spring Boot] DB설정 없이 실행하는 방법 (0) | 2022.10.21 |
[java] List를 문자열로 Join하는 방법 (0) | 2022.10.17 |
spring pageable를 이용한 페이징 처리 (0) | 2022.10.13 |
[Spring boot] Restful API 쉽게 호출하는 방법 by using RestTemplate (2) | 2022.10.11 |