LECTURE/JPA

JPA association mapping

heywoo 2023. 4. 10. 12:31


연관 관계란?

서로 다른 두 객체가 연관성을 가지고 관계를 맺는 것
  

연관 관계의 분류


 1. 방향(Direction)에 따른 분류


참조에 의한 객체의 연관 관계는 단방향이다. 테이블의 연관 관계는 외래 키를 이용한 양방향 연관 관계의 특징을 가진다.
객체간의 연관 관계를 양방향으로 만들고 싶을 경우 반대 쪽에서도 필드를 추가해서 참조를 보관하면 된다.
하지만 엄밀하게 이는 양방향 관계가 아니라 단방향 관계 2개로 볼 수 있다.

1-1. 단방향 연관 관계
1-2. 양방향 연관 관계



 2. 다중성(Multiplicity)에 대한 분류

연관 관계가 있는 객체 관계 혹은 테이블 관계에서 실제로 연관을 가지는(매핑되는) 객체의 수 또는 행의 수에 따라 분류된다.

2-1. 1:1(OneToOne) 연관 관계 
2-2. 1:N(OneToMany) 연관 관계    - 가장 자주
2-3. N:1(ManyToOne) 연관 관계    - 사용하는 관계

2-4. N:N(ManyToMany) 연관 관계 -> 물리적으로 구현하긴 힘듦

 

manytoone

@JoinColumn : 외래키를 매핑할 때 사용한다. 
 name : 매핑할 외래키의 이름
 referencesColumnName : 외래키가 참조하는 대상 케이블의 컬럼명
 foreignKey : 외래키 제약 조건을 직접 지정할 수 있으며 테이블 생성 시 사용된다.
 unique, nullable, insertable, updatable, columnDefinition, table : @column의 속성과 동일하다.

@ManyToOne : 다대일 관계에서 사용한다.

 optional : false로 설정하면 연관된 엔티티가 항상 있어야 한다.

 cascade : 영속성 전이 기능을 사용한다. (연관된 엔티티를 함께 영속성으로 관리한다는 의미)

 orphanRemoval : true로 설정하면 고아 객체 제거

@Entity(name="many_to_one_category")
@Table(name="TBL_CATEGORY")
public class Category {
	
	@Id
	@Column(name="CATEGORY_CODE")
	private int categoryCode;
	@Column(name="CATEGORY_NAME")
	private String categoryName;
	@Column(name="REF_CATEGORY_CODE")
	private Integer refCategoryCode;  //null값이 반환되어야 하기 때문에 Integer 타입으로 작성
	
	public Category() {} //기본 생성자는 반드시 있어야 한다.
    
    
    ..생략...

 

하나의 카테고리가 여러 메뉴를 가진다 -> 메뉴를 기준으로 N:1

//MenuAndCategory 클래스 필드 추가


@JoinColumn(name="CATEGORY_CODE")

@ManyToOne(cascade=CascadeType.PERSIST)

private Category category;

 

 

연관 관계를 가지는 엔티티를 조회하는 방법

1. 객체 그래프 탐색(객체 연관관계를 사용한 조회)

다대일 연관관계의 경우 실행 된 sql문을 보면 참조 테이블을 조인해서 결과를 조회한다.

	@Test
	public void 다대일_연관관계_객체_그래프_탐색을_이용한_조회_테스트() {
		
		//given
		int menuCode = 15;
		
		//when
		MenuAndCategory foundMenu = entityManager.find(MenuAndCategory.class, menuCode);
		Category menuCategory = foundMenu.getCategory();
		
		//then
		assertNotNull(menuCategory);
		System.out.println("menuCategory = " + menuCategory);
	}
Hibernate:
select menuandcat0_.MENU_CODE as menu_code1_1_0_,
menuandcat0_.CATEGORY_CODE as category_code5_1_0_,
menuandcat0_.MENU_NAME as menu_name2_1_0_,
menuandcat0_.MENU_PRICE as menu_price3_1_0_,
menuandcat0_.ORDERABLE_STATUS as orderable_status4_1_0_,
category1_.CATEGORY_CODE as category_code1_0_1_,
category1_.CATEGORY_NAME as category_name2_0_1_,
category1_.REF_CATEGORY_CODE as ref_category_code3_0_1_
from TBL_MENU menuandcat0_
left outer join TBL_CATEGORY category1_
on menuandcat0_.CATEGORY_CODE=category1_.CATEGORY_CODE
where menuandcat0_.MENU_CODE=?

 

2. 객체 지향 쿼리 사용(JPQL)

join 문법이 sql과는 다소 차이가 있지만 직접 쿼리를 작성할 수 있는 문법을 제공한다.

주의할 점은 FROM 절에 기술할 테이블명에는 반드시 엔티티명이 작성되어야 한다.

 

