본문 바로가기
실습/리눅스 서버 + 스프링 부트

JPA에서 외래키 사용

by 이민우 2022. 12. 13.
728x90
반응형

오늘은 JPA에서 외래키를 사용하는 방법을 작성해놓을까 한다.

 

DB를 설계할 때 테이블간 관계 표현을 위한 외래키 사용은 필수이다. JPA에서는 이러한 외래키를 클래스 안에 클래스를 선언함으로써 사용 가능하도록 기능을 제공하고 있다.

 

아래와 같은 DB가 있을 경우, 다음과 같이 클래스를 생성하면 JPA를 사용한 외래키를 생성할 수 있다.

목표 DB ERD

Parent.java

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name="parent_table")
public class Parent {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="parent_id", nullable=false, columnDefinition="int")
	private int parentId;
	
	@Column(name="parent_name", nullable=false, columnDefinition="varchar(10)")
	private String parentName;
	
}

Child.java

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name="child_table")
public class Child {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="child_id", nullable=false, columnDefinition="int")
	private int childId;
	
	@Column(name="child_name", nullable=false, columnDefinition="varchar(10)")
	private String childName;
	
	@ManyToOne(cascade=CascadeType.REMOVE)
	@JoinColumn(name="parent_id", referencedColumnName="parent_id")
	Parent parent;
}
  • 위에서 @ManyToOne은 현재클래스:목표 클래스 간 N:1 매핑임을 명시한다. cascade, fetchType을 지정할 수 있다.
  • 그리고 @JoinColumn은 외래 클래스와 매핑될 컬럼을 의미한다. name은 해당 테이블에서의 컬럼명, refrencedColumnName은 목표 테이블의 pk이다. 이 외에도 unique=true 와 같은 옵션으로 unique, nullable, insertable, updateable을 설정할 수 있으며, 일반 @Column과 마찬가지로 columnDefinition을 사용할 수 있다.

 

이제 완료되었다면 Repository를 만들어준 후 테스트를 해본다.

테스트에 앞서 application.properties (혹은 yml)에 아래와 같은 설정을 입력한다.

spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=0000


spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

그리고 다음과 같은 테스트 코드를 생성하여 구동시켜보자. 그러면 아래와 같이 Table과 컬럼들이 정상적으로 생성된다.

	@Autowired ParentRepository parentRepo;
	@Autowired ChildRepository childRepo;

	@Test
	void contextLoads() {
		Parent parent = new Parent();
		parent.setParentName("부모1");
		
		parentRepo.save(parent);
		
		Child child = new Child();
		child.setChildName("자식1");
		child.setParent(parent);
		
		childRepo.save(child);
	}

 

그런데 위의 코드에는 문제점이 하나 있다.

만약 child를 생성 후 삭제하면 어떻게 될까? 다음 코드를 구동해보자.

	@Autowired ParentRepository parentRepo;
	@Autowired ChildRepository childRepo;

	@Test
	void contextLoads() {
		Parent parent = new Parent();
		parent.setParentName("부모1");
		
		parentRepo.save(parent);
		
		Child child = new Child();
		child.setChildName("자식1");
		child.setParent(parent);
		
		childRepo.save(child);
		
		childRepo.delete(child);
	}

위 코드 실행 후 DB에 돌아가보면 child 뿐 아니라 parent까지 삭제되었음을 알 수 있다. DELETE Cascade 옵션이 Parent > Child가 아니라 Child > Parent에 걸려있기 때문이다.

게다가 N:1 매핑관계이기에 하나라도 child가 더 존재하면 child의 삭제 자체가 불가능해진다.

 

그리고 만약 부모가 없을 때 자식을 생성하려면 어떻게 될까?

DB를 초기화한 후 아래와 같은 코드를 돌려보았다.

	@Autowired ParentRepository parentRepo;
	@Autowired ChildRepository childRepo;

	@Test
	void contextLoads() {
		Parent parent = new Parent();
		parent.setParentName("부모1");
		
		//제거
		//parentRepo.save(parent);
		
		Child child = new Child();
		child.setChildName("자식1");
		child.setParent(parent);
		
		childRepo.save(child);
	}

위와 같은 에러가 발생하며 삽입에 실패하는 것을 확인할 수 있다.

 

그렇다면 @JoinColumn의 옵션에 insertable을 추가하면 어떨까? 이름만 봤을 때는 insert가 자동으로 될 것 같은 느낌이 들어 실험을 해보았다.

