과거 작성한 코드를 보니, 값 타입 컬렉션을 잘못 사용하고 있었다. 왜 잘못 사용했는지 알아보고, 언제 사용하면 좋을지 알아본다.
전체 코드: https://github.com/ji-jjang/Learning/tree/main/Practice/ValueObjectCollection
delete from space_images where space_id=1;
insert into space_images (space_id,image_paths) values (1,'hello');
insert into space_images (space_id,image_paths) values (1,'hello3');
@BatchSize(size = 10)
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "space_images", joinColumns = @JoinColumn(name = "space_id"))
@Column
private List<String> imagePaths = new ArrayList<>();
select
ip1_0.space_id,ip1_0.image_paths
from
space_images ip1_0
where ip1_0.space_id in (1,2,3,4,5,6,7,8,9,10);
@Fetch(FetchMode.SUBSELECT)
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "space_images", joinColumns = @JoinColumn(name = "space_id"))
@Column
private List<String> imagePaths = new ArrayList<>();
select
ip1_0.space_id,ip1_0.image_paths
from
space_images ip1_0
where
ip1_0.space_id in (select s1_0.id from spaces s1_0);
@EntityGraph(attributePaths = {"imagePaths"})
Page<Space> findAll(Pageable pageable);
select
s1_0.id,s1_0.closing_time ...
from
spaces s1_0
left join
space_images ip1_0 on s1_0.id=ip1_0.space_id;
값 컬렉션이 아닌 엔티티를 만들어 일대다 관계로 설정하고, 영속성 전이(cascade) + 고아 객체 제거(orphan remove) 기능을 적용하면 위의 문제를 해결할 수 있다.
@Entity
@Table(name = "questions")
@NoArgsConstructor
@Data
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(
name = "question_choice",
joinColumns = @JoinColumn(name = "question_id")
)
@OrderColumn(name = "idx")
@Column(name = "test")
private List<String> choices;
public Question(String text, List<String> choices) {
this.text = text;
this.choices = choices;
}
}
@Test
@Rollback(value = false)
void saveQuestion() {
Question question = new Question("내용 1", List.of("보기 1", "보기 2", "보기 3"));
questionRepository.save(question);
}
// insert into questions (text) values ('내용 1');
// insert into question_choice (question_id,idx,test) values (1,0,'보기 1');
// insert into question_choice (question_id,idx,test) values (1,1,'보기 2');
// insert into question_choice (question_id,idx,test) values (1,2,'보기 3');
@Test
@Transactional
void findQuestionById() {
Question question = questionRepository.findById(1L).get();
for (var choice : question.getChoices()) {
System.out.println("choice = " + choice);
}
}
// Lazy
// select q1_0.id,q1_0.text from questions q1_0 where q1_0.id=1;
// select c1_0.question_id,c1_0.idx,c1_0.test from question_choice c1_0 where c1_0.question_id=1;
// Eager
// select q1_0.id,c1_0.question_id,c1_0.idx,c1_0.test,q1_0.text from questions q1_0 left join question_choice c1_0 on q1_0.id=c1_0.question_id where q1_0.id=1;
@Test
@Rollback(value = false)
@Transactional
void addAndRemoveChoice() {
Question question = questionRepository.findById(1L).get();
question.getChoices().add("보기 4");
question.getChoices().remove("보기 1");
}
// add 하고, remove을 별도로 실행했을 경우
// insert into question_choice (question_id,idx,test) values (1,3,'보기 4');
// delete from question_choice where question_id=1 and idx=3;
// update question_choice set test='보기 4' where question_id=1 and idx=2;
// update question_choice set test='보기 3' where question_id=1 and idx=1;
// update question_choice set test='보기 2' where question_id=1 and idx=0;
// add와 remove를 동시에 실행했을 경우
// update question_choice set test='보기 4' where question_id=1 and idx=2;
// update question_choice set test='보기 3' where question_id=1 and idx=1;
// update question_choice set test='보기 2' where question_id=1 and idx=0;
@Entity
@Table(name = "roles")
@NoArgsConstructor
@Getter
@Setter
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "role_perm", joinColumns = @JoinColumn(name = "role_id")) // 롤에 있는 id 컬럼을 참조, 조인할 때 사용할 컬럼 지정
@Column(name = "perm") // 실제 값을 가지고 있는 컬럼
private Set<String> permissions = new HashSet<>();
public Role(String name, Set<String> permissions) {
this.name = name;
this.permissions = permissions;
}
}
@Test
@Rollback(value = false)
void saveRole() {
Role role = new Role("관리자", Set.of("권한 1", "권한 2", "권한 3"));
roleRepository.save(role);
// insert into roles (name,id) values ('관리자',default);
// insert into role_perm (role_id,perm) values (1,'권한 3');
// insert into role_perm (role_id,perm) values (1,'권한 2');
// insert into role_perm (role_id,perm) values (1,'권한 1');
}
@Test
@Transactional
void findRoleById() {
Role role = roleRepository.findById(1L).get();
for (var perm : role.getPermissions()) {
System.out.println("perm = " + perm);
}
}
// JPA는 기본적으로 @ManyToOne과 @OneToOne에서는 즉시 로딩, @OneToMany과 @ManyToMany에서는 지연로딩 방식으로 동작한다.
// 값 컬렉션도 @OneToMany로 볼 수 있으므로 기본 전략은 지연 로딩이다.
// select r1_0.id,r1_0.name from roles r1_0 where r1_0.id=1;
// select p1_0.role_id,p1_0.perm from role_perm p1_0 where p1_0.role_id=1;
// @ElementCollection(fetch = FetchType.EAGER)
// Eager 방식: select r1_0.id,r1_0.name,p1_0.role_id,p1_0.perm from roles r1_0 left join role_perm
// p1_0 on r1_0.id=p1_0.role_id where r1_0.id=1;
@Test
@Rollback(value = false)
@Transactional
void addAndRemovePermission() {
Role role = roleRepository.findById(1L).get();
role.getPermissions().add("권한 4");
role.getPermissions().remove("권한 1");
}
// delete from role_perm where role_id=1 and perm='권한 1';
// insert into role_perm (role_id,perm) values (1,'권한 4');
@Entity
@Table(name = "docs")
@NoArgsConstructor
@Data
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ElementCollection
@CollectionTable(
name = "doc_prop",
joinColumns = @JoinColumn(name = "doc_id"))
@MapKeyColumn(name = "name")
@Column(name = "value")
private Map<String, String> props = new HashMap<>();
public Document(String title, String content, Map<String, String> props) {
this.title = title;
this.content = content;
this.props = props;
}
}
@Test
@Rollback(value = false)
public void saveDoc() {
Map<String, String> props = new HashMap<>();
props.put("props1", "value1");
props.put("props2", "value2");
Document doc = new Document("제목 1", "내용 1", props);
documentRepository.save(doc);
}
// insert into docs (content,title) values ('내용 1','제목 1');
// insert into doc_prop (doc_id,name,value) values (1,'props1','value1');
// insert into doc_prop (doc_id,name,value) values (1,'props2','value2');
@Test
@Transactional
void findQuestionById() {
Document document = documentRepository.findById(1L).get();
for (var prop : document.getProps().entrySet()) {
System.out.println(prop.getKey() + ": " + prop.getValue());
}
}
// select d1_0.id,d1_0.content,d1_0.title from docs d1_0 where d1_0.id=1;
// select p1_0.doc_id,p1_0.name,p1_0.value from doc_prop p1_0 where p1_0.doc_id=1;
@Test
@Rollback(value = false)
@Transactional
void addAndRemoveProp() {
Document document = documentRepository.findById(1L).get();
document.getProps().put("props3", "value3");
document.getProps().remove("props1");
}
// fetch
// select d1_0.id,d1_0.content,d1_0.title from docs d1_0 where d1_0.id=1;
// select p1_0.doc_id,p1_0.name,p1_0.value from doc_prop p1_0 where p1_0.doc_id=1;
// delete from doc_prop where doc_id=1 and name='props1';
// insert into doc_prop (doc_id,name,value) values (1,'props3','value3');
// eager
// select d1_0.id,d1_0.content,p1_0.doc_id,p1_0.name,p1_0.value,d1_0.title from docs d1_0 left join doc_prop p1_0 on d1_0.id=p1_0.doc_id where d1_0.id=1;
// delete from doc_prop where doc_id=1 and name='props1';
// insert into doc_prop (doc_id,name,value) values (1,'props3','value3');
자료 구조가 아주 간단하고, 자주 쓰이지 않으면 사용할 수 있지만 대부분의 경우 값 컬렉션 보단 별도의 엔티티를 만들어 사용하는 게 좋다고 생각한다.