back end/java

JPA에서 복합키를 사용하는 방법

노루아부지 2022. 10. 18. 00:25
반응형

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를 사용했을 때의 결과를 선호합니다.

여러분들은 어떠신가요?

728x90
반응형