Association Mapping

MINIMI·2023년 4월 10일

JPA

목록 보기
4/9
post-thumbnail

4-1. ManyToOne

  • 연관관계란?
    • 서로 다른 두 객체가 연관성을 가지고 관계를 맺는 것
  • 연관 관계의 분류
    • 방향(Direction)에 따른 분류
      • 참조에 의한 객체의 연관 관계는 단방향
      • 테이블의 연관 관계는 외래 키를 이용하여 양방향 연관 관계의 특징을 가짐
      • 객체간의 인과 관계를 양방향으로 만들고 싶을 경우 반대 쪽에서도 필드를 추가해서 참조를 보관
      • 하지만 엄밀하게 이는 양방향 관계가 아니라 단방향 관계 2개로 볼 수 있음
    • 다중성(Multiplicity)에 대한 분류
      • 연간 관계가 있는 객체 관계 혹은 테이블 관계에서 실제로 연관을 가지는(매핑되는) 객체의 수 또는 행의 수에 따라 분류
      • 1:1(OneToOne)
      • 1:N(OneToMany)
      • N:1(ManyToOne)
      • N:N(ManyToMany)
  • 연관 관계를 가지는 엔티티 조회 방법
    • 객체 그래프 탐색(객체 연관관계를 사용한 조회)
    • 객체 지향 쿼리 사용(JPQL)
@Entity(name="many_to_one_menu_and_category")
@Table(name="TBL_MENU")
public class MenuAndCategory {
	
	@Id
	@Column(name="MENU_CODE")
	private int menuCode;
	@Column(name="MENU_NAME")
	private String menuName;
	@Column(name="MENU_PRICE")
	private int menuPrice;
	
	/* @JoinColumn : 외래키를 매핑할 때 사용한다.
	 * name 	   : 매핑할 외래키의 이름 
	 * referencesColumnName : 외래키가 참조하는 대상 테이블의 컬럼명
	 * foreignKey : 외래키 제약 조건을 직접 지정할 수 있으며 테이블 생성시 사용 된다/
	 * unique, nullable, insertable, updatable, columnDefinition, table : @Column의 속성과 동일하다. 
	 * 
	 * @ManyToOne : 다대일 관계에서 사용한다. 
	 * optional : false로 설정하면 연관 된 엔티티가 있어야 한다.
	 * cascade : 영속성 전이 기능을 사용한다.(연관 된 엔티티를 함께 영속성으로 관리한다는 의미)*/
	@JoinColumn(name="CATEGORY_CODE")
	@ManyToOne(cascade=CascadeType.PERSIST)
	private Category category;
	
	@Column(name="ORDERABLE_STATUS")
	private String orderableStatus;
@Test
	public void 다대일_연관관계_객체_그래프_탐색을_이용한_조회_테스트() {
		
		//given
		int menuCode = 15;
		
		//when
		/* 다대일 연관관계의 경우 실행 된 sql문을 보면 참조 테이블을 조인해서 결과를 조회한다. */
		MenuAndCategory foundMenu = entityManager.find(MenuAndCategory.class, menuCode);
		Category menuCategory = foundMenu.getCategory();
		
		//then
		assertNotNull(menuCategory);
		System.out.println("menuCategory = " + menuCategory);
	}
	
	@Test
	public void 다대일_연관관계_객체지향쿼리_사용한_카테고리_이름_조회_테스트() {
		
		//given
		/* join 문법이 sql과는 다소 차이가 있지만 직접 쿼리를 작성할 수 있는 문법을 제공한다.
		 * 주의할 점은 FROM 절에 기술할 테이블명에는 반드시 엔티티명이 작성 되어야 한다. */
		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);
	}
	
	@Test
	public void 다대일_연관관계_객체_삽입_테스트() {
		
		//given
		MenuAndCategory menuAndCategory = new MenuAndCategory();
		menuAndCategory.setMenuCode(999);
		menuAndCategory.setMenuName("죽방멸치빙수");
		menuAndCategory.setMenuPrice(30000);
		
		Category category = new Category();
		category.setCategoryCode(333);
		category.setCategoryName("신규카테고리");
		category.setRefCategoryCode(null);
		
		menuAndCategory.setCategory(category);
		menuAndCategory.setOrderableStatus("Y");
		
		//when
		EntityTransaction entityTransaction = entityManager.getTransaction();
		entityTransaction.begin();
		/* commit을 할 경우 flush하며 컨텍스트 내의 영속성 객체를 insert하는 쿼리를 동작시키는데 부모 테이블(TBL_CATEGORY)에 값이 먼저 들어있어야 자식 테이블(TBL_MENU)에 데이터를 넣을 수 있다.
		 * @ManyToOne 어노테이션에 영속성 전이 설정을 해주어야 한다. 
		 * 영속성 전이란? 특정 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화 한다는 의미 
		 * cascade=CascadeType.PERSIST를 설정하면 Menu를 저장하기 전에 Category부터 저장하게 된다. */
		entityManager.persist(menuAndCategory);
		entityTransaction.commit();
		
		//then
		MenuAndCategory foundMenu = entityManager.find(MenuAndCategory.class, 999);
		assertEquals(999, foundMenu.getMenuCode());
		assertEquals(333, foundMenu.getCategory().getCategoryCode());
	}

