Visão geral
Neste guia, você pode aprender como usar a Extensão do MongoDB para Hibernate ORM para fazer query em entidades que estão vinculadas por uma associação. Você pode usar a Hibernate linguagem de query (HQL) ou a Jakarta Persistence linguagem de query (JPQL).
Para unir entidades, você pode navegar pelo caminho de associação em uma cláusula join ou nomear a entidade em uma cláusula join e fornecer uma cláusula ON. Você também pode adicionar uma cláusula ON a uma associação join.
A extensão Hibernate ORM traduz cada junção para um estágio $lookup do MongoDB e um estágio $unwind.
Observação
Colunas de referência em condições ON
Uma condição ON deve comparar colunas. A extensão Hibernate ORM não oferece suporte à comparação de referências de entidade, como em on m = c.movie, ou à navegação em uma associação dentro da condição, como em on c.movie.title = 'Blue Jasmine'. Você pode comparar dois identificadores compostos inteiros, conforme descrito na seção Composite Key Joins deste guia. Para aprender quais tipos de join a extensão Hibernate ORM suporta, consulte Query Support na página de Compatibilidade de recurso.
Dados de amostra
Entidade de Filme
Os exemplos neste guia usam a entidade Movie, que representa a coleção sample_mflix.movies dos conjuntos de dados de amostra do Atlas. A entidade Movie tem a seguinte definição:
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; }}Para aprender como criar um aplicativo Java que use a Extensão MongoDB para Hibernate ORM para interagir com essa coleção de amostras do MongoDB, veja o tutorial Get Started.
Os exemplo nesta página juntam a coleção sample_mflix.movies à coleção sample_mflix.comments. Os documento na coleção comments armazenar o valor _id do filme que descrevem em um campo movie_id.
A entidade Movie mapeia seu campo comments para as entidades Comment que fazem referência a ela. A data de lançamento do filme é armazenada no campo released da entidade Movie.
Entidade de comentário
A seguinte entidade Comment mapeia para a coleção comments e referencia sua entidade Movie relacionada em seu campo 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; }
}Entidade de Usuário
Os exemplos na seção Condições ON Compostas também usam a seguinte entidade User, que mapeia para a coleção 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; }
}Inner Join
Um inner join retorna apenas entidades que possuem uma correspondência em ambos os lados da associação.
O exemplo a seguir usa uma junção interna para recuperar o título de cada filme lançado em 2015 juntamente com o nome de cada pessoa que comentou sobre ele. Filmes sem comentários são excluídos dos resultados:
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]);}A extensão Hibernate ORM traduz o join anterior para os seguintes estágios:
{ "$lookup": { "from": "comments", "localField": "_id", "foreignField": "movie_id", "as": "#c1_0" }},{ "$unwind": "$#c1_0" }A extensão Hibernate ORM gera o nome do campo as a partir do alias em sua declaração de query e o utiliza para referenciar campos unidos em estágios posteriores do pipeline.
Ligação externa esquerda
Um left outer join retorna todas as entidades no lado esquerdo da associação, incluindo entidades que não têm correspondência no lado direito. Entidades que não têm correspondência no lado direito retornam resultados null para o lado direito do join.
O exemplo a seguir usa um left outer join para recuperar o título de cada filme lançado em 2015 junto com o nome de cada pessoa que comentou sobre ele. Filmes que não têm comentários são incluídos nos resultados:
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]);}A extensão Hibernate ORM traduz o join anterior para os seguintes estágios. A opção preserveNullAndEmptyArrays retém filmes que não têm comentários correspondentes:
{ "$lookup": { "from": "comments", "localField": "_id", "foreignField": "movie_id", "as": "#c1_0" }},{ "$unwind": { "path": "$#c1_0", "preserveNullAndEmptyArrays": true }}A extensão Hibernate ORM gera o nome do campo as a partir do alias em sua declaração de query e o utiliza para referenciar campos unidos em estágios posteriores do pipeline.
Join Fetch
Uma cláusula JOIN FETCH carrega uma entidade associada na mesma query que seu pai, para que você possa acessar a associação após o fechamento da sessão.
O exemplo a seguir usa uma cláusula JOIN FETCH para recuperar comentários e carregar a entidade Movie que cada comentário referencia:
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());}A extensão Hibernate ORM traduz uma cláusula JOIN FETCH para os mesmos estágios de pipeline que o join equivalente.
Condições ON compostas
Uma condição ON pode combinar várias comparações de campo com AND ou OR. Use uma condição composta para unir entidades que estão relacionadas por mais de um campo. Para unir entidades que têm uma chave primária composta, consulte a seção Uniões de chaves compostas deste guia.
O exemplo a seguir faz um join das coleções users e comments tanto no campo name quanto no email, de modo que um comentário corresponda a um usuário apenas quando ambos os campos forem iguais:
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]);}Como a condição compara mais de um campo, a extensão Hibernate ORM usa a forma let e pipeline do estágio $lookup em vez da forma localField e foreignField. A opção let vincula cada campo da coleção externa a uma variável, e a opção pipeline compara essas variáveis aos campos da coleção unida em um operador $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" }Junções de chave composta
Você pode realizar joins de entidades cuja chave primária seja uma chave composta declarada com a anotação @EmbeddedId. Para aprender como definir uma chave composta, consulte a seção Chaves primárias compostas do guia Criar entidades.
Os exemplos nesta seção usam as seguintes entidades Book e Review, que são ambas identificadas por um componente publisherId e um componente bookNo. A entidade Review referencia sua entidade Book relacionada em seu campo book.
Cada entidade declara sua chave em um registro @Embeddable separado. Declarar a chave como um registro em vez de uma classe fornece os métodos equals() e hashCode() que o Hibernate ORM exige para comparar valores de identificador.
O seguinte registro BookId define a chave da entidade Book:
package org.example;
import jakarta.persistence.Embeddable;
@Embeddablepublic record BookId(long publisherId, long bookNo) {}A entidade Book tem a seguinte definição:
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; }
}O seguinte registro ReviewId define a chave da entidade Review:
package org.example;
import jakarta.persistence.Embeddable;
@Embeddablepublic record ReviewId(long publisherId, long bookNo) {}A entidade Review tem a seguinte definição. O campo book mapeia a associação para a entidade 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; }
}O código a seguir insere os documento que os exemplo nesta seção usam:
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."));Armazenamento de chave estrangeira composta
Uma chave estrangeira é o campo ou conjunto de campos que uma entidade usa para armazenar o identificador de uma entidade que ela referencia. A extensão Hibernate ORM usa valores de chave estrangeira para corresponder a documentos em cada lado de uma junção.
Quando uma associação tem como alvo uma entidade que possui uma chave composta, a extensão Hibernate ORM armazena a chave estrangeira no campo de associação como um subdocumento. O subdocumento espelha os componentes do subdocumento _id da entidade de destino.
A entidade Review produz documentos no seguinte formato, no qual o campo book contém a chave estrangeira como um subdocumento:
{ "_id": { "bookNo": 7, "publisherId": 30 }, "comment": "An excellent read.", "book": { "bookNo": 2, "publisherId": 10 }}A extensão Hibernate ORM oferece suporte a esse layout para associações @ManyToOne e @OneToOne. O lado inverso de uma associação, que você mapeia com o elemento mappedBy, não armazena nenhum campo de chave estrangeira.
Unir uma Associação de Chave Composta
Para realizar um join em uma associação cuja entidade de destino possui uma chave composta, use uma cláusula join, como faria para uma entidade que possui uma chave de campo único.
O exemplo a seguir recupera o identificador de cada revisar junto com o identificador do livro que ele revisar por meio de um inner join no campo book da entidade Review:
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]);}O livro Winter Light não aparece nos resultados porque não tem revisar.
Como uma chave composta abrange mais de um campo, a extensão Hibernate ORM usa a forma let e pipeline do estágio $lookup. A opção let vincula cada componente de chave estrangeira a uma variável. A opção pipeline compara essas variáveis com os componentes do subdocumento _id de destino em um operador $expr:
{ "$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" }Você também pode usar uma cláusula JOIN FETCH para carregar uma associação de chave composta na mesma query que seu pai, conforme descrito na seção Join Fetch deste guia.
Comparar identificadores compostos inteiros
Em uma condição ON, você pode comparar dois identificadores compostos inteiros com o operador =. A extensão Hibernate ORM decompõe a comparação em uma verificação de igualdade para cada componente de chave.
Você pode usar uma comparação de identificador inteiro em um left outer join e pode combiná-la com condições adicionais usando o operador AND, como em ON b.id = r.id AND b.title = r.comment.
O exemplo a seguir une cada livro a revisar que tem um identificador correspondente:
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 não aparece nos resultados porque nenhum revisar compartilha seu identificador.
A extensão Hibernate ORM traduz o join anterior para os seguintes estágios:
{ "$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" }Limitações de junção de chave composta
As junções de chaves compostas têm as seguintes limitações:
Você não pode aplicar a anotação
@JoinColumnou@JoinColumnsa uma associação cuja entidade de destino tenha uma chave composta. A extensão Hibernate ORM deriva os nomes dos campos de chave estrangeira do nome da associação e gera umFeatureNotSupportedExceptionquando seu aplicativo é iniciado se você os substituir.Você não pode comparar identificadores compostos inteiros com um operador de ordenação como
>ou<em uma condiçãoON. Compare os componentes principais individuais em vez disso.A extensão Hibernate ORM não oferece suporte a uma associação
@ManyToManyquando qualquer uma das entidades possui uma chave composta.
Condições ON de faixa e desigualdade
Uma condição ON pode comparar campos com operadores de faixa e desigualdade, como < e >.
O exemplo a seguir une as coleções movies e comments para recuperar comentários sobre filmes lançados em 2015 que foram publicados após a data de lançamento do filme:
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]);}A extensão Hibernate ORM combina a correspondência de chave da associação e a condição ON em um único operador $expr, e usa a forma let e pipeline do estágio $lookup:
{ "$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" }Informações adicionais
Para aprender mais sobre como realizar outras operações em seus dados do MongoDB, consulte o guia Operações CRUD ou Especificar uma Query.
Para saber mais sobre como usar HQL e JPQL para executar queries, consulte Um guia para a linguagem de query do Hibernate na documentação do Hibernate ORM.