Para agentes de IA: hay un índice de documentación disponible en https://www.mongodb.com/es/docs/llms.txt — versiones en markdown de todas las páginas están disponibles agregando .md a cualquier ruta URL.
Docs Menu

Unir entidades entre colecciones

En esta guía, aprenderá a usar la extensión MongoDB para Hibernate ORM para realizar consultas entre entidades vinculadas mediante una asociación. Puede usar el lenguaje de consulta de Hibernate (HQL) o el lenguaje de consulta de persistencia de Jakarta (JPQL).

Para unir entidades, puede navegar por la ruta de asociación en una cláusula de unión o nombrar la entidad en una cláusula de unión y proporcionar una cláusula ON. También puede agregar una cláusula ON a una unión de asociación.

La extensión Hibernate ORM traduce cada unión a una etapa $lookup de MongoDB y a una etapa $unwind.

Nota

Columnas de referencia en condiciones ON

Una condición ON debe comparar columnas. La extensión Hibernate ORM no admite la comparación de referencias de entidades, como en on m = c.movie, ni la navegación de una asociación dentro de la condición, como en on c.movie.title = 'Blue Jasmine'. Puede comparar dos identificadores compuestos completos, como se describe en la sección Uniones de claves compuestas de esta guía. Para saber qué tipos de uniones admite la extensión Hibernate ORM, consulte Compatibilidad con consultas en la página Compatibilidad de características.

Los ejemplos de esta guía utilizan la entidad Movie, que representa la colección sample_mflix.movies del conjuntos de datos de muestras de Atlas. La entidad Movie tiene la siguiente definición:

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 a crear una aplicación Java que utilice la extensión MongoDB para Hibernate ORM para interactuar con esta colección de muestra de MongoDB, consulta el tutorial Primeros pasos.

Los ejemplos de esta página unen la colección sample_mflix.movies con la colección sample_mflix.comments. Los documentos de la colección comments almacenan el valor _id de la película que describen en un campo movie_id.

La entidad Movie asigna su campo comments a las entidades Comment que la referencian. La fecha de estreno de la película se almacena en el campo released de la entidad Movie.

La siguiente entidad Comment se asigna a la colección comments y hace referencia a su entidad Movie relacionada en su 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;
}
}

Los ejemplos de la sección Compound ON Conditions también utilizan la siguiente entidad User, que se corresponde con la colección 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;
}
}

Una unión interna devuelve solo las entidades que tienen una coincidencia en ambos lados de la asociación.

El siguiente ejemplo utiliza una unión interna para recuperar el título de cada película estrenada en 2015 junto con el nombre de cada persona que comentó sobre ella. Las películas que no tienen comentarios se excluyen de los 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]);
}

La extensión Hibernate ORM traduce la unión anterior a las siguientes etapas:

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

La extensión Hibernate ORM genera el nombre del campo as a partir del alias en su instrucción de consulta y lo utiliza para hacer referencia a los campos unidos en etapas posteriores del proceso.

Una unión externa izquierda devuelve todas las entidades del lado izquierdo de la asociación, incluidas las entidades que no tienen coincidencia en el lado derecho. Las entidades que no tienen coincidencia en el lado derecho devuelven resultados null para el lado derecho de la unión.

El siguiente ejemplo utiliza una unión externa izquierda para recuperar el título de cada película estrenada en 2015 junto con el nombre de cada persona que comentó sobre ella. Las películas que no tienen comentarios se incluyen en los 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]);
}

La extensión Hibernate ORM traduce la unión anterior a las siguientes etapas. La opción preserveNullAndEmptyArrays conserva las películas que no tienen comentarios coincidentes:

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

La extensión Hibernate ORM genera el nombre del campo as a partir del alias en su instrucción de consulta y lo utiliza para hacer referencia a los campos unidos en etapas posteriores del proceso.

Una cláusula JOIN FETCH carga una entidad asociada en la misma consulta que su entidad principal, de modo que puede acceder a la asociación después de que se cierre la sesión.

El siguiente ejemplo utiliza una cláusula JOIN FETCH para recuperar comentarios y cargar la entidad Movie a la que hace referencia cada comentario:

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

La extensión Hibernate ORM traduce una cláusula JOIN FETCH a las mismas etapas de la canalización que la unión equivalente.

Una condición ON puede combinar comparaciones de varios campos con AND o OR. Utilice una condición compuesta para unir entidades relacionadas por más de un campo. Para unir entidades con una clave primaria compuesta, consulte la sección «Uniones de clave compuesta» de esta guía.

El siguiente ejemplo combina las colecciones users y comments en los campos name y email, de modo que un comentario coincide con un usuario solo cuando ambos campos son iguales:

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

Debido a que la condición compara más de un campo, la extensión Hibernate ORM utiliza la forma let y pipeline de la etapa $lookup en lugar de la forma localField y foreignField. La opción let vincula cada campo de la colección externa a una variable, y la opción pipeline compara esas variables con los campos de la colección unida en un 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" }