	@Test
	public void 다대일_연관관계_객체지향쿼리_사용한_카테고리_이름_조회_테스트() {
		
		//given
		String jpql = "SELECT c.categoryName FROM many_to_one_menu_and_category m JOIN m.category c WHERE m.menuCode = 15";
		
		//when
		String category = entityManager.createQuery(jpql, String.class).getSingleResult();
		
		//then
		assertNotNull(category);
		System.out.println("category = " + category);
		
		
	}

 

다대일 연관관계 객체 삽입

commit을 할 경우 flush하며 컨텍스트 내의 영속성 객체를 insert 하는 쿼리 동작시키는데

부모 테이블(TBL_CATEGORY)에 값이 먼저 들어있어야 자식 테이블(TBL_MENU)에 데이터를 넣을 수 있다.

@ManyToOne 어노테이션에 영속성 전이 설정을 해주어야 한다. -> cascade

영속성 전이? 특정 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화 한다는 의미이다.

	@Test
	public void 다대일_연관관계_객체_삽입_테스트() {
		
		//given
		MenuAndCategory menuAndCategory = new MenuAndCategory();
		menuAndCategory.setMenuCode(99999);
		menuAndCategory.setMenuName("죽방멸치빙수");
		menuAndCategory.setMenuPrice(30000);
		
		Category category = new Category();
		category.setCategoryCode(33333);
		category.setCategoryName("신규카테고리");
		
		menuAndCategory.setCategory(category);
		menuAndCategory.setOrderableStatus("Y");
		
		//when
		entityManager.persist(menuAndCategory);
		entityTransaction.commit();
		
		//then
		MenuAndCategory foundMenuAndCategory = entityManager.find(MenuAndCategory.class, 99999);
		assertEquals(99999, foundMenuAndCategory.getMenuCode());
		assertEquals(33333, foundMenuAndCategory.getCategory().getCategoryCode());
	}

 

onetomany

@OneToMany : 일대다 관계에서 사용한다.

@Entity(name="one_to_many_category")
@Table(name="TBL_CATEGORY")
public class CategoryAndMenu {
	
	@Id
	@Column(name="CATEGORY_CODE")
	private int categoryCode;
	@Column(name="CATEGORY_NAME")
	private String categoryName;
	@Column(name="REF_CATEGORY_CODE")
	private Integer refCategoryCode;
	
	@JoinColumn(name="CATEGORY_CODE")
	@OneToMany(cascade=CascadeType.PERSIST)
	private List<Menu> menuList; 	//하나의 카테고리 당 여러 메뉴가 있기에 List
	
	public CategoryAndMenu() {}
    
    생략...

 

일대다 연관관계 객체 조회

 

일대다 연관관계의 경우 해당 테이블만 조회하고 연관된 메뉴 테이블은 아직 조회하지 않는다. 

출력 구문 작성 후, 사용하는 경우 연관 테이블을 조회해오는 동작이 일어난다. 

	@Test
	public void 일대다_연관관계_객체_그래프_탐색을_이용한_조회_테스트() {
		
		
		//given
		int categoryCode = 10;
		
		//when
		CategoryAndMenu categoryAndMenu = entityManager.find(CategoryAndMenu.class, categoryCode);
		
		//then
		assertNotNull(categoryAndMenu);
		
		System.out.println(categoryAndMenu); //출력 시 toString 호출 -> menuList 반환해야 함
	}

 

일대다 연관관계 객체 삽입

부모키가 존재하지 않으면 자식 테이블에 값을 넣을 수 없기 때문에 추가될 카테고리코드를 받아온다.

