학과 조회 API 작성

송선권·2023년 12월 31일
0
post-thumbnail

객체의 필드 객체의 필드를 기반으로 조회한다.

이번 프로젝트에서는 Spring Data JPA를 사용하는데, 기존에 정의된 Repository<T, ID>를 상속하면 save나 findAll 등의 메서드를 별도로 정의해주지 않아도 해당 라이브러리가 메서드명을 기반으로 쿼리를 자동으로 작성하여 동작시켜준다. 이는 보통 findById(Long id)와 같이 선언하여 객체의 필드(id)를 기반으로 객체를 찾는 형태로 많이 사용된다. 이번에는 객체의 필드 객체 속 필드를 기반으로 조회가 가능한지 찾아보았는데, findByShopId(Long id)와 같이 선언하면 shop을 필드로 갖는 객체 중에 shopid를 기준으로 조회할 수 있었다. 즉, 필드 객체 속 필드를 기반으로 조회가 가능했다.

Duplicate row was found and 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 관계로 수정하였다.

Unknown column 'd2_0.name' in 'on clause'

@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")에서 referencedColumnNamedept_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 방법을 도입했고, 그 과정을 간단하게 알아보겠다.

우선 키가 될 칼럼들을 모아둔 새로운 클래스를 정의한다. 이 클래스는 아래 조건들이 충족되어야 한다.

  • public 클래스일 것
  • 기본 생성자 필수
  • 엔티티 클래스에서 작성한 필드 명과 동일하게 작성할 것 (컬럼명이 아님에 주의)
  • Serializable을 구현해야 함
  • equals와 hashCode를 구현해야 함
@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;
    ...
}

참고: JPA 복합키 사용하기 (@IdClass)

0개의 댓글