[java] JPA 값 콜렉션 매핑 By starseat 2022-11-07 23:49:54 java/spring Post Tags # JPA 값 콜렉션 Set ## 단순 값 Set 매핑 ![image.png](/uploads/_temp/20221107/16e03c86e3f18e64dceb2b5bb90f4fa2.png) ```java @Entity @Table(name = "role") public class Role { @Id private String id; private String name; @ElementCollection @CollectionTable( name = "role_perm", joinColumns = @JoinColumn(name = "role_id") ) @Column(name = "perm") private Set permissions = new HashSet<>(); ... ``` ### 저장 ```java Role role = new Role(roleId, "관리자", Set.of("F1", "F2")); em.persist(role); ``` ```sql insert into role (name, id) values (?, ?) insert into role_perm (role_id, perm) values (?, ?) insert into role_perm (role_id, perm) values (?, ?) ``` ### 조회 (lazy) - 연관된 테이블 데이터가 **필요할 때(사용하려고 할 때)** 나중에 조회 ```java Role role = em.find(Role.class, roleId); for(String perm : role.getPermissions()) { log.info("perm: {}", perm); } ``` - Role role = `em.find(Role.class, roleId)` 부분에서 다음 SQL 실행 ```sql select r1_0.id, r1_0.name from role r1_0 where r1_0.id = ? ``` - for(String perm : `role.getPermissions()`) 부분에서 다음 SQL 실행 ```sql select p1_0.role_id, p1_0.perm from role_perm p1_0 where p1_0.role_id = ? ``` ### 조회 (eager) - 위의 `Lazy` 조회처럼 필요할 때 조회가 아닌 `즉시 조회` - @ElementCollection(`fetch = FetchType.EAGER`) 방법으로 설정 ```java @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(...) @Column(name = " perm") private Set permissions = new HashSet<>(); ``` ```java Role role = em.find(Role.class, roleId); for (String perm : role.getPermissions()) { log.info("perm: {}", perm); } ``` - 아래 방법처럼 한번에 조회 ```sql select r1_0.id, r1_0.name, p1_0.role_id, p1_0.perm from role r1_0 left join role_perm p1_0 on r1_0.id = p1_0.role_id where r1_0.id = ? ``` ### Set 수정: add(), remove() ```java Role role = em.find(Role.class, roleId); role.getPermissions().add("F3"); role.getPermissions().remove("F1"); ``` ```sql delete from role_perm where role_id = ? and perm = ? insert into role_perm (role_id, perm) values (?, ?) ``` ### Set 새로 할당 ```java Role role = em.find(Role.class, roleId); role.setPermissions(Set.of("F4", "F5")); ``` ```java // Role.setPermissions() public void setPermissions(Set newPermissions) { this.permissions = newPermissions; } ``` - 새로 할당하면 아래처럼 delete 후 insert 진행 ```sql delete from role_perm where role_id = ? insert into role_perm (role_id, perm) values (?, ?) insert into role_perm (role_id, perm) values (?, ?) ``` ### Set clear() ```java Role role = em.find(Role.class, roleId); role.revokeAll(); ``` ```java // Role.revokeAll() public void revokeAll() { this.permissions.clear(); // Set 데이터에 접근 } ``` ```sql select p1_0.role_id, p1_0.perm from role_perm p1_0 where p1_0.role_id = ? delete from role_perm where role_id = ? ``` ## Embeddable 타입 Set ![image.png](/uploads/_temp/20221108/d31c0dd37211a5652c1607ef52aca2d4.png) ```java @Entity @Table(name = "role") public class Role2 { @Id private String id; private String name; @ElementCollection @CollectionTable( name = "role_perm" joinColumns = @JoinColumn(name = "role_id") ) private Set permissions = new HashSet<>(); ... } ``` ```java @Embeddable public class GrantedPermission { @Column(name = "perm") private String permission; private String grantor; } ``` # JPA 값 콜렉션 List ![image.png](/uploads/_temp/20221108/82786248321f97d9bb1fffaf1473d83a.png) - Set 과 사용 방식은 거의 동일하나 `@OrderColumn` 을 사용하는게 다름 ```java @Entity @Table(name = "question") public class Question { @Id private String id; private String text; @ElementCollection @CollectionTable( name = "question_choice", joinColumns = @JoinColumn(name = "question_id") ) @OrderColumn(name = "idx") // Set 과의 차이점, List 의 index @Column(name = "text") // question_choice 의 text 컬럼 private List choices; ... } ``` ## 저장 ```java Question q = new Question(id, "질문", List.of("보기 1", "보기 2")); em.persist(q); ``` ```sql insert into question (text, id) values (?, ?) insert into question_choice (question_id, idx, text) values (?, ?, ?) // "A01", 0, "보기 1" insert into question_choice (question_id, idx, text) values (?, ?, ?) // "A01", 1, "보기 2" ``` ## 조회 ```java Question q = em.find(Question.class, id); log.info("보기 개수: {}, q.getChoices().size()); ``` ### lazy 일 경우 - `em.find(Question.class, id)` 시 아래 `select` 실행 ```sql select q1_0.id, q1_0.text from question q1_0 where q1_0.id = ? ``` - `q.getChoices().size()` 시 아래 `select` 실행 ```sql select c1_0.question_id, c1_0.idx, c1_0.text from question_choice c1_0 where c1_0.question_id = ? ``` ### eager 일 경우 - `@ElementCollection(fetch = FetchType.EAGER)` 설정 필요 - lazy 와는 다르게 `left join` 으로 한번에 가져옴 ```sql select q1_0.id, c1_0.question_id, c1_0.idx, c1_0.text, q1_0.text from question q1_0 left join question_choice c1_0 on q1_0.id = c1_0.question_id where q1_0.id = ? ``` ## List 수정 -> 새로 할당 - 모두 지우고 새롭게 insert ```java Question q = em.find(Question.class, id); q.setChoices(List.of("답 1", "답 2")); ``` ```java public void setChoices(List choices) { this.choices = choices; } ``` ```sql delete from question_choice where question_id = ? insert into question_choice (question_id, idx, text) valeus (?, ?, ?) insert into question_choice (question_id, idx, text) valeus (?, ?, ?) ``` ## 엔티티 삭제 - 연관된 테이블의 데이터 삭제 후 원본 테이블도 삭제 ```java Question q = em.find(Question.class, id); em.remove(q); ``` ```sql delete from question_choice where question_id = ? delete from question where id = ? ``` ## Embeddable 타입 List ![image.png](/uploads/_temp/20221108/a61e3d85924bb430cf013e6ec6f6832e.png) ### 매핑 설정 ```java @Entity @Table(name = "question") public class Question2 { @Id private String id; private String text; @ElementCollection @CollectionTable( name = "question_choice", joinColumns = @JoinColumn(name = "question_id") ) @OrderColumn(name = "idx") private List choices; ... } ``` ```java @Embeddable public class Choice { private String text; private Boolean input; ... } ``` # JPA 값 콜렉션 Map ![image.png](/uploads/_temp/20221108/e64a6f5ab19df975e8ddf1415a9854ee.png) - Set, List 와 사용 방식은 거의 동일하나 `@MapKeyColumn` 을 사용하는게 다름 ```java @Entity @Table(name = "doc") public class Document { @Id private String id; private String title; private String content; @ElementCollection @CollectionTable( name = "doc_prop", joinColumns = @JoinColumn(name = "doc_id") ) @MapKeyColumn(name = "name") // doc_prop 테이블의 name 컬럼, java Map 의 Key @Column(name = "value") // doc_prop 테이블의 value 컬럼, java Map 의 value private Map props = new HashMap<>(); ... } ``` ## 저장 ```java Map props = new HashMap<>(); props.put("p1", "v1"); props.put("p2", "v2"); Document doc = new Document(id, "제목", "내용", props); em.persist(doc); ``` ```sql insert into doc (content, title, id) values (?, ?, ?) insert into doc_prop (doc_id, name, value) values (?, ?, ?) // "DOC1", "p1", "v1" insert into doc_prop (doc_id, name, value) values (?, ?, ?) // "DOC1", "p2", "v2" ``` ## Map 에 값 추가/수정/삭제 - 위의 Set, List 와 동일 ## Embeddable 타입 Map ![image.png](/uploads/_temp/20221108/515305345c5f0dd1835fa04d3f3839f7.png) ### 매핑 설정 ```java @Entity @Table(name = "doc") public class Document { @Id private String id; private String title; private String content; @ElementCollection @CollectionTable( name = "doc_prop", joinColumns = @JoinColumn(name = "doc_id") ) @MapKeyColumn(name = "name") private Map props = new HashMap<>(); ... } ``` ```java @Embeddable @Access(AccessType.FIELD) public class PropValue { private String value; private Boolean enabled; ... } ``` # 콜렉션 주의 사항 - `CQRS`(Command Query Responsibility Segregation) 도입 고려 - 변경 기능(CUD)을 위한 모델과 조회 기능을 위한 모델 분리 - 변경 기능(CUD) - JAP 활요 - 조회 기능 - MyBatis/jdbcTemplate/JPA 중 알맞은 기술 사용 - 모든 기능을 JPA로 구현할 필요 없음 - 특히 목록, 상세와 같은 조회 기능 # 참조 - [최범균님의 값 콜렉션 Set 매핑](https://www.youtube.com/watch?v=lQ4-kVeHVGk) - [최범균님의 값 콜렉션 List 매핑](https://www.youtube.com/watch?v=Wq4B5RpIeAY) - [최범균님의 값 콜렉션 Map 매핑](https://www.youtube.com/watch?v=CPIgicoqLnM) - [최범균님의 콜렉션 주의사항](https://www.youtube.com/watch?v=yK4Avtxqz-k) Previous Post [java] JPA Embeddable Next Post [java] JPA 영속 컨텍스트 & 라이프사이클