はじめに
この記事では、Java の JPA と Hibernate によるリレーションシップ マッピングについて詳しく説明します。 ドメインモデルをデータベース構造に直接マッピングすることができ、Connection
、ResultSet
などの面倒な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
です。モデルに設定する前に、関係には所有側、できればデータベースで外部キーを保持する側があることを思い出しましょう。
この例では、
CourseMaterial
がCourse
を参照することは理にかなっています (逆も可能ですが)。方向について言えば、関係を双方向にし、コースがあればそのコースにある資料にアクセスできるようにしましょう。
Course
クラスに次のように追加します。@OneToOne(mappedBy = "course")private CourseMaterial material;
ここで、
Course
内の教材はCourseMaterial
エンティティのcourse
フィールドによってすでにマッピングされていることを Hibernate に伝えているのです。関係を双方向にすることに加えて、カスケード操作を追加したり、エンティティを熱心にまたは遅延的に読み込ませることもできます。
Many-to-Many
さて、最後になりましたが。 多対多のリレーションシップ。
事実上、データベースでは、多対多のリレーションシップは、他の両方のテーブルを参照する中間テーブルを含みます。
つまり、この例では、多対多の関係は、
Student
とCourse
インスタンスの間のものになります。 ただし、今回は、リレーションシップを表すテーブルを設定するために@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);
このコードを実行すると、
Course
、Teacher
、Student
のインスタンスとその関係が持続されます。 たとえば、永続化されたコースから学生を取得し、すべてがうまくいくかどうかを確認してみましょう。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で見ることができます。
- Guide to JAP with Hibernate.JPA with Hibernate – Relationship Mapping