4-2. OneToMany

@Entity(name="one_to_many_category_and_menu")
@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;
@Test
	public void 일대다_연관관계_객체_그래프_탐색을_이용한_조회_테스트() {
		
		//given
		int categoryCode = 10;
		
		//when
		/* 일대다 연관관계의 경우 해당 테이블만 조회하고 연관 된 메뉴 테이블은 아직 조회하지 않는다. */
		CategoryAndMenu categoryAndMenu = entityManager.find(CategoryAndMenu.class, categoryCode);
		
		//then
		assertNotNull(categoryAndMenu);
		/* 출력 구문 작성 후, 사용하는 경우 연관 테이블을 조회해오는 동작이 일어난다. */
		System.out.println(categoryAndMenu);
	}
	
	@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.setMenuPrice(50000);
		menu.setOrderableStatus("Y");
		/* 부모키가 존재하지 않으면 자식 테이블에 값을 넣을 수 없다. */
		menu.setCategoryCode(categoryAndMenu.getCategoryCode());
		
		menuList.add(menu);
		
		categoryAndMenu.setMenuList(menuList);
		
		//when
		EntityTransaction entityTransaction = entityManager.getTransaction();
		entityTransaction.begin();
		entityManager.persist(categoryAndMenu);
		entityTransaction.commit();
		
		//then
		CategoryAndMenu foundMenu = entityManager.find(CategoryAndMenu.class, 888);
		System.out.println(foundMenu);
	}

4-3. BiDirection

  • 양방향 연관관계 매핑
    • 데이터베이스의 테이블은 외래키 하나로 양방향 조회 가능
    • 객체는 서로 다른 두 단방향 참조를 합쳐서 양방향
    • 두 개의 연관 관계 중 관계의 주인을 정하고, 주인이 아닌 연관 관계를 하나 더 추가하는 방식으로 진행
    • 반대 방향으로도 access함
    • 객체 그래프 탐색을 할 일이 많은 경우 양방향 연관관계 매핑 사용
  • 연관 관계의 주인을 정하는 기준
    • 비즈니스 중요도를 배제하고 단순히 외래키 관리자의 의미 부여
    • 외래키를 가지고 있는 엔티티가 연관 관계의 주인
@Entity(name="bidirection_menu")
@Table(name="TBL_MENU")
public class Menu {
	
	@Id
	@Column(name="MENU_CODE")
	private int menuCode;
	@Column(name="MENU_NAME")
	private String menuName;
	@Column(name="MENU_PRICE")
	private int menuPrice;
	/* 연관관계의 주인인 경우 전과 똑같은 방식으로 연관 관계 매핑을 처리하면 된다. */
	@JoinColumn(name="CATEGORY_CODE")
	@ManyToOne
	private Category category;
	
	@Column(name="ORDERABLE_STATUS")
	private String orderableStatus;
@Entity(name="bidirection_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;
	/* mappedBy : 연관 관계의 주인을 정하기 위해서 연관 관계의 주인이 아닌 객체에 mappedBy를 써서 연관 관계 주인 객체의
	 * 필드명을 매핑 시켜 놓으면 로직으로 양방향 관계를 적용할 수 있다. */
	@OneToMany(mappedBy="category")
	private List<Menu> menuList;
@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());
		
		/* 주의사항
		 * toString() 오버라이딩 시 양방향 연관 관계는 재귀호출이 일어나기 때문에 stackOverFlowError가 발생하게 된다.
		 * 따라서 재귀가 일어나지 않게 하기 위해서는 엔티티의 주인이 아닌 쪽의 toString을 연관 객체 부분이 출력 되지 않도록 해야 한다.
		 * 특히 자동 완성 및 중복 라이브러리를 이용하는 경우 해당 문제 발생 가능성이 매우 높아진다. */
		System.out.println(foundMenu);
		System.out.println(foundCategory);
		
		/* category에 포함 된 메뉴 목록 출력 구문을 작성하고 나면 실제 사용에 필요해지기 때문에 가짜 연관관계에 해당하는 엔티티도
		 * 다시 조회하는 쿼리가 한 번 더 동작한다. */
		System.out.println(foundCategory.getMenuList());
	}
@Test
	public void 양방향_연관관계_주인_객체를_이용한_삽입_테스트() {
		
		//given
		Menu menu = new Menu();
		menu.setMenuCode(125);
		menu.setMenuName("연관관계주인메뉴");
		menu.setMenuPrice(10000);
		menu.setOrderableStatus("Y");
		
		/* 양방향 연관관계를 설정하고 흔히 하는 실수는 연관관계의 주인에는 값을 입력하고, 주인이 아닌 곳에는 값을 입력하지 않는 경우
		 * 외래키 컬럼이 not null 제약조건이 설정되어 있는 경우이다. 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);
		
		//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);
	
	}
}
profile
DREAM STARTER

0개의 댓글