	@Test
	public void 일대다_연관관계_객체_삽입_테스트() {
		
		//given
		CategoryAndMenu categoryAndMenu = new CategoryAndMenu();
		categoryAndMenu.setCategoryCode(888);
		categoryAndMenu.setCategoryName("일대다추가카테고리");
		categoryAndMenu.setRefCategoryCode(null);
		
		List<Menu> menuList = new ArrayList<>();
		Menu menu = new Menu();
		menu.setMenuCode(777);
		menu.setMenuName("일대다아이스크림");
		menu.setOrderableStatus("Y");
		menu.setCategoryCode(categoryAndMenu.getCategoryCode());
		
		menuList.add(menu);
		categoryAndMenu.setMenuList(menuList); //menuList는 참조 값이기 때문에 setMenuList에 넣어준다
		
		//when
		EntityTransaction entityTransaction = entityManager.getTransaction();
		entityTransaction.begin();
		entityManager.persist(categoryAndMenu);
		entityTransaction.commit();
		
		//then
		CategoryAndMenu foundCategoryAndMenu = entityManager.find(CategoryAndMenu.class, 888);
		System.out.println(foundCategoryAndMenu);

		
		//insert 시가 아니라 업데이트 되면서 처리된다.
		
	}

 

insert 시가 아니라 update 시에 category_code가 처리된다.

 

 

bidirection

양방향 연관관계 매핑

데이터베이스의 테이블은 외래키 하나로 양방향 조회가 가능하지만 객체는 서로 다른 두 단방향 참조를 합쳐서 양방향이라고 한다.

따라서 두 개의 연관 관계 중 연관 관계의 주인을 정하고, 주인이 아닌 연관 관계를 하나 더 추가하는 방식으로 작성하게 된다.

반대 방향으로도 access하여 객체 그래프 탐색을 할 일이 많은 경우 양방향 연관관계 매핑을 사용한다. (항상 사용하는 것은 아님)

 

연관 관계의 주인을 정하는 기준

양방향 연관 관계 시 연관관계의 주인(Owner)라는 이름으로 오해가 있을 수 있다.

비즈니스 로직 상 더 중요하다고 연관 관계의 주인으로 선택하면 안되며, 비즈니스 중요도롤 배제하고 단순히 외래키 관리자의 의미를 부여해야 한다.

연관 관계의 주인은 외래키를 가지고 있는 엔티티이다.

 

Menu : 연관 관계의 주인의 경우 전과 똑같은 방식으로 연관 관계 매핑을 처리하면 된다.

	@JoinColumn(name="CATEGORY_CODE")
	@ManyToOne
	private Category category;

 

Category : 연관 관계 주인을 정하기 위해서 연관 관계이 주인이 아닌 객체에 mappedBy를 써서 연관 관계 주인 객체의 
필드명을 매핑 시켜 놓으면 로직으로 양방향 관계를 적용할 수 있다. 

(@JoinColumn는 필요 없음.)

	@OneToMany(mappedBy="category")
	private List<Menu> menuList;

 

양방향 연관관계 매핑 조회

주의사항!

toString() 오버라이딩 시 양방향 연관 관계는 재귀호출이 일어나기 때문에 stackOverFlowError가 발생하게 된다.
따라서 재귀가 일어나지 않게 하기 위해서는 엔티티의 주인이 아닌 쪽(카테고리)이 toString을 연관 객체 부분이 출력되지 않도록 수정 해야 한다.
-> 특히 자동 완성 및 롬복 라이브러리를 이용하는 경우 해당 문제 발생 가능성이 매우 높아진다. 

