JPA と Hibernate のガイド – リレーションシップ マッピング

はじめに

この記事では、Java の JPA と Hibernate によるリレーションシップ マッピングについて詳しく説明します。 ドメインモデルをデータベース構造に直接マッピングすることができ、ConnectionResultSetなどの面倒なJDBCコンポーネントを使用せずに、コード内でオブジェクトを柔軟に操作することが可能です。 今回はリレーションシップマッピングを取り上げます。

  • Guide to JPA with Hibernate – Basic Mapping
  • Guide to JPA with Hibernate – Relationships Mapping (you are here)
  • Guide to JPA with Hibernate.JPA with Hibernate.JPA – Relationships Mapping
    • Guide to JAP with Hibernate.JPA with Hibernate – Relationship Mapping
        – JPA with JPA with Hibernate – Relationship Mapping

      • Guide to JPA with Hibernate – Querying (Coming soon!)

      始める前に、このシリーズの前編で使用した例を思い出してみましょう。 このモデルは次のようになります。

      見てわかるように、特定のプロパティを持ついくつかのクラスがあります。 これらのクラスは、それらの間の関係を持っています。 この記事の終わりまでに、これらのクラスをすべてデータベース テーブルにマッピングし、それらの関係を保持します。

      さらに、JDBC の手間をかけずに、オブジェクトとしてそれらを取得し操作できるようになります。 クラス図を見てみると、いくつかの関係があることがわかります。

      教師とコース – 学生とコース – コースと教材。

      学生とアドレスの間の接続もありますが、これらは関係とは見なされていません。 これは、Address がエンティティではない (つまり、それ自身のテーブルにマッピングされていない) ためです。

      • One-to-Many
      • Many-to-One
      • One-to-One
      • Many-to-Many

      これらの関係に対して1つずつ対処していきましょう。

      One-to-Many/Many-to-One

      まず、密接な関係にあるOne-to-ManyとMany-to-Oneの関係から始めましょう。

      One-to-Many リレーションシップとは何ですか。

      その名前が示すように、これは、あるエンティティを他の多くのエンティティにリンクするリレーションシップです。 教師は複数のコースを提供できますが、コースは 1 人の教師によってのみ提供されます (これが多対一の視点 – 1 人の教師に多数のコース)。

      別の例として、ソーシャル メディアでは、写真に多数のコメントを付けることができますが、それらのコメントはそれぞれその 1 つの写真に属しています。

      この関係をマップする方法の詳細に入る前に、エンティティを作成しましょう。

      @Entitypublic class Teacher { private String firstName; private String lastName;}@Entitypublic class Course { private String title;}

      ここで、Teacher クラスのフィールドには、コースのリストが含まれている必要があります。 このリレーションシップをデータベースにマッピングする場合、他のエンティティにエンティティのリストを含めることはできませんので、@OneToManyアノテーションを付けます。

      @OneToManyprivate List<Course> courses;

      ここではフィールドタイプとしてListを使用しましたが、SetまたはMapにすることもできます(ただしこの場合は少し設定が必要です)。

      JPAは、リレーションシップをどのように処理するかについての私たちの入力に応じて、これを実行します。

      @OneToMany@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private List<Course> courses;

      このアノテーションを使用すると、COURSEテーブルがTEACHERテーブルのID列を参照する外部キー列TEACHER_IDを持っていなければならないとJPAに伝えます。

      これらのテーブルにデータを追加してみましょう。

      insert into TEACHER(ID, LASTNAME, FIRSTNAME) values(1, 'Doe', 'Jane');insert into COURSE(ID, TEACHER_ID, TITLE) values(1, 1, 'Java 101');insert into COURSE(ID, TEACHER_ID, TITLE) values(2, 1, 'SQL 101');insert into COURSE(ID, TEACHER_ID, TITLE) values(3, 1, 'JPA 101');

      そして、関係が期待通りに機能しているかどうかを確認してみましょう。

      Teacher foundTeacher = entityManager.find(Teacher.class, 1L);assertThat(foundTeacher.id()).isEqualTo(1L);assertThat(foundTeacher.lastName()).isEqualTo("Doe");assertThat(foundTeacher.firstName()).isEqualTo("Jane");assertThat(foundTeacher.courses()) .extracting(Course::title) .containsExactly("Java 101", "SQL 101", "JPA 101");

      インスタンスを取得すると、教師のコースが自動的に収集されることがわかります。

      Java でのテストに慣れていない場合は、JUnit 5 を使用した Java でのユニット テストを読むとよいでしょう!

      所有側と双方向性

      前の例では、Teacher クラスは一対多関係の所有側と呼ばれます。

      このリレーションシップでは、Course を参照側と呼びます。 を所有側にするには、代わりに Teacher フィールドを Course クラスの @ManyToOne と対応させます。

      Course foundCourse = entityManager.find(Course.class, 1L);assertThat(foundCourse.id()).isEqualTo(1L);assertThat(foundCourse.title()).isEqualTo("Java 101");assertThat(foundCourse.teacher().lastName()).isEqualTo("Doe");assertThat(foundCourse.teacher().firstName()).isEqualTo("Jane");

      今回は、@OneToMany と同じように @ManyToOne アノテーションを使用しました。

      注意: 外部キーが保持されるクラス/テーブルにリレーションの所有者側を置くことは、良い習慣です。

      @Entitypublic class Teacher { // ... @OneToMany(mappedBy = "teacher") private List<Course> courses;}@Entitypublic class Course { // ... @ManyToOne @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID") private Teacher teacher;}

      私たちは、 マッピングを Course エンティティ上に維持します。

      ここで重要なのは、参照する側の @OneToMany アノテーションで mappedBy フラグを使用することです。 2 つの一方向の関係が存在することになります。

      これを使用すると、フィールドが別のエンティティによってすでにマッピングされていることをJPAに伝えることになります。

      Eager vs Lazy Loading

      注目に値するもうひとつのことは、eager および lazy loading です。 すべてのリレーションシップがマップされているため、不要な場合は、ソフトウェアのメモリにあまりにも多くのエンティティを置くことによる影響を避けることが賢明です。

      Courseが重いオブジェクトで、ある操作のためにデータベースからすべてのTeacherオブジェクトをロードすると想像してください。 この操作ではコースを取得または使用する必要はありませんが、Teacher オブジェクトと一緒にロードされています。

      これは、アプリケーションのパフォーマンスにとって破壊的です。 技術的には、これはデータ転送オブジェクト デザイン パターンを使用して、コースなしで Teacher 情報を取得することで解決できます。

      しかし、このパターンから得られるものがコースを除外するだけなら、これは大規模なやりすぎになることがあります。

      ありがたいことに、JPA は先を見越して、1 対多のリレーションシップはデフォルトで遅延ロードされるようにしました。

      この例では、Teacher#courses メソッドを呼び出すまで、コースはデータベースから取得されないことを意味します。

      対照的に、多対一の関係はデフォルトで熱心で、エンティティが読み込まれると同時に関係が読み込まれることを意味します。

      両方のアノテーションの fetch 引数を設定することにより、これらの特性を変更することができます。 コースは、Teacher オブジェクトをロードするとすぐに、熱心にロードされるでしょう。 対照的に、teacher は、courses をフェッチしても、その時点で不要な場合はロードされません。

      任意性

      さて、任意性について説明しましょう。

      関係は任意または必須の場合があります。 一方、Many-to-One 側では、それを必須にするオプションがあります。

      デフォルトでは、リレーションシップはオプションで、教師を割り当てずに Course を保存できることを意味します。

      @ManyToOne(optional = false)@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

      このように、もはや先生を割り当てずにコースを保存することはできません:

      Course course = new Course("C# 101");assertThrows(Exception.class, () -> entityManager.persist(course));

      しかし、先生に割り当てれば、再びうまくいきます:

      Teacher teacher = new Teacher();teacher.setLastName("Doe");teacher.setFirstName("Will");Course course = new Course("C# 101");course.setTeacher(teacher);entityManager.persist(course);

      まあ、少なくとも、そのように見えます。 もしこのコードを実行したら、例外が投げられたでしょう:

      javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course

      これはなぜですか? 永続化しようとしている Course オブジェクトには、有効な Teacher オブジェクトが設定されています。

      したがって、Teacherオブジェクトは管理されたエンティティではありません。

      Teacher teacher = new Teacher();teacher.setLastName("Doe");teacher.setFirstName("Will");entityManager.persist(teacher);Course course = new Course("C# 101");course.setTeacher(teacher);entityManager.persist(course);entityManager.flush();

      このコードを実行すると、両方のエンティティを永続化し、それらの間の関係を維持します。

      Cascading Operations

      ただし、別のことも可能でした。カスケードを使用して、Course オブジェクトの永続化の際に Teacher オブジェクトの永続化を伝達することができました。

      これはより理にかなっており、例外をスローした最初の例のように期待通りに動作します。

      これを行うには、アノテーションの cascade フラグを変更します:

      @ManyToOne(optional = false, cascade = CascadeType.PERSIST)@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

      この方法で、Hibernate はこの関係で必要なオブジェクトを同様に持続させることを知っています。 PERSIST, MERGE, REMOVE, REFRESH, DETACH, そして ALL(前のものをすべて組み合わせたもの)。

      また、リレーションの一対多側にcascade引数を置いて、操作が教師からそのコースへもカスケードできるようにすることもできます。

      One-to-One

      一対多/多対一のリレーションとその設定を通じて、JPAにおけるリレーションシップ・マッピングの基礎を設定したので、次は一対一のリレーションに進みましょう。

      今回は、片方に1つのエンティティ、もう片方にたくさんのエンティティという関係ではなく、片方に最大1つのエンティティという関係にします。

      これは、たとえばCourseとそのCourseMaterialの関係などです。

      @Entitypublic class CourseMaterial { @Id private Long id; private String url;}

      1 つのエンティティを 1 つの他のエンティティにマッピングする注釈は、驚くことに、@OneToOne です。

      モデルに設定する前に、関係には所有側、できればデータベースで外部キーを保持する側があることを思い出しましょう。

      この例では、CourseMaterialCourse を参照することは理にかなっています (逆も可能ですが)。

      方向について言えば、関係を双方向にし、コースがあればそのコースにある資料にアクセスできるようにしましょう。 Course クラスに次のように追加します。

      @OneToOne(mappedBy = "course")private CourseMaterial material;

      ここで、Course 内の教材は CourseMaterial エンティティの course フィールドによってすでにマッピングされていることを Hibernate に伝えているのです。

      関係を双方向にすることに加えて、カスケード操作を追加したり、エンティティを熱心にまたは遅延的に読み込ませることもできます。

      Many-to-Many

      さて、最後になりましたが。 多対多のリレーションシップ。

      事実上、データベースでは、多対多のリレーションシップは、他の両方のテーブルを参照する中間テーブルを含みます。

      つまり、この例では、多対多の関係は、StudentCourse インスタンスの間のものになります。 ただし、今回は、リレーションシップを表すテーブルを設定するために @JoinTable アノテーションも使用します。 アノテーションはいくつかのパラメータを取ります。 まず最初に、テーブルに名前を付ける必要があります。 ここでは STUDENTS_COURSES.

      その後、STUDENTS_COURSES に入力するために結合するカラムを Hibernate に伝える必要があります。 最初のパラメーターである joinColumns は、テーブル内のリレーションの所有者側の結合カラム (外部キー) をどのように構成するかを定義します。 この場合、所有側は Course です。

      一方、inverseJoinColumns パラメータは同じことを参照側 (Student) に対して行います。

      学生とコースのデータセットを設定してみましょう。

      Student johnDoe = new Student();johnDoe.setFirstName("John");johnDoe.setLastName("Doe");johnDoe.setBirthDateAsLocalDate(LocalDate.of(2000, FEBRUARY, 18));johnDoe.setGender(MALE);johnDoe.setWantsNewsletter(true);johnDoe.setAddress(new Address("Baker Street", "221B", "London"));entityManager.persist(johnDoe);Student willDoe = new Student();willDoe.setFirstName("Will");willDoe.setLastName("Doe");willDoe.setBirthDateAsLocalDate(LocalDate.of(2001, APRIL, 4));willDoe.setGender(MALE);willDoe.setWantsNewsletter(false);willDoe.setAddress(new Address("Washington Avenue", "23", "Oxford"));entityManager.persist(willDoe);Teacher teacher = new Teacher();teacher.setFirstName("Jane");teacher.setLastName("Doe");entityManager.persist(teacher);Course javaCourse = new Course("Java 101");javaCourse.setTeacher(teacher);entityManager.persist(javaCourse);Course sqlCourse = new Course("SQL 101");sqlCourse.setTeacher(teacher);entityManager.persist(sqlCourse);

      もちろん、これはいきなりは動作しないでしょう。 コースに学生を追加できるメソッドを追加する必要があります。

      public class Course { private List<Student> students = new ArrayList<>(); public void addStudent(Student student) { this.students.add(student); }}

      これでデータセットが完成です。

      Course javaCourse = new Course("Java 101");javaCourse.setTeacher(teacher);javaCourse.addStudent(johnDoe);javaCourse.addStudent(willDoe);entityManager.persist(javaCourse);Course sqlCourse = new Course("SQL 101");sqlCourse.setTeacher(teacher);sqlCourse.addStudent(johnDoe);entityManager.persist(sqlCourse);

      このコードを実行すると、CourseTeacherStudent のインスタンスとその関係が持続されます。 たとえば、永続化されたコースから学生を取得し、すべてがうまくいくかどうかを確認してみましょう。

      Course courseWithMultipleStudents = entityManager.find(Course.class, 1L);assertThat(courseWithMultipleStudents).isNotNull();assertThat(courseWithMultipleStudents.students()) .hasSize(2) .extracting(Student::firstName) .containsExactly("John", "Will");

      もちろん、以前のリレーションシップで行ったのと同じ方法で、リレーションシップを双方向にマッピングすることができます。

      また、操作をカスケードしたり、エンティティを遅延ロードまたは待機ロード (Many-to-Many リレーションシップはデフォルトで遅延) するかどうかを定義したりすることもできます。 多対一、一対多、多対多、一対一のリレーションシップを取り上げました。 さらに、カスケード操作、双方向性、オプション性、eager/lazy loading fetch-typesについても調べました。

      このシリーズのコードはGitHubで見ることができます。

コメントを残す

メールアドレスが公開されることはありません。