JPA 및 Hibernate를 사용한 Java 지속성, Part 2 : 다 대다 관계

이 튜토리얼의 전반부에서는 Java Persistence API의 기본 사항을 소개하고 Hibernate 5.3.6 및 Java 8을 사용하여 JPA 애플리케이션을 구성하는 방법을 보여주었습니다. 해당 튜토리얼을 읽고 예제 애플리케이션을 공부했다면 JPA에서 JPA 엔티티 및 다 대일 관계 모델링. JPQL (JPA Query Language)을 사용하여 명명 된 쿼리를 작성하는 연습도했습니다.

이 튜토리얼의 후반부에서는 JPA 및 Hibernate에 대해 더 자세히 살펴 보겠습니다. MovieSuperHero엔터티 간의 다 대다 관계를 모델링하고 , 이러한 엔터티에 대한 개별 리포지토리를 설정하고, 엔터티를 H2 인 메모리 데이터베이스에 유지하는 방법을 학습 합니다. 또한 JPA에서 캐스케이드 작업의 역할에 대해 자세히 알아보고 CascadeType데이터베이스의 엔터티에 대한 전략을 선택하기위한 팁을 얻을 수 있습니다. 마지막으로 IDE 또는 명령 줄에서 실행할 수있는 작동하는 애플리케이션을한데 모을 것입니다.

이 튜토리얼은 JPA 기초에 초점을 맞추고 있지만 JPA의 고급 주제를 소개하는 다음 Java 팁을 확인하십시오.

  • JPA 및 Hibernate의 상속 관계
  • JPA 및 Hibernate의 복합 키
다운로드 코드 받기이 자습서에서 사용 된 예제 응용 프로그램의 소스 코드를 다운로드합니다. JavaWorld를 위해 Steven Haines가 작성했습니다.

JPA의 다 대다 관계

다 대다 관계 는 관계의 양쪽이 서로에 대한 여러 참조를 가질 수있는 엔터티를 정의합니다. 예를 들어, 영화와 슈퍼 히어로를 모델링 할 것입니다. Part 1의 Authors & Books 예제와 달리 영화에는 여러 슈퍼 히어로가있을 수 있고 슈퍼 히어로는 여러 영화에 나타날 수 있습니다. 우리의 슈퍼 히어로 Ironman과 Thor는 두 영화 "The Avengers"와 "Avengers : Infinity War"에 출연합니다.

JPA를 사용하여이 다 대다 관계를 모델링하려면 세 개의 테이블이 필요합니다.

  • 영화
  • SUPER_HERO
  • SUPERHERO_MOVIES

그림 1은 세 개의 테이블이있는 도메인 모델을 보여줍니다.

스티븐 헤인즈

참고 SuperHero_MoviesA는 테이블을 조인 사이 MovieSuperHero테이블. JPA에서 조인 테이블은 다 대다 관계를 용이하게하는 특별한 종류의 테이블입니다.

단방향 또는 양방향?

JPA에서는 @ManyToMany주석을 사용하여 다 대다 관계를 모델링합니다. 이 유형의 관계는 단방향 또는 양방향 일 수 있습니다.

  • 단방향 관계 에서는 관계의 한 항목 만 다른 항목을 가리 킵니다.
  • A의 양방향 관계 두 단체는 서로를 가리 킵니다.

우리의 예는 양방향입니다. 즉, 영화는 모든 슈퍼 히어로를 가리키고 슈퍼 히어로는 모든 영화를 가리 킵니다. 양방향 다 대다 관계에서 한 엔터티 관계를 소유하고 다른 엔터티 관계에 매핑됩니다 . 주석 의 mappedBy속성 @ManyToMany을 사용하여이 매핑을 만듭니다.

목록 1은 SuperHero클래스 의 소스 코드를 보여줍니다 .

