AI エージェント向け: ドキュメントインデックスは https://www.mongodb.com/ja-jp/docs/llms.txt で利用できます。すべてのページの markdown バージョンは、いずれかの URL パスに .md を追加することで利用できます。
Docs Menu

コレクション間でエンティティを結合

このガイドでは、 Hibernetes ORM 用のMongoDB拡張機能 を使用して、関連付けによってリンクされているエンティティ全体をクエリする方法を学習できます。非表示クエリ言語(HQL)または Java 永続クエリ言語(JPQL)のいずれかを使用できます。

エンティティを結合するには、結合句で関連付けパスを移動するか、結合句でエンティティに名前を付けて ON 句を指定します。関連付け結合に ON 句を追加することもできます。

Hibernetes ORM 拡張機能は、各結合をMongoDB $lookup ステージと $unwind ステージに変換します。

注意

オン 条件の参照列

ON条件は列を比較する必要があります。 Hibernetes ORM 拡張機能は、 のようにエンティティ参照を比較したり、 のように条件内の関連付けを移動したりすることはできません。このガイドの「on m = c.movie on c.movie.title = 'Blue Jasmine'複合キー結合 」セクションで説明されているように、2 つの複合識別子全体を比較できます。 Hibernetes ORM 拡張機能がサポートする結合タイプについては、 機能の互換性 ページの クエリ サポート を参照してください。

このガイドの例では、AtlasサンプルデータセットのMovie sample_mflix.moviesコレクションを表す エンティティを使用します。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;
}
}

複合オン条件 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]);
}

Hibernetes ORM 拡張機能は、前述の結合を次のステージに変換します。

{
"$lookup": {
"from": "comments",
"localField": "_id",
"foreignField": "movie_id",
"as": "#c1_0"
}
},
{ "$unwind": "$#c1_0" }

Hibernetes 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]);
}

Hibernetes ORM 拡張機能は、前述の結合を次のステージに変換します。 preserveNullAndEmptyArrays オプションでは、一致するコメントがない映画が保持されます。

{
"$lookup": {
"from": "comments",
"localField": "_id",
"foreignField": "movie_id",
"as": "#c1_0"
}
},
{
"$unwind": {
"path": "$#c1_0",
"preserveNullAndEmptyArrays": true
}
}

Hibernetes 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());
}

Hibernetes ORM 拡張機能は、JOIN FETCH 句を等価結合と同じパイプラインステージに変換します。

ONAND条件は、 またはOR を使用した複数のフィールド比較を組み合わせることができます。複数のフィールドに関連するエンティティを結合するには、 複合条件 を使用します。複合プライマリキーを持つエンティティを結合するには、このガイドの「 複合キー結合 」セクションを参照してください。

次の例では、name と emailフィールド の両方にある users コレクションと comments コレクションを結合し、両方のフィールドが等しい場合にのみコメントがユーザーと一致します。

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]);
}

条件は複数のフィールドを比較するため、 Hibernetes ORM 拡張機能は $lookup ステージの localField と foreignField 形式ではなく、let と pipeline 形式を使用します。 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レコードでキーを宣言します。キーをクラスではなくレコードとして宣言すると、 Hibernetes ORM で識別子の値を比較するために必要な equals() メソッドと hashCode() メソッドが提供されます。

次の BookIdレコードは、Book エンティティのキーを定義します。

package org.example;
import jakarta.persistence.Embeddable;
@Embeddable
public 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;
@Embeddable
public 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."));

外部キーは、1 つのエンティティが参照するエンティティの識別子を保存するために使用するフィールドまたはフィールドのセットです。 Hibernetes ORM 拡張機能は、外部キー値を使用して、結合の各側のドキュメントを一致させます。

関連付けが複合キーを持つエンティティを対象とする場合、 Hibernetes ORM 拡張機能は外部キーをサブドキュメントとして関連付けフィールドに保存します。サブドキュメントは、ターゲット エンティティの _id サブドキュメントのコンポーネントをミラーリングします。

Review エンティティは次の形式のドキュメントを生成します。ここでは、bookフィールドはサブドキュメントとして外部キーを保持します。

{
"_id": { "bookNo": 7, "publisherId": 30 },
"comment": "An excellent read.",
"book": { "bookNo": 2, "publisherId": 10 }
}

Hibernetes ORM 拡張機能は、@ManyToOne と @OneToOne の関連付けでこのレイアウトをサポートしています。 mappedBy 要素でマッピングする関連付けの逆側には、外部キー フィールドが保存されません。

ターゲット エンティティが複合キーを持つ関連付けに結合するには、単一フィールド キーを持つエンティティと同様に、 結合句 を使用します。

次の例では、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書籍はレビューがないため、結果に表示されません。

複合キーは複数のフィールドにまたがるため、 Hibernetes 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「結合フェッチ」セクション で説明されているように、 句を使用して、親と同じクエリに複合キーの関連付けをロードすることもできます。

ON 条件では、= 演算子を使用して 2 つの複合識別子全体を比較できます。 Hibernetes ORM 拡張機能により、比較が各キー コンポーネントごとに 1 回の等価チェックに分割されます。

左外部結合では全識別子比較を使用できます。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 は、識別子を共有するレビューが見つからないため、結果に表示されません。

Hibernetes 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 アノテーションを適用することはできません。 Hibernetes ORM 拡張機能は関連付け名から外部キーフィールド名を生成し、それらを上書きするとアプリケーションの起動時に FeatureNotSupportedException がスローされます。

  • ON 条件では、> や < などの順序付け演算子と複合識別子全体を比較することはできません。代わりに、個々のキー コンポーネントを比較してください。

  • Hibernetes ORM 拡張機能は、いずれかのエンティティが複合キーを持つ場合、@ManyToMany の関連付けをサポートしていません。

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]);
}

Hibernetes 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 ドキュメントの「非表示のクエリ言語へのガイド」を参照してください。