이번 프로젝트에서는 Spring Data JPA를 사용하는데, 기존에 정의된 Repository<T, ID>를 상속하면 save나 findAll 등의 메서드를 별도로 정의해주지 않아도 해당 라이브러리가 메서드명을 기반으로 쿼리를 자동으로 작성하여 동작시켜준다. 이는 보통 findById(Long id)
와 같이 선언하여 객체의 필드(id
)를 기반으로 객체를 찾는 형태로 많이 사용된다. 이번에는 객체의 필드 객체 속 필드를 기반으로 조회가 가능한지 찾아보았는데, findByShopId(Long id)
와 같이 선언하면 shop
을 필드로 갖는 객체 중에 shop
의 id
를 기준으로 조회할 수 있었다. 즉, 필드 객체 속 필드를 기반으로 조회가 가능했다.
ASSERT
was specified@Entity
@Table(name = "dept_infos")
public class Dept {
@Id
@Column(name = "name")
private String name;
@Column(name = "curriculum_link")
private String curriculumLink;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "name", referencedColumnName = "name")
private DeptNum deptNum;
}
@Entity
@Table(name = "dept_nums")
public class DeptNum {
@Column(name = "dept_name")
private String name;
@Column(name = "dept_num")
private Long number;
}
현재 구조는 dept_infos
테이블과 dept_nums
테이블로 이루어져 있으며, OneToOne 단방향 연관관계로 Dept가 연관관계의 주인이다. 이렇게 실행하면 조회가 정상적으로 이루어지지만 하나의 name에 대응하는 number가 여럿이면 다음 에러가 발생한다.
Duplicate row was found and ASSERT was specified
OneToOne인데 대상이 둘 이상 발견되었다는 에러이다. 실제로 DB를 보면 "컴퓨터공학부"가 두 값을 가지고 있다.
이 문제를 해결하기 위해 OneToOne을 OneToMany로 바꿔 1:1 관계에서 1:n 관계로 수정하였다.
@Entity
@Table(name = "dept_infos")
public class Dept {
@Id
@Column(name = "name")
private String name;
@Column(name = "curriculum_link")
private String curriculumLink;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "name", referencedColumnName = "name")
private DeptNum deptNum;
}
@Entity
@Table(name = "dept_nums")
public class DeptNum {
@Column(name = "dept_name")
private String name;
@Column(name = "dept_num")
private Long number;
}
위 코드를 기반으로 Optional<Dept> findByDeptNumNumber(Long number)
을 실행해보았다.(멤버 변수들 중 deptNum 필드의 number 필드 값이 일치하는 Dept 객체를 반환받는다.) 하지만 여기서 에러가 발생했다.
Unknown column 'd2_0.name' in 'on clause'
Hibernate로 찍힌 로그를 살펴보면 실행된 쿼리는 다음과 같다.
select
d1_0.name,
d1_0.curriculum_link,
d1_0.is_deleted
from
dept_infos d1_0
left join
dept_nums d2_0
on d1_0.name=d2_0.name
where
d2_0.dept_num=?
on 절의 d2_0.name
, 즉 DeptNum의 name 칼럼을 찾을 수 없다는 에러이다.
CREATE TABLE `dept_nums` (
`dept_name` varchar(45) COLLATE utf8mb4_bin NOT NULL,
`dept_num` varchar(5) COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`dept_name`,`dept_num`),
KEY `idx_dept_num` (`dept_num`),
CONSTRAINT `fk_dept_name` FOREIGN KEY (`dept_name`) REFERENCES `dept_infos` (`name`) ON DELETE NO ACTION ON UPDATE CASCADE
)
CREATE TABLE `dept_nums` (
`dept_name` varchar(45) COLLATE utf8mb4_bin NOT NULL,
`dept_num` varchar(5) COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`dept_name`,`dept_num`),
KEY `idx_dept_num` (`dept_num`),
CONSTRAINT `fk_dept_name` FOREIGN KEY (`dept_name`) REFERENCES `dept_infos` (`name`) ON DELETE NO ACTION ON UPDATE CASCADE
)
위 DDL이 두 테이블의 정의문이다.
@JoinColumn(name = "name", referencedColumnName = "name")
에서 referencedColumnName
을 dept_name
으로 수정해보았지만 당연히 매핑이 불가능하다는 에러가 발생했다.
A '@JoinColumn' references a column named 'dept_name' but the target entity 'in.koreatech.koin.domain.dept.domain.Dept' has no property which maps to this column
두 테이블의 연관관계는 위와 같다. dept_nums
테이블의 dept_name
이 FK이기 때문에 연관관계의 주인을 dept_infos
에서 dept_nums
로 수정했다. 그리고 양쪽에서 참조하기 위해 양방향 연관관계를 가지도록 수정했다.
dept_nums
에서 FK 칼럼을 필드로 가지지 않도록 수정하는 대신 연관관계 칼럼을 추가했다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_name")
private Dept dept;
그리고 dept_nums
가 연관관계의 주인으로 바뀌었기 때문에 dept_infos
에서는 @JoinColumn
이 제거되었다.
@OneToMany(mappedBy = "dept")
private Set<DeptNum> deptNums = new LinkedHashSet<>();
이렇게 연관관계 매핑 관련 코드를 수정하니 정상 동작을 확인할 수 있었다.
마지막으로 복합키 매핑을 진행했다.
사실 dept_nums
는 두 칼럼 모두 PK로 이루어져 있다. 하지만 기존 코드는 number 칼럼만 PK로 지정되어 있다.
@Id
@Column(name = "dept_num")
private Long number;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_name")
private Dept dept;
JPA에서는 @Id
를 명시함으로써 PK를 지정할 수 있는데, 하나의 엔티티에 하나의 @Id
만 명시할 수 있었다. 하지만 그렇다고 PK를 하나만 둘 수 있는 것은 아니다. PK가 여럿인 경우를 복합키라고 하는데, JPA에서 복합키 매핑을 하는 방법에는 @EmbeddedId
를 사용하는 방법과 @IdClass
를 사용하는 방법이 있다. 나는 @IdClass
방법을 도입했고, 그 과정을 간단하게 알아보겠다.
우선 키가 될 칼럼들을 모아둔 새로운 클래스를 정의한다. 이 클래스는 아래 조건들이 충족되어야 한다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
public class DeptNumId implements Serializable {
@Column(name = "dept_num", nullable = false)
private Long number;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_name")
private Dept dept;
}
다음으로 복합키를 두고자 하는 클래스에 @IdClass(~~.class)
로 정의해둔 복합키 클래스를 지정한다.
@Entity
@Table(name = "dept_nums")
@IdClass(DeptNumId.class)
public class DeptNum { ... }
마지막으로 해당 클래스에 PK로 사용하고자 하는 모든 필드에 @Id
어노테이션을 붙이면 된다. 하지만 내 경우는 FK가 PK이기 때문에 @Id
대신 @MapsId
를 사용해야 한다.
public class DeptNum {
@Id
@Column(name = "dept_num")
private Long number;
@MapsId
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_name")
private DeptInfo deptInfo;
...
}