对于 AI 代理:可在 https://www.mongodb.com/zh-cn/docs/llms.txt 获取文档索引—通过在任何 URL 路径后添加 .md 可获取所有页面的 Markdown 版本。
See how MongoDB 9.0 delivers up to 2x higher throughput.
MongoDB Branding Shape
Register now >
Docs 菜单

跨集合连接实体

在本指南中,您可以学习;了解如何使用MongoDB Extension for Hibernate ORM 跨通过关联链接的实体进行查询。您可以使用 Hibernate Query Language (HQL) 或 Jakarta Persistence Query Language (JPQL)。

要连接实体,您可以在连接子句中导航关联路径,或者在连接子句中命名实体并提供 ON 子句。您还可以向关联联接添加 ON 子句。

Hibernate ORM 扩展将每个联接转换为MongoDB $lookup 阶段和 $unwind 阶段。

注意

ON 条件中的引用列

ON 条件必须比较列。 Hibernate ORM 扩展不支持比较实体引用(如 on m = c.movie 中),也不支持在条件内导航关联(如 on c.movie.title = 'Blue Jasmine' 中)。您可以比较两个完整的组合标识符,如本指南的“组合键连接”部分所述。要学习;了解Hibernate ORM 扩展支持哪些连接类型,请参阅“功能兼容性”页面上的“查询支持”。

The examples in this guide use the Movie entity, which represents the sample_mflix.movies collection from the Atlas sample datasets. The Movie entity has the following definition:

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

学习如何创建使用 MongoDB Extension for Hibernate ORM 与此 MongoDB 示例集合交互的 Java 应用程序,请参阅 入门 教程。

本页上的示例将 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;
}
}

复合 ON 条件部分中的示例还使用以下 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 条件可以将多个字段与 AND 或 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]);
}

由于条件比较多个字段,因此 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;
@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."));

外键是一个实体用于存储其引用的实体的标识符的字段或字段设立。 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 元素映射的关联的反向一侧不存储外键字段。

要连接目标实体具有组合键的关联,请使用连接子句,就像连接具有单字段键的实体一样。

以下示例通过 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 子句在与其父项相同的查询中加载复合键关联,如本指南的“连接获取”部分所述。

在 ON 条件中,您可以使用 =操作符比较两个完整复合标识符。 Hibernate ORM 扩展将比较分解为针对每个关键组件的相等性检查。

您可以在左外连接中使用整体标识符比较,也可以使用 AND操作符将其与附加条件结合使用,如 ON b.id = r.id AND b.title = r.comment 所示。

以下示例将每本书连接到具有匹配标识符的查看:

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 条件可以使用范围和不等式操作符(例如 < 和 >)来比较字段。

以下示例连接 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数据执行其他操作的更多信息,请参阅执行增删改查操作或指定查询指南。

要了解有关使用 HQL 和 JPQL 运行查询的更多信息,请参阅 Hibernate ORM 文档中的 Hibernate 查询语言指南。