아쉽지만 insertable은 엔티티 저장 시 외래키 필드도 함께 저장한다는 의미였고, 외래키 엔티티가 DB에 없을 때 함께 저장해주는 것이 아니라 읽기 전용이 아닐 때 사용한다. updateable도 마찬가지였다. 그래서 insertable을 추가한다고 부모 엔티티가 함께 저장되지는 않았다.

  • insertable : insert 작업 수행 시 외래키를 함께 저장할 것인지 여부 (읽기 전용일 때 false). 만약 false 지정 시 insert 해도 외래키쪽은 null로 저장된다.
  • updateable : update 작업 수행 시 외래키를 함께 저장할 것인지 여부 (읽기 전용일 때 false). 만약 false 지정 시 update해도 외래키는 수정되지 않는다.

 


다음은 멀티키일 경우의 외래키 선언이다.

멀티키일 경우는 다음과 같이 사용하면 된다.

 

먼저 Parent를 다음과 같이 선언하고, PK 클래스를 만들어주자.

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name="parent_table")
@IdClass(ParentPK.class)
public class Parent {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="parent_id", nullable=false, columnDefinition="int")
	private int parentId;
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="parent_id2", nullable=false, columnDefinition="int")
	private int parentId2;
	
	@Column(name="parent_name", nullable=false, columnDefinition="varchar(10)")
	private String parentName;
}
@Getter
@Setter
@ToString
public class ParentPK implements Serializable {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="parent_id", nullable=false, columnDefinition="int")
	private int parentId;
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="parent_id2", nullable=false, columnDefinition="int")
	private int parentId2;
}

 

PK가 하나였을 때는 @JoinColumn 어노테이션을 이용해 PK를 지정했다.

하지만 PK가 여러개일 때는, 다음과 같이 @JoinColumns로 Parent의 PK들을 @JoinColumn으로 묶어주면 된다.

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name="child_table")
public class Child {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="child_id", nullable=false, columnDefinition="int")
	private int childId;
	
	@Column(name="child_name", nullable=false, columnDefinition="varchar(10)")
	private String childName;
	
	@ManyToOne()
	@JoinColumns({
		@JoinColumn(name="parent_id"),
		@JoinColumn(name="parent_id2"),
	})
	Parent parent;
}

 


 

마지막은 양방향성 엔티티를 만드는 방법을 알아보자.

DB를 초기화한 후 Parent 클래스에 아래 코드를 추가한다.

  • mappedBy 파라미터 안에는 Child 클래스에서 선언한 Parent의 클래스 내 변수의 이름을 삽입한다.

 

이제 다시 테스트 코드를 돌려보면 (parent도 save 하는 초기 코드) 아래와 같이 DB에 칼럼이 삽입됨을 확인할 수 있다.

 

게다가 Cascade가 Parent에 걸려있기 때문에 (기존 Child의 Cascade는 삭제했다.) Parent가 삭제되면 Child도 전부 삭제됨을 확인할 수 있었다.

	@Autowired ParentRepository parentRepo;
	@Autowired ChildRepository childRepo;

	@Test
	void contextLoads() {
		Parent parent = new Parent();
		parent.setParentName("부모1");
		
		parentRepo.save(parent);
		
		Child child = new Child();
		child.setChildName("자식1");
		child.setParent(parent);
		
		childRepo.save(child);
		
		Child child2 = new Child();
		child2.setChildName("자식1");
		child2.setParent(parent);
		
		childRepo.save(child2);
		
		parent.getChildList().add(child);
		parent.getChildList().add(child2);
		
		parentRepo.delete(parent);
	}

 

**추가

만약 양방향성 구조에서 Parent에 Child를 추가한 후 Child는 저장하지 않고 Parent만 저장하면 어떻게 될까?

궁금해서 한 번 실험을 해보았다. 하지만 결과는 Parent만 저장되고 Child는 저장되지 않음을 확인할 수 있었다.

	@Autowired ParentRepository parentRepo;
	@Autowired ChildRepository childRepo;

	@Test
	void contextLoads() {
		Parent parent = new Parent();
		parent.setParentName("부모1");
		
		//parentRepo.save(parent);
		
		Child child = new Child();
		child.setChildName("자식1");
		child.setParent(parent);
		
		parent.getChildList().add(child);
		
		parentRepo.save(parent);
		//childRepo.save(child);
	}

 

 

 

728x90
반응형