	@Test
	public void 양방향_연관관계_매핑_조회_테스트() {
		
		//given
		int menuCode = 10;
		int categoryCode = 10;
		
		//when
		/* 진짜 연관 관계는 처음 조회 시부터 조인한 결과를 인출해온다. */
		Menu foundMenu = entityManager.find(Menu.class, menuCode);
		/* 가짜 연관 관계는 해당 엔티티를 조회하고 필요 시 연관 관계 엔티티를 조회하는 쿼리를 다시 실행하게 된다. */
		Category foundCategory = entityManager.find(Category.class, categoryCode);
		
		//then
		assertEquals(menuCode, foundMenu.getMenuCode());
		assertEquals(categoryCode, foundCategory.getCategoryCode());
	

		System.out.println(foundMenu);
		System.out.println(foundCategory);
		

		foundCategory.getMenuList().forEach(System.out::println);
	}

 

category에 포함된 메뉴 목록 출력 구문을 작성하고 나면 실제 사용에 필요해지기 때문에 가짜 연관 관계에 해당하는 엔티티도 다시 조회하는 쿼리가 한 번 더 동작한다. 

 

 

양방향 연관관계 주인 객체를 이용한 삽입

양방향 연관관계를 설정하고 흔히 하는 실수는 연관관계의 주인에는 값을 입력하고, 주인이 아닌 곳에는 값을 입력하지 않는 경우 외래키 컬럼이 not null 제약 조건이 설정되어 있는 경우이다.

	@Test
	public void 양방향_연관관계_주인_객체를_이용한_삽입테스트() {
		
		//given
		Menu menu = new Menu();
		menu.setMenuCode(125);
		menu.setMenuName("연관관계주인메뉴");
		menu.setMenuPrice(10000);
		menu.setOrderableStatus("Y");
		//null값이 외래키 컬럼에 삽입되지 않으므로 에러가 발생하기에 카테고리 정보를 추가한다.
		menu.setCategory(entityManager.find(Category.class, 4));
		
		//when
		EntityTransaction entityTransaction = entityManager.getTransaction();
		entityTransaction.begin();
		entityManager.persist(menu);
		entityTransaction.commit();
		
		//then
		Menu foundMenu = entityManager.find(Menu.class, menu.getMenuCode());
		assertEquals(menu.getMenuCode(), foundMenu.getMenuCode());
		System.out.println(foundMenu);
		
	}

 

양방향 연관관계 주인이 아닌 객체를 이용한 삽입

영속성 전이를 하지 않은 이상 주인인 메뉴를 처리할 필요가 없다.

	@Test
	public void 양방향_연관관계_주인이_아닌_객체를_이용한_삽입_테스트() {
		
		//given
		Category category = new Category();
		category.setCategoryCode(1004);
		category.setCategoryName("양방향카테고리");
		category.setRefCategoryCode(null);
		//카테고리 정보만 insert하고자 할 때는 menuList는 채울 필요가 없다.
	
		//when
		EntityTransaction entityTransaction = entityManager.getTransaction();
		entityTransaction.begin();
		entityManager.persist(category);
		entityTransaction.commit();
		
		//then
		Category foundCategory = entityManager.find(Category.class, category.getCategoryCode());
		assertEquals(category.getCategoryCode(), foundCategory.getCategoryCode());
		System.out.println(foundCategory);
		
	}

 

 

 

OneToOne

 

1. 외래키가 TBL_USER1에 있는 경우 단방향 연관관계 

	@Test
	public void 일대일_연관관계_테스트1() {
		
		
		//given
		Long userCode = 1L;
				
		//when
		User1 user = entityManager.find(User1.class, userCode);
				
		//then
		assertNotNull(user);
		System.out.println(user);
		
	}

 

2.  외래키가 TBL_USER2에 있는 경우 양방향 연관관계 

@Test
	public void 일대일_연관관계_테스트2() {
		
		
		//given
		Long userCode = 1L;
				
		//when
		User2 user = entityManager.find(User2.class, userCode);
				
		//then
		assertNotNull(user);
		System.out.println(user);
		System.out.println(user.getUserInfo2());
		System.out.println(user.getUserInfo2().getUser()); //역방향 참조
	}

 

 

3.외래키가 TBL_USERINFO3에 있는 경우 양방향 연관관계 

1:1 관계에서 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않기 때문에 무조건 양방향 연관관계만 구현할 수 있다.

(주 테이블: 데이터가 먼저 생성되는 테이블. user테이블 / 대상 테이블 : 주 테이블에서 파생된 테이블 userinfo )

	@Test
	public void 일대일_연관관계_테스트3() {
		
		
		//given
		Long userCode = 1L;
				
		//when
		User3 user = entityManager.find(User3.class, userCode);
				
		//then
		assertNotNull(user);
		System.out.println(user);
		System.out.println(user.getUserInfo3());
	}