Puedes unir entidades cuya clave primaria sea una clave compuesta declarada con la anotación @EmbeddedId. Para aprender a definir una clave compuesta, consulta la sección Claves primarias compuestas de la guía Crear entidades.

Los ejemplos de esta sección utilizan las siguientes entidades Book y Review, ambas vinculadas a un componente publisherId y a un componente bookNo. La entidad Review hace referencia a su entidad relacionada Book en su campo book.

Cada entidad declara su clave en un registro @Embeddable separado. Declarar la clave como un registro en lugar de una clase proporciona los métodos equals() y hashCode() que Hibernate ORM requiere para comparar valores de identificadores.

El siguiente registro BookId define la clave de la entidad Book:

package org.example;
import jakarta.persistence.Embeddable;
@Embeddable
public record BookId(long publisherId, long bookNo) {
}

La entidad Book tiene la siguiente definición:

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

El siguiente registro ReviewId define la clave de la entidad Review:

package org.example;
import jakarta.persistence.Embeddable;
@Embeddable
public record ReviewId(long publisherId, long bookNo) {
}

La entidad Review tiene la siguiente definición. El campo book asigna la asociación a la entidad 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;
}
}

El siguiente código inserta los documentos que utilizan los ejemplos de esta sección:

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."));

Una clave foránea es el campo o conjunto de campos que una entidad utiliza para almacenar el identificador de la entidad a la que hace referencia. La extensión Hibernate ORM utiliza valores de clave foránea para hacer coincidir los documentos a ambos lados de una unión.

Cuando una asociación apunta a una entidad que tiene una clave compuesta, la extensión ORM de Hibernate almacena la clave externa en el campo de asociación como un subdocumento. El subdocumento refleja los componentes del subdocumento _id de la entidad de destino.

La entidad Review produce documentos en el siguiente formato, en el que el campo book contiene la clave foránea como un subdocumento:

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

La extensión Hibernate ORM admite este diseño para las asociaciones @ManyToOne y @OneToOne. El reverso de una asociación, que se asigna con el elemento mappedBy, no almacena campos de clave externa.

Para unir una asociación cuya entidad de destino tiene una clave compuesta, utilice una cláusula JOIN, como lo haría para una entidad que tiene una clave de un solo campo.

El siguiente ejemplo recupera el identificador de cada reseña junto con el identificador del libro que reseña mediante una unión interna en el campo book de la entidad 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]);
}

El libro Winter Light no aparece en los resultados porque no tiene reseñas.

Debido a que una clave compuesta abarca más de un campo, la extensión ORM de Hibernate utiliza la forma let y pipeline de la etapa $lookup. La opción let vincula cada componente de la clave externa a una variable. La opción pipeline compara esas variables con los componentes del subdocumento _id de destino en un 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" }

También puede utilizar una cláusula JOIN FETCH para cargar una asociación de clave compuesta en la misma consulta que su elemento principal, como se describe en la sección Join Fetch de esta guía.

En una condición ON, puede comparar dos identificadores compuestos completos con el operador =. La extensión ORM de Hibernate descompone la comparación en una comprobación de igualdad para cada componente de la clave.

Puedes usar una comparación de identificador completo en una unión externa izquierda, y puedes combinarla con condiciones adicionales usando el operador AND, como en ON b.id = r.id AND b.title = r.comment.

El siguiente ejemplo vincula cada libro con la reseña que tiene un identificador coincidente:

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 No aparece en los resultados porque ninguna reseña comparte su identificador.

La extensión Hibernate ORM traduce la unión anterior a las siguientes etapas:

{
"$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" }

Las uniones de clave compuesta presentan las siguientes limitaciones:

  • No se puede aplicar la anotación @JoinColumn ni @JoinColumns a una asociación cuya entidad de destino tenga una clave compuesta. La extensión Hibernate ORM deriva los nombres de los campos de clave externa del nombre de la asociación y genera una excepción FeatureNotSupportedException al iniciar la aplicación si se sobrescriben.

  • No se pueden comparar identificadores compuestos completos con un operador de ordenación como > o < en una condición ON. En su lugar, compare los componentes clave individuales.

  • La extensión Hibernate ORM no admite una asociación @ManyToMany cuando cualquiera de las entidades tiene una clave compuesta.

Una condición ON puede comparar campos con operadores de rango y desigualdad, como < y >.

El siguiente ejemplo combina las colecciones movies y comments para recuperar comentarios sobre películas estrenadas en 2015 que se publicaron después de la fecha de estreno de la película:

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

La extensión Hibernate ORM combina la coincidencia de clave de la asociación y la condición ON en un único operador $expr, y utiliza la forma let y pipeline de la etapa $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" }

Para obtener más información sobre cómo realizar otras operaciones con sus datos de MongoDB,consulte la guía "Realizar operaciones CRUD" o "Especificar una consulta".

Para aprender más sobre el uso de HQL y JPQL para ejecutar queries, consulte Una guía sobre el languaje del query de Hibernate en la documentación de Hibernate ORM.