Overview
このガイドでは、MongoDB Extension for Hibernate ORMを使用して、関連付けによってリンクされたエンティティ間でクエリを実行する方法を学ぶことができます。Hibernate クエリ言語 (HQL) または Jakarta Persistence クエリ言語 (JPQL) のいずれかを使用できます。
エンティティを結合するには、join句で関連パスをナビゲートするか、join句でエンティティを指定してON句を指定します。関連付け結合にON句を追加することもできます。
Hibernate ORM 拡張機能は、各接合を MongoDB の $lookup ステージと $unwind ステージに翻訳します。
サンプル データ
映画エンティティ
このガイドの例では、Atlasサンプルデータセットの sample_mflix.moviesコレクションを表す Movie エンティティを使用します。Movie エンティティには次の定義があります。
import com.mongodb.hibernate.annotations.ObjectIdGenerator;import org.bson.types.ObjectId;import java.time.Instant;import java.util.List;import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;import jakarta.persistence.FetchType;import jakarta.persistence.Id;import jakarta.persistence.OneToMany;import jakarta.persistence.Table;
@Entity@Table(name = "movies")public class Movie { @Id @ObjectIdGenerator private ObjectId id; private String title; private String plot; private int year; private List<String> cast; private List<String> directors; private Instant released; @Embedded private Awards awards; @OneToMany(mappedBy = "movie", fetch = FetchType.LAZY) private List<Comment> comments;
public Movie(String title, String plot, int year, List<String> cast, List<String> directors, Instant released, Awards awards) { this.title = title; this.plot = plot; this.year = year; this.cast = cast; this.directors = directors; this.released = released; this.awards = awards; }
public Movie() {
}
public ObjectId getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getPlot() { return plot; }
public void setPlot(String plot) { this.plot = plot; }
public int getYear() { return year; }
public void setYear(int year) { this.year = year; }
public List<String> getCast() { return cast; }
public void setCast(List<String> cast) { this.cast = cast; }
public List<String> getDirectors() { return directors; }
public void setDirectors(List<String> directors) { this.directors = directors; }
public Awards getAwards() { return awards; }
public void setAwards(Awards awards) { this.awards = awards; }
public Instant getReleased() { return released; }
public void setReleased(Instant released) { this.released = released; }
public List<Comment> getComments() { return comments; }
public void setComments(List<Comment> comments) { this.comments = comments; }}Hibernetes ORM 用のMongoDB拡張機能を使用してこのMongoDBサンプルコレクションと交流するJavaアプリケーションを作成する方法については、Get Started チュートリアルを参照してください。
このページの例では、sample_mflix.movies コレクションと sample_mflix.comments コレクションを結合します。comments コレクションのドキュメントは、記述する映画の _id 値を movie_id フィールドに保存します。
Movie エンティティは、その comments フィールドを、それを参照する Comment エンティティにマッピングします。映画の公開日は、Movie エンティティの released フィールドに保存されます。
コメントエンティティ
次の Comment エンティティは comments コレクションにマッピングされ、その movie フィールドで関連する Movie エンティティを参照します。
package org.example;
import com.mongodb.hibernate.annotations.ObjectIdGenerator;import jakarta.persistence.Column;import org.bson.types.ObjectId;import java.time.Instant;
import jakarta.persistence.Entity;import jakarta.persistence.FetchType;import jakarta.persistence.Id;import jakarta.persistence.JoinColumn;import jakarta.persistence.ManyToOne;import jakarta.persistence.Table;
@Entity@Table(name = "comments")public class Comment { @Id @ObjectIdGenerator @Column(name = "_id") private ObjectId id; private String name; private String email; private String text; private Instant date;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "movie_id") private Movie movie;
public Comment(String name, String email, String text, Instant date, Movie movie) { this.name = name; this.email = email; this.text = text; this.date = date; this.movie = movie; }
public Comment() {
}
public ObjectId getId() { return id; }
public void setId(ObjectId id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public Instant getDate() { return date; }
public void setDate(Instant date) { this.date = date; }
public Movie getMovie() { return movie; }
public void setMovie(Movie movie) { this.movie = movie; }
}ユーザーエンティティ
Compound ON Conditions セクションの例でも、次の User エンティティを使用します。これは sample_mflix.users コレクションにマップされます。
package org.example;
import com.mongodb.hibernate.annotations.ObjectIdGenerator;import jakarta.persistence.Column;import org.bson.types.ObjectId;
import jakarta.persistence.Entity;import jakarta.persistence.Id;import jakarta.persistence.Table;
@Entity@Table(name = "users")public class User { @Id @ObjectIdGenerator @Column(name = "_id") private ObjectId id; private String name; private String email;
public User(String name, String email) { this.name = name; this.email = email; }
public User() {
}
public ObjectId getId() { return id; }
public void setId(ObjectId id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}内部結合
内部結合は、関連付けの両側で一致するエンティティのみを返します。
次の例では、内部結合を使用して、2015 年に公開された各映画のタイトルと、それにコメントした各人の名前を検索します。コメントがない映画は結果から除外されます。
var innerJoinResults = session.createQuery("select m.title, c.name from Movie m join m.comments c where m.year = :y", Object[].class) .setParameter("y", 2015) .getResultList();for (var row : innerJoinResults) { System.out.println("Title: " + row[0] + ", Commenter: " + row[1]);}var innerJoinResults = entityManager.createQuery("select m.title, c.name from Movie m join m.comments c where m.year = :y", Object[].class) .setParameter("y", 2015) .getResultList();for (var row : innerJoinResults) { System.out.println("Title: " + row[0] + ", Commenter: " + row[1]);}Hibernate ORM拡張機能は、前述の結合を次のステージに変換します。
{ "$lookup": { "from": "comments", "localField": "_id", "foreignField": "movie_id", "as": "#c1_0" }},{ "$unwind": "$#c1_0" }Hibernate ORM拡張機能は、クエリステートメントのエイリアスからasフィールド名を生成し、それを使用してパイプラインの後のステージで結合されたフィールドを参照します。
左外部結合
左外部結合は、関連付けの左側にあるすべてのエンティティを返します。これには、右側に一致するものがないエンティティも含まれます。右側に一致するものがないエンティティは、結合の右側に対して null の結果を返します。
次の例では、左外部結合を使用して、2015に公開された各映画のタイトルと、その映画にコメントした各ユーザーの名前を検索します。コメントがない映画も結果に含まれます。
var leftJoinResults = session.createQuery("select m.title, c.name from Movie m left join m.comments c where m.year = :y", Object[].class) .setParameter("y", 2015) .getResultList();for (var row : leftJoinResults) { System.out.println("Title: " + row[0] + ", Commenter: " + row[1]);}var leftJoinResults = entityManager.createQuery("select m.title, c.name from Movie m left join m.comments c where m.year = :y", Object[].class) .setParameter("y", 2015) .getResultList();for (var row : leftJoinResults) { System.out.println("Title: " + row[0] + ", Commenter: " + row[1]);}Hibernate ORM拡張機能は、前述の結合を次のステージに変換します。preserveNullAndEmptyArraysオプションは、一致するコメントがない映画を保持します。
{ "$lookup": { "from": "comments", "localField": "_id", "foreignField": "movie_id", "as": "#c1_0" }},{ "$unwind": { "path": "$#c1_0", "preserveNullAndEmptyArrays": true }}Hibernate ORM拡張機能は、クエリステートメントのエイリアスからasフィールド名を生成し、それを使用してパイプラインの後のステージで結合されたフィールドを参照します。
結合取得
JOIN FETCH 句は、親と同じクエリで関連エンティティを読み込むため、セッションが閉じた後でも関連付けにアクセスできます。
次の例では、JOIN FETCH 句を使用してコメントを検索し、各コメントが参照する Movie エンティティを読み込みます。
var comments = session.createQuery("from Comment c join fetch c.movie where c.name = :n", Comment.class) .setParameter("n", "Andrea Le") .getResultList();for (var c : comments) { System.out.println("Commenter: " + c.getName() + ", Title: " + c.getMovie().getTitle());}var comments = entityManager.createQuery("select c from Comment c join fetch c.movie where c.name = :n", Comment.class) .setParameter("n", "Andrea Le") .getResultList();for (var c : comments) { System.out.println("Commenter: " + c.getName() + ", Title: " + c.getMovie().getTitle());}Hibernate ORM拡張機能は、JOIN FETCH句を同等の結合と同じパイプラインステージに変換します。
複合ON条件
ON 条件は、AND または OR を使用して複数のフィールド比較を結合できます。複数のフィールドで関連しているエンティティを結合するには、複合条件を使用します。複合プライマリキーを持つエンティティを結合するには、このガイドの複合キー結合セクションを参照してください。
次の例では、users コレクションと comments コレクションを name フィールドと email フィールドの両方で結合し、両方のフィールドが等しい場合にのみコメントがユーザーと一致するようにします。
var compoundOnResults = session.createQuery("select u.name, c.text from User u join Comment c on u.name = c.name and u.email = c.email", Object[].class) .getResultList();for (var row : compoundOnResults) { System.out.println("Name: " + row[0] + ", Comment: " + row[1]);}var compoundOnResults = entityManager.createQuery("select u.name, c.text from User u join Comment c on u.name = c.name and u.email = c.email", Object[].class) .getResultList();for (var row : compoundOnResults) { System.out.println("Name: " + row[0] + ", Comment: " + row[1]);}条件が複数のフィールドを比較するため、Hibernate ORM拡張機能は$lookupステージのletおよびpipeline形式を使用し、localFieldおよびforeignField形式は使用しません。letオプションは外部コレクションの各フィールドを変数にバインドし、pipelineオプションはそれらの変数を$expr演算子内の結合されたコレクションのフィールドと比較します。
{ "$lookup": { "from": "comments", "let": { "v0_u1_0_name": "$name", "v1_u1_0_email": "$email" }, "pipeline": [ { "$match": { "$expr": { "$and": [ { "$eq": [ "$$v0_u1_0_name", "$name" ] }, { "$eq": [ "$$v1_u1_0_email", "$email" ] } ] } } } ], "as": "#c1_0" }},{ "$unwind": "$#c1_0" }複合キージョイン
@EmbeddedId アノテーションで宣言された複合キーをプライマリキーとするエンティティを結合できます。複合キーの定義方法を学ぶには、「エンティティの作成」ガイドの「複合プライマリキー」セクションを参照してください。
このセクションの例では、次の Book と Review エンティティを使用します。これらは、publisherId コンポーネントと bookNo コンポーネントの両方をキーとしています。Review エンティティは、関連する Book エンティティを book フィールド内で参照します。
各エンティティは、個別の @Embeddable レコードでキーを宣言します。キーをクラスではなくレコードとして宣言することで、Hibernate ORM が識別子の値を比較するために必要な equals() メソッドと hashCode() メソッドが提供されます。
次の BookId レコードは、Book エンティティのキーを定義します。
package org.example;
import jakarta.persistence.Embeddable;
@Embeddablepublic record BookId(long publisherId, long bookNo) {}Book エンティティには、次の定義があります。
package org.example;
import jakarta.persistence.EmbeddedId;import jakarta.persistence.Entity;import jakarta.persistence.Table;
@Entity(name = "Book")@Table(name = "books")public class Book { @EmbeddedId private BookId id; private String title;
public Book(BookId id, String title) { this.id = id; this.title = title; }
public Book() {
}
public BookId getId() { return id; }
public void setId(BookId id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
}次の ReviewId レコードは、Review エンティティのキーを定義します。
package org.example;
import jakarta.persistence.Embeddable;
@Embeddablepublic record ReviewId(long publisherId, long bookNo) {}Review エンティティには、次の定義があります。book フィールドは、Book エンティティへの関連付けをマップします。
package org.example;
import jakarta.persistence.EmbeddedId;import jakarta.persistence.Entity;import jakarta.persistence.ManyToOne;import jakarta.persistence.Table;
@Entity(name = "Review")@Table(name = "reviews")public class Review { @EmbeddedId private ReviewId id; private String comment;
@ManyToOne private Book book;
public Review(ReviewId id, Book book, String comment) { this.id = id; this.book = book; this.comment = comment; }
public Review() {
}
public ReviewId getId() { return id; }
public void setId(ReviewId id) { this.id = id; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
public Book getBook() { return book; }
public void setBook(Book book) { this.book = book; }
}次のコードは、このセクションの例で使用されるドキュメントを挿入します。
var blueDoor = new Book(new BookId(10, 2), "The Blue Door");var winterLight = new Book(new BookId(10, 3), "Winter Light");var saltAndStone = new Book(new BookId(20, 1), "Salt and Stone");session.persist(blueDoor);session.persist(winterLight);session.persist(saltAndStone);
session.persist(new Review(new ReviewId(30, 7), blueDoor, "Gripping from the first page."));session.persist(new Review(new ReviewId(30, 8), blueDoor, "A slow but rewarding read."));session.persist(new Review(new ReviewId(20, 1), saltAndStone, "Beautifully written."));session.persist(new Review(new ReviewId(10, 3), saltAndStone, "Dense, but worth the effort."));var blueDoor = new Book(new BookId(10, 2), "The Blue Door");var winterLight = new Book(new BookId(10, 3), "Winter Light");var saltAndStone = new Book(new BookId(20, 1), "Salt and Stone");entityManager.persist(blueDoor);entityManager.persist(winterLight);entityManager.persist(saltAndStone);
entityManager.persist(new Review(new ReviewId(30, 7), blueDoor, "Gripping from the first page."));entityManager.persist(new Review(new ReviewId(30, 8), blueDoor, "A slow but rewarding read."));entityManager.persist(new Review(new ReviewId(20, 1), saltAndStone, "Beautifully written."));entityManager.persist(new Review(new ReviewId(10, 3), saltAndStone, "Dense, but worth the effort."));複合外部キーのストレージ
**外部キー**とは、あるエンティティが使用して参照するエンティティの識別子を保存するフィールドまたはフィールドのセットです。Hibernate ORM 拡張機能は、外部キー値を使用してジョインの各側のドキュメントを照合します。
関連付けが複合キーを持つエンティティをターゲットにする場合、Hibernate ORM 拡張機能は外部キーを関連付けフィールドにサブドキュメントとして保存します。サブドキュメントは、ターゲットエンティティの _id サブドキュメントのコンポーネントをミラーリングします。
Review エンティティは、次の形式でドキュメントを生成します。この形式では、book フィールドが外部キーをサブドキュメントとして保持します。
{ "_id": { "bookNo": 7, "publisherId": 30 }, "comment": "An excellent read.", "book": { "bookNo": 2, "publisherId": 10 }}Hibernate ORM拡張機能は、@ManyToOneおよび@OneToOneの関連付けに対してこのレイアウトをサポートしています。mappedBy要素でマッピングされる関連付けの逆側には、外部キーフィールドは保存されません。
複合キーアソシエーションを結合する
ターゲットエンティティが複合キーを持つアソシエーションを結合するには、単一フィールドキーを持つエンティティの場合と同様に、join句を使用します。
次の例では、Reviewエンティティのbookフィールドで内部結合を実行し、各検討することの識別子と、それが検討する書籍の識別子を検索します。
var compositeJoinResults = session.createQuery("select r.id, b.id, b.title from Review r join r.book b", Object[].class) .getResultList();for (var row : compositeJoinResults) { System.out.println("Review: " + row[0] + ", Book: " + row[1] + ", Book Title: " + row[2]);}var compositeJoinResults = entityManager.createQuery("select r.id, b.id, b.title from Review r join r.book b", Object[].class) .getResultList();for (var row : compositeJoinResults) { System.out.println("Review: " + row[0] + ", Book: " + row[1] + ", Book Title: " + row[2]);}Winter Light という本には検討するがないため、結果には表示されません。
複合キーは複数のフィールドにまたがるため、Hibernate ORM拡張機能は$lookupステージのletおよびpipeline形式を使用します。letオプションは、各外部キーコンポーネントを変数にバインドします。pipelineオプションは、それらの変数を$expr演算子のターゲット_idサブドキュメントのコンポーネントと比較します。
{ "$lookup": { "from": "books", "let": { "v0_r1_0_book_publisherId": "$book.publisherId", "v1_r1_0_book_bookNo": "$book.bookNo" }, "pipeline": [ { "$match": { "$expr": { "$and": [ { "$eq": [ "$_id.publisherId", "$$v0_r1_0_book_publisherId" ] }, { "$eq": [ "$_id.bookNo", "$$v1_r1_0_book_bookNo" ] } ] } } } ], "as": "#b1_0" }},{ "$unwind": "$#b1_0" }JOIN FETCH クローズを使用することで、parent と同じクエリ内で複合キー関連付けを読み込むこともできます。詳細は、このガイドのJoin Fetchセクションを参照してください。
複合識別子全体を比較する
ON 条件では、= 演算子を使用して 2 つの複合識別子全体を比較できます。Hibernate ORM 拡張機能は、比較を各キーコンポーネントの等価性チェックに分解します。
左外部結合で識別子全体を比較できます。また、ON b.id = r.id AND b.title = r.commentのようにAND演算子を使用して、追加の条件と組み合わせることもできます。
次の例では、各書籍を一致する識別子を持つレビューと結合します。
var wholeIdResults = session.createQuery("select b.id, b.title, r.id from Book b join Review r on b.id = r.id", Object[].class) .getResultList();for (var row : wholeIdResults) { System.out.println("Book Title: " + row[1] + ", Book: " + row[0] + ", Review: " + row[2]);}var wholeIdResults = entityManager.createQuery("select b.id, b.title, r.id from Book b join Review r on b.id = r.id", Object[].class) .getResultList();for (var row : wholeIdResults) { System.out.println("Book Title: " + row[1] + ", Book: " + row[0] + ", Review: " + row[2]);}The Blue Door 検討するでその識別子を共有するものがないため、結果に表示されません。
Hibernate ORM拡張機能は、前述の結合を次のステージに変換します。
{ "$lookup": { "from": "reviews", "let": { "v0_b1_0__id_publisherId": "$_id.publisherId", "v1_b1_0__id_bookNo": "$_id.bookNo" }, "pipeline": [ { "$match": { "$expr": { "$and": [ { "$eq": [ "$$v0_b1_0__id_publisherId", "$_id.publisherId" ] }, { "$eq": [ "$$v1_b1_0__id_bookNo", "$_id.bookNo" ] } ] } } } ], "as": "#r1_0" }},{ "$unwind": "$#r1_0" }複合キー結合の制限
複合キー結合には、以下の制限があります。
ターゲットエンティティが複合キーを持つ関連付けに
@JoinColumnまたは@JoinColumnsアノテーションを適用することはできません。Hibernate ORM拡張機能は、関連付け名から外部キーフィールド名を導出します。これらをオーバーライドすると、アプリケーションの起動時にFeatureNotSupportedExceptionがスローされます。ON条件で>や<などの順序演算子を使用して、複合識別子全体を比較することはできません。その代わりに、個別の主要コンポーネントを比較してください。Hibernate ORM拡張機能は、いずれかのエンティティが複合キーを持つ場合、
@ManyToMany関連付けをサポートしていません。
範囲および不等号のON条件
ON条件では、範囲演算子や非等価演算子を使用してフィールドを比較できます。例えば<や>などがあります。
以下の例では、movies コレクションと comments コレクションを結合して、2015 年に公開された映画に対するコメントのうち、映画の公開日以降に書き込みされたものを検索します。
var nonEquijoinResults = session.createQuery("select m.title, c.name from Movie m join m.comments c on c.date > m.released where m.year = :y", Object[].class) .setParameter("y", 2015) .getResultList();for (var row : nonEquijoinResults) { System.out.println("Title: " + row[0] + ", Commenter: " + row[1]);}var nonEquijoinResults = entityManager.createQuery("select m.title, c.name from Movie m join m.comments c on c.date > m.released where m.year = :y", Object[].class) .setParameter("y", 2015) .getResultList();for (var row : nonEquijoinResults) { System.out.println("Title: " + row[0] + ", Commenter: " + row[1]);}Hibernate ORM拡張機能は、関連付けのキー一致とON条件を単一の$expr演算子に結合し、$lookupステージのletおよびpipeline形式を使用します。
{ "$lookup": { "from": "comments", "let": { "v0_m1_0__id": "$_id", "v1_m1_0_released": "$released" }, "pipeline": [ { "$match": { "$expr": { "$and": [ { "$eq": [ "$$v0_m1_0__id", "$movie_id" ] }, { "$gt": [ "$date", "$$v1_m1_0_released" ] } ] } } } ], "as": "#c1_0" }},{ "$unwind": "$#c1_0" }詳細情報
MongoDB データに対するその他の操作の実行について詳しくは、CRUD 操作またはクエリの指定ガイドをご覧ください。
HQL と JQL を使用してクエリを実行する方法の詳細については、非表示の ORM ドキュメントの「非表示のクエリ言語へのガイド」を参照してください。