연관 관계
서로 다른 두 객체가 연관성을 가지고 관계를 맺는 것을 연관 관계라고 함
연관 관계의 분류
1. 방향에 따른 분류(단반향, 양방향)
- 테이블의 연관 관계는 외래키를 이용하여 양방향 연관 관계의 특성을 가짐
- 참조에 의한 객체의 연관관계는 단반향
- 객체 간의 연관 관계를 양방향으로 만들고 싶으면 반대 쪽에도 필드를 추가해서 참조를 보관하면 됨
( 양방향 관계가 아닌 서로 다른 단방향 관계 2개로 볼 수 있음)
2. 다중성에 대한 분류
- 연관 관계가 있는 객체거나 혹은 테이블 관계에서 실제로 연관을 가지는(매핑되는) 객체의 수( 객체 관계 또는 행(테이블 관계)의 수에 따라 분류 됨
(1) 1:1(OneToOne) 연관 관계
(2) 1:N(OneToMany) 연관 관계
(3) N:1(ManyToOne) 연관 관계
(4) N:N(ManyToMany) 연관 관계
3. 외래키 매핑
- @JoinColumn
(1) name : 매핑할 외래키 이름 ( 기본 값 : 필드명 + _ + 참조하는 테이블의 기본키와 컬럼명)
ex) category_category_code
(2) referenceColumnName : 외래키가 참조하는 대상 테이블의 컬럼명 ( 기본 값 : 참조하는 테이블의 기본키 컬럼)
(작성하지 않아도 됨. 명시해야 하는 경우에 작성)
(3) foreignKey : 외래키 제약 조건을 직접 지정하여 테이블을 생성할 때만 사용
4. 다대일 관계 매핑
- @ManyToOne
(1) optional : false로 설정하면 연관 된 엔터티가 항상 있어야 함
(2) fetch : 글로벌 패치 전략을 설정
(3) cascade : 영속성 전이 기능을 사용
(4) targetEntity : 연관 된 엔티티의 타입 정보를 설정. 제네릭으로 타입 정보를 알 수 있어 거의 사용하지 않음
(5) orphanRemoval : true로 설정하면 고아 객체 제거
5. 양방향 연관 관계 매핑
- 데이터베이스 테이블은 외래키 하나로 양방향 조회가 가능하지만, 객체는 서로 다은 단반향 두 개를 합쳐서 양방향이 라고 함
- 두 개의 연관 관계 중 연관 관계의 주인을 정하고, 주인이 아닌 연관 관계 하나를 더 추가하는 방식으로 작성
- 양방향 연관 관계를 매핑하는 경우는 반대 방향으로도 access하여 객체 그래프 탐색을 할 일이 많은 경우에 하게 됨
- mappedBy : 연관 관계의 주인이 아닌 객체에 작성하여 연관 관계 주인의 객체의 필드명을 매핑
- 연관관계의 주인 : 데이터베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)하는 관지라를 뜻함(보통 FK를 가지고 있는 엔터티가 주인)
6. 연관 관계 가지는 엔터티 조회하는 방법
(1) 객체 그래프 탐색(객체 연관 관계를 사용한 조회)
(2) 객체 지향 쿼리 사용(JPQL)
@ManyToOne
Menu 엔티티 (@ManyToOne)
@Entity(name="ManyToOneMenu")
@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;
@ManyToOne(cascade=CascadeType.PERSIST)
// menu entity를 persist할 때 category에게 영속성을 전이하여 같이 저장하겠다는 의미
@JoinColumn(name="CATEGORY_CODE")
private Category category;
@Column(name="ORDERABLE_STATUS")
private String orderableStatus;
public Menu() {}
public Menu(int menuCode, String menuName, int menuPrice, Category category, String orderableStatus) {
super();
this.menuCode = menuCode;
this.menuName = menuName;
this.menuPrice = menuPrice;
this.category = category;
this.orderableStatus = orderableStatus;
}
public int getMenuCode() {
return menuCode;
}
public void setMenuCode(int menuCode) {
this.menuCode = menuCode;
}
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public int getMenuPrice() {
return menuPrice;
}
public void setMenuPrice(int menuPrice) {
this.menuPrice = menuPrice;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public String getOrderableStatus() {
return orderableStatus;
}
public void setOrderableStatus(String orderableStatus) {
this.orderableStatus = orderableStatus;
}
@Override
public String toString() {
return "Menu [menuCode=" + menuCode + ", menuName=" + menuName + ", menuPrice=" + menuPrice + ", category="
+ category + ", orderableStatus=" + orderableStatus + "]";
}
}
category 엔티티
@Entity(name="ManyToOneCategory")
@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;
//db에 상위 카테고리와 하위 카테고리가 저장되어 있는 상황이기 때문에 상위 카테고리의 null값이
//반환 될 수 있게 기본자료형이 아닌 wrapper클래스로 선언해야 함.
public Category() {
}
public Category(int categoryCode, String categoryName, Integer refCategoryCode) {
super();
this.categoryCode = categoryCode;
this.categoryName = categoryName;
this.refCategoryCode = refCategoryCode;
}
public int getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(int categoryCode) {
this.categoryCode = categoryCode;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public Integer getRefCategoryCode() {
return refCategoryCode;
}
public void setRefCategoryCode(Integer refCategoryCode) {
this.refCategoryCode = refCategoryCode;
}
@Override
public String toString() {
return "Category [categoryCode=" + categoryCode + ", categoryName=" + categoryName + ", refCategoryCode="
+ refCategoryCode + "]";
}
}
엔터티 조회
public class ManyToOneAssociationTests {
private static EntityManagerFactory entityManagerFactory;
private EntityManager entityManager;
@BeforeAll
public static void initFactory() {
entityManagerFactory = Persistence.createEntityManagerFactory("jpatest");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
@Test
public void 다대일_연관관계_객체_그래프_탐색을_이용한_조회_테스트() {
//given
int menuCode = 15;
//when
/* 다대일 연관 관계의 경우 실행 된 sql을 보면 참조 테이블을 조인해서 결과를 조회한다.*/
Menu foundMenu = entityManager.find(Menu.class, menuCode);
Category menuCategory = foundMenu.getCategory();
//then
assertNotNull(foundMenu);
assertNotNull(menuCategory);
}
@Test
public void 다대일_연관관계_객체_지향_쿼리_사용한_카테고리_이름_조회_테스트() {
//given
String jpql = "SELECT c.categoryName FROM ManyToOneMenu m JOIN m.category c WHERE m.menuCode = 15"; //from 엔티티명 별칭 JOIN m.category
//when
/* 조회 시 조인 구문이 실행 되며 연관 테이블을 미리 조회해 온다.*/
String category = entityManager.createQuery(jpql, String.class).getSingleResult(); //두번쨰 인자로는 반환한 값의 타입을 스트링 타입으로 반환할거게요 / 가져올 값이 하나의 행이다 .
//then
assertEquals("일식", category);
}
객체 삽입
@Test
public void 다대일_연관관계_객체_삽입_테스트() {
//given
Menu menu = new Menu();
menu.setMenuCode(999);
menu.setMenuName("죽방멸치빙수");
menu.setMenuPrice(30000);
Category category = new Category();
category.setCategoryCode(333);
category.setCategoryName("신규카테고리");
category.setRefCategoryCode(null);
menu.setCategory(category);
menu.setOrderableStatus("Y");
//신규 카테고리가 만들어지면서 그 값을 가지는 메뉴가 하나 삽입되는 상황
//When
/* Category(부모테이블)에 값이 먼저 들어있어야 Menu(자식테이블)에 데이터를 넣을 수 있으므로
* 반드시 @ManyToOne 어노테이션에 영속성 전이 설정을 해주어야 한다.
* 설정을 해주었을 경우 Menu를 저장하기 전에 Category부터 저장하게 된다.
* */
EntityTransaction entityTransaction = entityManager.getTransaction();
entityTransaction.begin();
entityManager.persist(menu);
entityTransaction.commit();
//them
Menu foundMenu = entityManager.find(Menu.class, 999);
assertEquals(999, foundMenu.getMenuCode());
assertEquals(333, foundMenu.getCategory().getCategoryCode());
}
@OneToMany
Category 엔티티(@OneToMany)
@Entity(name="OneToManyCategory")
@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;
@OneToMany(cascade=CascadeType.PERSIST) // 일대다 관계라는 매핑 정보
@JoinColumn(name="CATEGORY_CODE") // 매핑할 외래키 이름 지정
private List<Menu> menuList;
public Category() {}
public Category(int categoryCode, String categoryName, Integer refCategoryCode, List<Menu> menuList) {
super();
this.categoryCode = categoryCode;
this.categoryName = categoryName;
this.refCategoryCode = refCategoryCode;
this.menuList = menuList;
}
public int getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(int categoryCode) {
this.categoryCode = categoryCode;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public Integer getRefCategoryCode() {
return refCategoryCode;
}
public void setRefCategoryCode(Integer refCategoryCode) {
this.refCategoryCode = refCategoryCode;
}
public List<Menu> getMenuList() {
return menuList;
}
public void setMenuList(List<Menu> menuList) {
this.menuList = menuList;
}
@Override
public String toString() {
return "Category [categoryCode=" + categoryCode + ", categoryName=" + categoryName + ", refCategoryCode="
+ refCategoryCode + ", menuList=" + menuList + "]";
}
}
Menu 엔티티
@Entity(name="OneToManyMenu")
@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;
@Column(name="CATEGORY_CODE")
private int categoryCode;
@Column(name="ORDERABLE_STATUS")
private String orderableStatus;
public Menu() {}
public Menu(int menuCode, String menuName, int menuPrice, int categoryCode, String orderableStatus) {
super();
this.menuCode = menuCode;
this.menuName = menuName;
this.menuPrice = menuPrice;
this.categoryCode = categoryCode;
this.orderableStatus = orderableStatus;
}
public int getMenuCode() {
return menuCode;
}
public void setMenuCode(int menuCode) {
this.menuCode = menuCode;
}
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public int getMenuPrice() {
return menuPrice;
}
public void setMenuPrice(int menuPrice) {
this.menuPrice = menuPrice;
}
public int getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(int categoryCode) {
this.categoryCode = categoryCode;
}
public String getOrderableStatus() {
return orderableStatus;
}
public void setOrderableStatus(String orderableStatus) {
this.orderableStatus = orderableStatus;
}
@Override
public String toString() {
return "Menu [menuCode=" + menuCode + ", menuName=" + menuName + ", menuPrice=" + menuPrice + ", categoryCode="
+ categoryCode + ", orderableStatus=" + orderableStatus + "]";
}
}
조회 및 삽입
public class OneToManyAssociationTests {
private static EntityManagerFactory entityManagerFactory;
private EntityManager entityManager;
@BeforeAll
public static void initFactory() {
entityManagerFactory = Persistence.createEntityManagerFactory("jpatest");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
@Test
public void 일대다_연관관계_객체_그래프_탐색을_이용한_조회_테스트() {
//given
int categoryCode = 10;
//when
/* 일대다 연관관계의 경우 해당 테이블만 조회하고 연관된 메뉴 테이블은 아직 조회하지 않는다.(사용할때조회함)*/
Category category = entityManager.find(Category.class, categoryCode);
//then
assertNotNull(category);//이 시점까지는 카테고리에대한 테이블만 조회있어쏙, 메뉴에 대해서 조회하지 않음
/* 메뉴 리스트 사용 시 다시 한 번 조회 구문을 수행한다. */
category.getMenuList().forEach(System.out::println);
//사용하는 시점에서 다시 한번 조회 구문을 수행함. 조인하지 않고 그값을 where절에 넣어서 값을 가져옴
}
@Test
public void 일대다_연관관계_객체_삽입_테스트() {
//given
Category category = new Category();
category.setCategoryCode(889);
category.setCategoryName("일대다추가카테고리2");
category.setRefCategoryCode(null);
List<Menu> menuList = new ArrayList<>();
Menu menu = new Menu();
menu.setMenuCode(777);
menu.setMenuName("1:N 아이스크림2" );
menu.setMenuPrice(50000);
menu.setOrderableStatus("Y");
menu.setCategoryCode(category.getCategoryCode());
menuList.add(menu);
category.setMenuList(menuList);
//when
EntityTransaction entityTransaction = entityManager.getTransaction();
entityTransaction.begin();
entityManager.persist(category);
entityTransaction.commit();
//then
Category foundCategory = entityManager.find(Category.class, 888);
assertNotNull(foundCategory);
}
양방향 연관관계 매핑
Category 엔터티
@Entity(name="BidirectionCategory")
@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;
/*여기에서는 Menu 테이블이 FK를 가지고 있으므로
연관 관계의 주인이고 Catetory는 주인이 아니므로 mappedBy 속성을 설정해준다.*/
@OneToMany(mappedBy = "category")//메뉴의 카테고리 필드명 입력
public List<Menu> menuList;
public Category() {
}
public Category(int categoryCode, String categoryName, Integer refCategoryCode, List<Menu> menuList) {
super();
this.categoryCode = categoryCode;
this.categoryName = categoryName;
this.refCategoryCode = refCategoryCode;
this.menuList = menuList;
}
public int getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(int categoryCode) {
this.categoryCode = categoryCode;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public Integer getRefCategoryCode() {
return refCategoryCode;
}
public void setRefCategoryCode(Integer refCategoryCode) {
this.refCategoryCode = refCategoryCode;
}
public List<Menu> getMenuList() {
return menuList;
}
public void setMenuList(List<Menu> menuList) {
this.menuList = menuList;
}
@Override
public String toString() {
return "Category [categoryCode=" + categoryCode + ", categoryName=" + categoryName + ", refCategoryCode="
+ refCategoryCode + /*", menuList=" + menuList + */"]";
}
}
Menu 엔터티
@Entity(name="BidirectionMenu")
@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;
public Menu() {}
public Menu(int menuCode, String menuName, int menuPrice, Category category, String orderableStatus) {
super();
this.menuCode = menuCode;
this.menuName = menuName;
this.menuPrice = menuPrice;
this.category = category;
this.orderableStatus = orderableStatus;
}
public int getMenuCode() {
return menuCode;
}
public void setMenuCode(int menuCode) {
this.menuCode = menuCode;
}
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public int getMenuPrice() {
return menuPrice;
}
public void setMenuPrice(int menuPrice) {
this.menuPrice = menuPrice;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public String getOrderableStatus() {
return orderableStatus;
}
public void setOrderableStatus(String orderableStatus) {
this.orderableStatus = orderableStatus;
}
@Override
public String toString() {
return "Menu [menuCode=" + menuCode + ", menuName=" + menuName + ", menuPrice=" + menuPrice + ", category="
+ category + ", orderableStatus=" + orderableStatus + "]";
}
조회/ 삽입
public class BiDirectionTests {
private static EntityManagerFactory entityManagerFactory;
private EntityManager entityManager;
@BeforeAll
public static void initFactory() {
entityManagerFactory = Persistence.createEntityManagerFactory("jpatest");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
/* 양방향 연관관계 매핑
* 데이터베이스 테이블은 외래키 하나로 양방향 조회가 가능하다.
* 하지만 객체는 서로 다른 단방향 두 개를 합쳐서 양방향이라고 한다.
* 따라서 두 개의 연관 관계 중 연관 관계의 주인을 정하고, 주인이 아닌 연관 관계 하나를 더 추가하는 방식으로 작성하게 된다.
* 양방향 연관 관계를 매핑하는 경우는 반대 방향으로도 access하여 객체 그래프 탐색을 할 일이 많은 경우 하게 된다.*/
@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);
/* 양방향 참조시 주의 사항
* toStirng() 오버라이딩 시 양방향 연관관계는 재귀 호출이 일어나 stackOverFlowError 가 발생한다
* 이러한 현상이 일어나지 않게 하기 위해서는 엔티티 주인이 아닌 쪽의 toString을 연관 객체 부분이 출력 되지 않도록 해야 한다.
* 특히 자동 완성 및 lombok 라이브러리를 이용하는 경우 해당 문제 발생 가능성이 매우 높아진다.*/
System.out.println(foundMenu);
System.out.println(foundCategory);
//then
assertEquals(menuCode, foundMenu.getMenuCode());
assertEquals(categoryCode, foundCategory.getCategoryCode());
/* category에 포함 된 메뉴 목록 출력 구문을 작성하면 실제 menuList를 사용해야하는 상황이므로
* 가짜 연관 관계에 해당하는 엔티티도 다시 조회하는 쿼리가 한 번 더 동작한다.
* 만약 join이 처음부터 되어 데이터를 가져와야 했다면 테이블 풀 스캐닝이 일어나기 떄문에 효율이 떨어지게 된다.*/
foundCategory.getMenuList().forEach(System.out::println);
}
@Test
public void 양방향_연관관계_주인_객체를_이용한_삽입_테스트() {
/* 양방향 연관 관계를 설정하고 흔히 하는 실수는
* 연관 관계 주인에는 값을 입력하고, 주인이 아닌 곳에는 값을 입력하지 않아
* 외래키 제약 조건이 not null인 상황에서 null 값을 외래키 컬럼에 삼입할 수 없기 때문에 에러가 발생하는 상황이다.*/
//given 메뉴테이블에 행을 하나 삽입하고 싶습니다. -> 카테고리는 notnull값이 들어 갈 수 없기 때문에 오류
Menu menu = new Menu();
menu.setMenuCode(20);
menu.setMenuName("연관관계주인메뉴");
menu.setMenuPrice(10000);
menu.setOrderableStatus("Y");
/* 카테고리 정보 추가 */
menu.setCategory(entityManager.find(Category.class, 4)); //한식(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());
}
@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.getRefCategoryCode());
assertEquals(category.getCategoryCode(), foundCategory.getCategoryCode());
}
}
'프로그래밍 > Spring & Spring boot' 카테고리의 다른 글
[Springboot / 스프링부트] thymeleaf (2) 타임리프 제어문 (0) | 2022.09.26 |
---|---|
[Springboot / 스프링부트] thymeleaf (1) 타임리프 표현식 (0) | 2022.09.26 |
[Spring/JPA] Mapping (2) @SequenceGenerator , @TableGenerator, @Embeddable, @IdClass (0) | 2022.09.23 |
[Spring/JPA ] Mapping(1) @Entity, @Table, @Enumerated, @Access (0) | 2022.09.23 |
[Spring/스프링] JPA(Java Persistence API) (0) | 2022.09.23 |