목록 1. SuperHero.java

 package com.geekcap.javaworld.jpa.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Entity @Table(name = "SUPER_HERO") public class SuperHero { @Id @GeneratedValue private Integer id; private String name; @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinTable( name = "SuperHero_Movies", joinColumns = {@JoinColumn(name = "superhero_id")}, inverseJoinColumns = {@JoinColumn(name = "movie_id")} ) private Set movies = new HashSet(); public SuperHero() { } public SuperHero(Integer id, String name) { this.id = id; this.name = name; } public SuperHero(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set getMovies() { return movies; } @Override public String toString() { return "SuperHero{" + "id=" + id + ", + name +"\'' + ", + movies.stream().map(Movie::getTitle).collect(Collectors.toList()) +"\'' + '}'; } } 

SuperHero클래스에는 파트 1에서 익숙해야하는 몇 가지 주석이 있습니다.

  • @EntitySuperHeroJPA 엔티티로 식별 됩니다.
  • @TableSuperHero엔티티를 "SUPER_HERO"테이블에 매핑 합니다.

또한 Integerid테이블의 기본 키가 자동으로 생성되도록 지정하는 필드에 유의하십시오 .

다음으로 @ManyToMany@JoinTable주석을 살펴 보겠습니다 .

가져 오기 전략

@ManyToMany주석 에서 주목할 점 은 게 으르거나 열망 할 수있는 가져 오기 전략 을 구성하는 방법 입니다. 이 경우, 우리는 설정 한 fetchEAGER우리가를 검색 할 때 너무, SuperHero데이터베이스에서, 우리는 또한 자동으로 해당 모두 검색 할 수 있습니다 Movie들.

LAZY대신 가져 오기 를 수행하기로 선택한 경우 Movie특별히 액세스 된대로 각 항목 만 검색합니다 . 지연 가져 오기는가에 SuperHero연결되어있는 동안에 만 가능 합니다 EntityManager. 그렇지 않으면 슈퍼 히어로의 영화에 액세스하면 예외가 발생합니다. 요청시 슈퍼 히어로의 영화에 액세스 할 수 있기를 원하므로이 경우 EAGER가져 오기 전략을 선택합니다 .

CascadeType.PERSIST

캐스케이드 작업 은 슈퍼 히어로와 해당 영화가 데이터베이스에서 유지되는 방식을 정의합니다. 선택할 수있는 캐스케이드 유형 구성이 많이 있으며이 자습서의 뒷부분에서 이에 대해 자세히 설명합니다. 지금은 cascade속성을로 설정했습니다. CascadeType.PERSIST즉, 슈퍼 히어로를 저장하면 영화도 함께 저장됩니다.

테이블 결합

JoinTable사이의 many-to-many 관계를 용이하게하는 클래스 SuperHeroMovie. 이 클래스에서는 SuperHeroMovie엔터티 모두에 대한 기본 키를 저장할 테이블을 정의합니다 .

목록 1은 테이블 이름이 SuperHero_Movies. 는 열 조인superhero_id역 열 가입 할 것이다 movie_id. SuperHero(가) 열이 채워집니다 가입 있도록 엔티티, 관계를 소유 SuperHero의 기본 키. 그런 다음 역 조인 열은 관계의 다른쪽에있는 엔터티 인 Movie.

목록 1의 이러한 정의에 따라 이름이라는 새 테이블이 생성 될 것으로 예상합니다 SuperHero_Movies. 표는 두 개의 열이있을 것이다 superhero_id참조, id의 열 SUPERHERO표를하고 movie_id, 참조합니다 id의 열 MOVIE테이블을.

무비 클래스

목록 2는 Movie클래스 의 소스 코드를 보여줍니다 . 양방향 관계에서 한 엔티티는 관계 (이 경우 SuperHero)를 소유하고 다른 엔티티는 관계 에 매핑됩니다. 목록 2의 코드에는 Movie클래스에 적용된 관계 매핑이 포함되어 있습니다.

목록 2. Movie.java

 package com.geekcap.javaworld.jpa.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "MOVIE") public class Movie { @Id @GeneratedValue private Integer id; private String title; @ManyToMany(mappedBy = "movies", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) private Set superHeroes = new HashSet(); public Movie() { } public Movie(Integer id, String title) { this.id = id; this.title = title; } public Movie(String title) { this.title = title; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Set getSuperHeroes() { return superHeroes; } public void addSuperHero(SuperHero superHero) { superHeroes.add(superHero); superHero.getMovies().add(this); } @Override public String toString() { return "Movie{" + "id=" + id + ", + title +"\'' + '}'; } }

다음 속성이 @ManyToMany목록 2 의 주석에 적용됩니다 .

  • mappedBy references the field name on the SuperHero class that manages the many-to-many relationship. In this case, it references the movies field, which we defined in Listing 1 with the corresponding JoinTable.
  • cascade is configured to CascadeType.PERSIST, which means that when a Movie is saved its corresponding SuperHero entities should also be saved.
  • fetch tells the EntityManager that it should retrieve a movie's superheroes eagerly: when it loads a Movie, it should also load all corresponding SuperHero entities.

Something else to note about the Movie class is its addSuperHero() method.

When configuring entities for persistence, it isn't enough to simply add a superhero to a movie; we also need to update the other side of the relationship. This means we need to add the movie to the superhero. When both sides of the relationship are configured properly, so that the movie has a reference to the superhero and the superhero has a reference to the movie, then the join table will also be properly populated.

We've defined our two entities. Now let's look at the repositories we'll use to persist them to and from the database.

Tip! Set both sides of the table

It's a common mistake to only set one side of the relationship, persist the entity, and then observe that the join table is empty. Setting both sides of the relationship will fix this.

JPA repositories

샘플 애플리케이션에서 모든 지속성 코드를 직접 구현할 수 있지만 리포지토리 클래스를 생성하면 지속성 코드를 애플리케이션 코드와 분리 할 수 ​​있습니다. Part 1의 Books & Authors 애플리케이션에서했던 것처럼 EntityManager, 우리가 유지하는 각 엔티티에 대해 하나씩 두 개의 저장소를 초기화하는 데 사용합니다.

목록 3은 MovieRepository클래스 의 소스 코드를 보여줍니다 .

목록 3. MovieRepository.java

 package com.geekcap.javaworld.jpa.repository; import com.geekcap.javaworld.jpa.model.Movie; import javax.persistence.EntityManager; import java.util.List; import java.util.Optional; public class MovieRepository { private EntityManager entityManager; public MovieRepository(EntityManager entityManager) { this.entityManager = entityManager; } public Optional save(Movie movie) { try { entityManager.getTransaction().begin(); entityManager.persist(movie); entityManager.getTransaction().commit(); return Optional.of(movie); } catch (Exception e) { e.printStackTrace(); } return Optional.empty(); } public Optional findById(Integer id) { Movie movie = entityManager.find(Movie.class, id); return movie != null ? Optional.of(movie) : Optional.empty(); } public List findAll() { return entityManager.createQuery("from Movie").getResultList(); } public void deleteById(Integer id) { // Retrieve the movie with this ID Movie movie = entityManager.find(Movie.class, id); if (movie != null) { try { // Start a transaction because we're going to change the database entityManager.getTransaction().begin(); // Remove all references to this movie by superheroes movie.getSuperHeroes().forEach(superHero -> { superHero.getMovies().remove(movie); }); // Now remove the movie entityManager.remove(movie); // Commit the transaction entityManager.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); } } } } 

MovieRepository로 초기화 EntityManager한 후 그 지속성 방법에 사용하는 부재의 변수에 저장한다. 이러한 각 방법을 고려할 것입니다.

지속성 방법

MovieRepository의 지속성 메서드를 검토하고 의 지속성 메서드와 상호 작용하는 방법을 살펴 보겠습니다 EntityManager.