본문 바로가기
Framework and Tool/JPA

JPA - 다양한 연관관계 - M : N

by ocwokocw 2021. 7. 8.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- M : N 관계

회원과 상품이 있다고 가정해보자. 회원은 여러 상품을 주문할 수 있고, 하나의 상품도 여러 회원들에 의해 주문될 수 있다. 이때 다중성의 관계는 M : N 이다.

이를 ER Diagram 으로 나타내면 위와 같다. 그러나 RDB(관계형 DB)는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 조인 테이블을 사용한다. 

1 번째 다이어그램에서 USER 와 PRODUCT 의 PK인 USER_ID 와 PRODUCT_ID 로 이루어진 조인테이블 USER_PRODUCT 가 추가되었다.

 

하지만 객체의 경우에는 서로에 대해 M : N 을 나타낼 때 조인객체를 두지 않는다.

위 다이어그램처럼 다대다 관계에서 회원은 상품을, 상품은 회원을 컬렉션으로 참조하면 된다.


- M : N 단방향

다대다 단방향 관계인 회원과 상품을 JPA 코드로 표현해보자. Product 엔티티는 특별한 맵핑없이 선언하면 된다.

 

@Entity
public class Product {

	@Id
	@Column(name = "PRODUCT_ID")
	private String id;
	
	private String name;

 

다대다 단방향 맵핑을 하는 User 엔티티를 살펴보자.

 

@Entity
public class User {

	@Id
	@Column(name = "USER_ID")
	private Long id;
	
	private String userName;
	
	@ManyToMany
	@JoinTable(name = "USER_PRODUCT",
			joinColumns = @JoinColumn(name = "USER_ID"),
			inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
	private List<Product> products = new ArrayList<>();

 

다중성은 다대다이므로 @ManyToMany 어노테이션을 사용한다. 조인 어노테이션은 일대다 관계에서 사용한 @JoinColumn 이 아닌 @JoinTable 어노테이션을 사용한다. 이때 @JoinTable 안에 사용하는 속성의 사용법은 아래와 같다.

  • name: 다대다 맵핑관계의 조인 관계테이블 명
  • joinColumns: User 와 맵핑할 조인컬럼 정보를 지정한다.
  • inverseJoinColumns: User 와 반대방향인 Product 와 맵핑할 조인컬럼 정보를 지정한다.

실행을 하면 @JoinTable 어노테이션 정보를 읽어서 아래와 같이 테이블이 생성한다.

만약 JPA 가 아닌 Mybatis 를 사용하면 조인 테이블을 SQL 마다 명시해서 조회해야 겠지만 JPA 는 조인테이블의 존재를 명시하지 않아도 객체 그래프를 참조하는것처럼 사용하면 된다. 회원과 상품을 저장후 탐색하는 코드를 작성해보자.

 

public static void main(String[] args) {
	SpringApplication.run(JpaApplication.class, args);
	
	EntityManager em = emf.createEntityManager();
	save(em);
	
	EntityManager em1 = emf.createEntityManager();
	find(em1);
	
	emf.close();
}

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Product product = new Product();
	product.setId("PRODUCT#1");
	product.setName("Product Name#1");
	em.persist(product);
	
	User user = new User();
	user.setId("USER#1");
	user.setUserName("User Name#1");
	user.setProducts(Arrays.asList(product));
	em.persist(user);
	
	tx.commit();
	em.close();
}

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	User user = em.find(User.class, "USER#1");
	user.getProducts()
		.forEach(System.out::println);
	
	tx.commit();
	em.close();
}

 

find 메소드에서 console 에는 아래와 같은 로그가 남는다.

 

Hibernate: 
    select
        products0_.user_id as user_id1_8_0_,
        products0_.product_id as product_2_8_0_,
        product1_.product_id as product_1_5_1_,
        product1_.name as name2_5_1_ 
    from
        user_product products0_ 
    inner join
        product product1_ 
            on products0_.product_id=product1_.product_id 
    where
        products0_.user_id=?
com.example.demo.test.Product@1ddf42dd

 

SQL from 절에 user_product 에서 user_id 로 검색한다음 product 를 join 하였다. 그리고 Sysout 으로 작성한 부분에는 인스턴스의 주소가 찍힌다.


- M : N 양방향

M : N 단방향을 알아보았으니 M : N 양방향에 대해서 알아보자. 상품에서도 회원을 컬렉션으로 참조한다.

 

@Entity
public class Product {

	@Id
	@Column(name = "PRODUCT_ID")
	private String id;
	
	private String name;

	@ManyToMany(mappedBy = "products")
	private List<User> users;

 

다중성은 다대다이므로 @ManyToMany 를 사용한다. 연관관계의 주인은 User 엔티티이므로 Product 엔티티에서 mappedBy 속성을 사용한다. User 엔티티에서 Product 를 참조하는 필드이름을 지정해준다.

 

이번에는 Product 에서 Uesr 를 참조하는 예제를 살펴보자.

 

public static void findInverse(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Product product = em.find(Product.class, "PRODUCT#1");
	List<User> users = product.getUsers();

	System.out.println("====findInverse====");
	users.forEach(user -> {
		System.out.println("User name: " + user.getUserName());
	});
	
	tx.commit();
	em.close();
}

 

User 엔티티에 제품을 추가하는 편의기능 메소드를 작성해준다. User 엔티티에서 컬렉션으로 제품목록을 참조하는 products 필드는 기본 setter 메소드로 작성하면 List 를 추가해야 한다. 단건 제품을 추가하는 편의 메소드를 추가해주자.

 

public void addProduct(Product product) {
	
	this.products.add(product);
	product.getUsers().add(this);
}

 

실행결과는 아래와 같다. user_product 에서 제품을 찾고 user 를 조인하여 조회하는 SQL 이 수행된다.

 

====findInverse====
Hibernate: 
    select
        users0_.product_id as product_2_8_0_,
        users0_.user_id as user_id1_8_0_,
        user1_.user_id as user_id1_7_1_,
        user1_.user_name as user_nam2_7_1_ 
    from
        user_product users0_ 
    inner join
        user user1_ 
            on users0_.user_id=user1_.user_id 
    where
        users0_.product_id=?
User name: User Name#1

댓글