Overview
在本指南中,您可以学习;了解如何使用MongoDB Extension for Hibernate ORM 跨通过关联链接的实体进行查询。您可以使用 Hibernate Query Language (HQL) 或 Jakarta Persistence Query Language (JPQL)。
要连接实体,您可以在连接子句中导航关联路径,或者在连接子句中命名实体并提供 ON 子句。您还可以向关联联接添加 ON 子句。
样本数据
电影实体
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; public class Movie { private ObjectId id; private String title; private String plot; private int year; private List<String> cast; private List<String> directors; private Instant released; private Awards awards; 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; public class Comment { private ObjectId id; private String name; private String email; private String text; private Instant date; 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; public class User { 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 进行比较。使用复合条件连接由多个字段关联的实体。要连接具有复合主键的实体,请参阅本指南的复合键连接部分。
以下示例同时在 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; public record BookId(long publisherId, long bookNo) { }
Book 实体具有以下定义:
package org.example; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; import jakarta.persistence.Table; public class Book { 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; 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; public class Review { private ReviewId id; private String comment; 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" }
比较整个复合标识符
在 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 条件
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 查询语言指南。