JPAおよびHibernateを使用したJavaPersistence、パート2:多対多の関係

このチュートリアルの前半では、Java Persistence APIの基礎を紹介し、Hibernate5.3.6とJava8を使用してJPAアプリケーションを構成する方法を示しました。このチュートリアルを読んでそのサンプルアプリケーションを学習した場合は、の基本を理解しています。 JPAエンティティとJPAでの多対1の関係のモデリング。また、JPAクエリ言語(JPQL)を使用して名前付きクエリを作成する練習もしました。

チュートリアルのこの後半では、JPAとHibernateについて詳しく説明します。MovieSuperHeroエンティティ間の多対多の関係をモデル化し、これらのエンティティの個別のリポジトリを設定し、エンティティをH2インメモリデータベースに永続化する方法を学習します。また、JPAでのカスケード操作の役割についてさらに学習CascadeTypeし、データベース内のエンティティーの戦略を選択するためのヒントを取得します。最後に、IDEまたはコマンドラインで実行できる動作中のアプリケーションをまとめます。

このチュートリアルはJPAの基礎に焦点を当てていますが、JPAのより高度なトピックを紹介する次のJavaのヒントを必ず確認してください。

  • JPAとHibernateの継承関係
  • JPAとHibernateの複合キー
ダウンロードコードを取得するこのチュートリアルで使用するアプリケーションの例のソースコードをダウンロードします。JavaWorldのためにStevenHainesによって作成されました。

JPAにおける多対多の関係

多対多の関係は、関係の両側が相互に複数の参照を持つことができるエンティティを定義します。この例では、映画とスーパーヒーローをモデル化します。パート1のAuthors&Booksの例とは異なり、映画には複数のスーパーヒーローを含めることができ、スーパーヒーローは複数の映画に登場する可能性があります。私たちのスーパーヒーローであるアイアンマンとトールは、どちらも「アベンジャーズ」と「アベンジャーズ:インフィニティウォー」の2本の映画に出演しています。

JPAを使用してこの多対多の関係をモデル化するには、次の3つのテーブルが必要です。

  • 映画
  • スーパーヒーロー
  • SUPERHERO_MOVIES

図1は、3つのテーブルを持つドメインモデルを示しています。

スティーブンヘインズ

とテーブルの間の結合テーブルSuperHero_Moviesあることに注意してください。JPAでは、結合テーブルは、多対多の関係を容易にする特別な種類のテーブルです。MovieSuperHero

一方向または双方向?

JPAでは、@ManyToManyアノテーションを使用して多対多の関係をモデル化します。このタイプの関係は、単方向または双方向にすることができます。

  • 一方向の関係の関係で唯一のエンティティが他のポイント。
  • 双方向の関係の両方のエンティティがお互いを指します。

この例は双方向です。つまり、映画はそのすべてのスーパーヒーローを指し、スーパーヒーローはすべての映画を指します。双方向の多対多の関係では、一方のエンティティ関係を所有し、もう一方のエンティティ関係にマップされます。注釈の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アノテーションで注目すべきことは、フェッチ戦略をどのように構成するかです。これは、怠惰な場合も熱心な場合もあります。この場合、をに設定しfetchているEAGERためSuperHero、データベースからを取得すると、対応するすべてのが自動的に取得されますMovie

LAZY代わりにフェッチを実行することを選択した場合、Movie特別にアクセスされたときにそれぞれを取得するだけです。遅延フェッチはSuperHero、がEntityManager;に接続されている場合にのみ可能です。そうしないと、スーパーヒーローの映画にアクセスすると例外がスローされます。スーパーヒーローの映画にオンデマンドでアクセスできるようにしたいので、この場合はEAGERフェッチ戦略を選択します。

CascadeType.PERSIST

カスケード操作は、スーパーヒーローとそれに対応する映画がデータベースとの間でどのように永続化されるかを定義します。選択できるカスケードタイプの構成は多数あります。これらについては、このチュートリアルの後半で詳しく説明します。今のところ、cascade属性をCascadeType.PERSISTに設定していることに注意してください。これは、スーパーヒーローを保存すると、その映画も保存されることを意味します。

テーブルを結合する

JoinTable間の多対多の関係を容易クラスであるSuperHeroとはMovie。このクラスでは、SuperHeroMovieエンティティの両方の主キーを格納するテーブルを定義します。

リスト1は、テーブル名がになることを指定していますSuperHero_Moviesカラムは参加になりsuperhero_id、そして逆結合列になりますmovie_idSuperHero列が移入されますので、参加主体は、関係を所有しているSuperHeroの主キー。次に、逆結合列は、関係の反対側にあるエンティティであるを参照しますMovie

リスト1のこれらの定義に基づいて、という名前の新しいテーブルが作成されると予想されSuperHero_Moviesます。:テーブルは、2つの列があります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

すべての永続性コードをサンプルアプリケーションに直接実装できますが、リポジトリクラスを作成すると、永続性コードをアプリケーションコードから分離できます。パート1のBooks&Authorsアプリケーションで行ったのと同じように、を作成しEntityManager、それを使用して、永続化するエンティティごとに1つずつ、2つのリポジトリを初期化します。

リスト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