현재 진행하는 웹 프로젝트에서 각 분류에 따른 체크박스를 구현하고자 함
한 식당이 각 분류에 따른 여러 데이터들을 가지므로 일대다 관계를 생각함
@OneToMany & @ManyToOne으로 엔티티로서 각 분류에 대한 테이블을 운영해도 되지만, 테이블이 많아질 것을 고려하고, 해당 데이터들은 변경될 일이 없는 단순 문자열 데이터들이기 때문에 @CollectionTable 및 @ElementCollection을 활용하고자 함
@Entity
@Getter @Setter(AccessLevel.PRIVATE)
public class Restaurant{
@Id @GeneratedValue
private Long id;
//생략
@ElementCollection
@CollectionTable(name="contain_food_type" , joinColumns = @JoinColumn(name="restaurant_id"))
private Set<String> containFoodTypes = new HashSet<>();
//생략
}
- 컬렉션은 위 사진 분류 중 포함 음식에 해당됨
- 체크박스로부터 체크된 데이터들이 해당 컬렉션에 바인딩되도록 할 것
- 컬렉션을 흔히 사용하는 List가 아닌 Set인 이유에 대해선 바로 뒤에서 설명할 것임
name : DB와 매핑될 테이블 이름joinColumns: 매핑할 외래키 이름 , JPA는 지정된 외래 키가 주인 엔티티(Restaurant)의 PK 따르게 해줌
- JPA는 containFoodTypes 컬렉션에 들어오는 각각의 값을 따로 지정한 이름의 테이블의 칼럼에 매핑하여줌
- 복합 키 중 RESTAURANT_ID는 PK이자 FK로 식당과 연쇄적으로 동작됨
- 즉 모든 칼럼이 복합 키가 되어 동작되니 모든 칼럼이 중복되면 안됨
- 예를 들어 {1 , "생선포함"} , {1 , "생선포함"} 이렇게 데이터가 들어오면 PRIMARY KEY의 UNIQUE 제약 조건에 위배되므로 중복된 데이터가 들어오지 않도록 해줘야됨
create table contain_food_type(
restaurant_id bigint not null,
contain_food_types varchar(255),
);
기본적으로 application.properties에서 spring.jpa.hibernate.ddl-auto=create를 써서 자동으로 위와 같은 DDL 생성해줌
하지만 운영 시에는 해당 설정이 모든 데이터를 지우고 새로 테이블을 생성해서 굉장히 위험하므로 해당 설정을 끄고 개발자가 직접 DDL을 작성해줘야함
따라서 개발자는 위의 형식과 일치하는 DDL로 테이블을 생성해줘야됨
create table contain_food_type(
restaurant_id bigint not null,
contain_food_types varchar(255) not null,
primary key(restaurant_id , contain_food_types),
foreign key(restaurant_id) references restaurant(id)
);
이때 JPA는 restaurant_id와 contain_food_types를 복합 키로 인식하므로 개발자가 직접 복합 키로 지정하고 restaurant_id 같은 경우는 FK로 설정하는 게 좋음
직접 PK와 FK를 지정하지 않는다고 문제는 안되지만, JPA는 기본적으로 그렇게 알고 동작하기 때문에 지정해주는 게 좋음
- 주인 테이블의 PK에 대한 외래 키이자 컬렉션 테이블의 기본 키로 주인 테이블과 동일한 PK를 공유하게 됨(식별 관계)
- 즉 Long으로 id를 지정했으니 bigint 타입이 자동으로 지정되게 됨
- 그에 따라 주인 테이블과 동일한 생명주기에 놓이게 되므로 자동으로 주인테이블에
cascade = CascadeType.ALL , orphanRemoval = true가 설정되는 것
- 주인 엔티티에서 리스트(
containFoodTypes)에 할당했던 값들이 해당 칼럼에 저장되게 됨- 다른 칼럼들과 마찬가지로
@Column(name= "")으로 칼럼명을 지정하지 않았으니 기본 리스트의 이름인containFoodTypes에 DB에서 자주 쓰이는 snake_case의 문법이 자동 적용되어conain_food_types이름으로 매핑되게 됨
JPA가 컬렉션 테이블로 지정한 테이블을 찾지 못할 경우
org.hibernate.exception.SQLGrammarException를 발생시키므로 정확히 테이블을 매핑해줘야함
select * from restaurant r join contain_food_type c on r.id = c.restaurant_id;
- 위와 같은 컬렉션 테이블 조인 쿼리를 JPA가 알아서 어노테이션을 감지하여 해당 조인 쿼리를 자동으로 작성해서 DB에 flush해주긴 함
- 문제는 DataGrip같은 DB 툴에서 쿼리 테스트를 할 경우 일대다 테이블이 DB에 저장된 것이므로 직접 컬렉션 테이블을 조인해줘야 가져올 수 있음
말로만 @CollectionTable과 @ElementCollection을 쓰지 말라고 공부했지 직접 체감해보진 못했기 때문에 프로젝트에 한번 적용해보았다.
하지만 직접 JPA가 만들어주는 DDL과 네이밍컨벤션과 칼럼 타입 복합 키로 PK와 칼럼을 하나로 묶어 기본 키로 지정해줘야하는 것까지 똑같이 DDL을 만들어줘야하는 번거로움이 있었다.
생각해보면 @CollectionTable & @ElementCollection의 취지는 개발자가 편하도록 JPA가 자동으로 주인 테이블의 생명주기와 일치하는 테이블을 별도로 만들어주는 것인데 자동 ddl 설정을 끌 경우 개발자가 별도로 @CollectionTable을 통해 만들어질 DDL을 직접 만들어줘야 하는 것이므로 차라리 일대다 관계의 테이블을 따로 생성해서 운영하는 것이 나을 것 같다는 생각을 하였다.
또 중복을 방지해야한다는 생각을 매번 해주면서 컬렉션을 정의 시 Set으로 일일이 설계하는 것도 개발자에겐 번거롭고 실수로 List로 정의하고 운영 시에 추후에 불러올 Side Effect가 어마무시하다.
굳이 JPA가 인식해주는 형식을 맞춰서 DDL도 개발자가 직접 만들어야할 뿐더러 , 해당 어노테이션을 쓸 경우 수정 삭제도 해주면 성능 상 좋지 않아지고 튜닝하기 어려워지므로 처음부터 번거롭더라도 아싸리 일대다 관계의 테이블을 만들어서 그럴 걱정 없게 설계하자 !