¿Cuál es "el lado inverso de la asociación" en una asociación bidireccional JPA OneToMany / ManyToOne?

167

En la sección de ejemplo de la @OneToManyreferencia de anotación JPA :

Ejemplo 1-59 @OneToMany - Clase de cliente con genéricos

@Entity
public class Customer implements Serializable {
    ...
    @OneToMany(cascade=ALL, mappedBy="customer")
    public Set<Order> getOrders() { 
        return orders; 
    }
    ...
}

Ejemplo 1-60 @ManyToOne - Clase de pedido con genéricos

@Entity
public class Order implements Serializable {
    ...
    @ManyToOne
    @JoinColumn(name="CUST_ID", nullable=false)
    public Customer getCustomer() { 
        return customer; 
    }
    ...
}

Me parece que la Customerentidad es la propietaria de la asociación. Sin embargo, en la explicación del mappedByatributo en el mismo documento, está escrito que:

si la relación es bidireccional, establezca el elemento mappedBy en el lado inverso (no propietario) de la asociación con el nombre del campo o propiedad que posee la relación como muestra el Ejemplo 1-60.

Sin embargo, si no me equivoco, parece que en el ejemplo, en mappedByrealidad se especifica en el lado propietario de la asociación, en lugar del lado no propietario.

Entonces mi pregunta es básicamente:

  1. En una asociación bidireccional (uno a muchos / muchos a uno), ¿cuál de las entidades es el propietario? ¿Cómo podemos designar a One side como el propietario? ¿Cómo podemos designar al lado Muchos como el propietario?

  2. ¿Qué se entiende por "el lado inverso de la asociación"? ¿Cómo podemos designar el lado uno como el inverso? ¿Cómo podemos designar el lado Muchos como el inverso?

Behrang Saeedzadeh
fuente
1
el enlace que proporcionó está desactualizado. Por favor actualice.
MartinL

Respuestas:

306

Para entender esto, debes dar un paso atrás. En OO, el cliente posee los pedidos (los pedidos son una lista en el objeto del cliente). No puede haber un pedido sin un cliente. Entonces el cliente parece ser el dueño de los pedidos.

Pero en el mundo SQL, un elemento contendrá un puntero al otro. Como hay 1 cliente para N pedidos, cada pedido contiene una clave externa para el cliente al que pertenece. Esta es la "conexión" y esto significa que el orden "posee" (o literalmente contiene) la conexión (información). Esto es exactamente lo contrario del mundo OO / modelo.

Esto puede ayudar a entender:

public class Customer {
     // This field doesn't exist in the database
     // It is simulated with a SQL query
     // "OO speak": Customer owns the orders
     private List<Order> orders;
}

public class Order {
     // This field actually exists in the DB
     // In a purely OO model, we could omit it
     // "DB speak": Order contains a foreign key to customer
     private Customer customer;
}

El lado inverso es el "propietario" OO del objeto, en este caso el cliente. El cliente no tiene columnas en la tabla para almacenar los pedidos, por lo que debe indicarle en qué lugar de la tabla de pedidos puede guardar estos datos (lo que ocurre a través de mappedBy).

Otro ejemplo común son los árboles con nodos que pueden ser padres e hijos. En este caso, los dos campos se usan en una clase:

public class Node {
    // Again, this is managed by Hibernate.
    // There is no matching column in the database.
    @OneToMany(cascade = CascadeType.ALL) // mappedBy is only necessary when there are two fields with the type "Node"
    private List<Node> children;

    // This field exists in the database.
    // For the OO model, it's not really necessary and in fact
    // some XML implementations omit it to save memory.
    // Of course, that limits your options to navigate the tree.
    @ManyToOne
    private Node parent;
}

Esto explica para los trabajos de diseño muchos a uno de "clave externa". Hay un segundo enfoque que utiliza otra tabla para mantener las relaciones. Eso significa que, para nuestro primer ejemplo, tiene tres tablas: la que tiene clientes, la que tiene pedidos y una tabla de dos columnas con pares de claves principales (customerPK, orderPK).

Este enfoque es más flexible que el anterior (puede manejar fácilmente uno a uno, muchos a uno, uno a muchos e incluso muchos a muchos). El precio es que

  • es un poco más lento (tener que mantener otra tabla y unirse usa tres tablas en lugar de solo dos),
  • la sintaxis de unión es más compleja (lo que puede ser tedioso si tiene que escribir manualmente muchas consultas, por ejemplo, cuando intenta depurar algo)
  • es más propenso a errores porque de repente puede obtener demasiados o muy pocos resultados cuando algo sale mal en el código que administra la tabla de conexión.

Es por eso que rara vez recomiendo este enfoque.

Aaron Digulla
fuente
36
Solo para aclarar: el lado múltiple es el propietario; el un lado es el inverso. No tienes otra opción (prácticamente hablando).
John
11
No, Hibernate inventó esto. No me gusta ya que expone parte de la implementación al modelo OO. Yo preferiría una @Parento @Childanotación en lugar de "Xtoy" a estado de conexión lo que los medios (en lugar de la forma en que se implementa )
Aaron Digulla
44
@AaronDigulla cada vez que tengo que pasar por un mapeo de OneToMany vine a leer esta respuesta, probablemente la mejor en el tema de SO.
Eugene
77
Guau. Si la documentación de los marcos ORM tuviera una explicación tan buena, ¡sería más fácil de asimilar! Excelente respuesta!
NickJ
2
@klausch: la documentación de Hibernate es confusa. Ignoralo. Mire el código, el SQL en la base de datos y cómo funcionan las claves externas. Si lo desea, puede llevarse un poco de sabiduría a casa: la documentación es una mentira. Usa la fuente, Luke.
Aaron Digulla
41

Increíblemente, en 3 años nadie ha respondido a su excelente pregunta con ejemplos de ambas formas de mapear la relación.

Como han mencionado otros, el lado "propietario" contiene el puntero (clave externa) en la base de datos. Puede designar a cualquier lado como propietario, sin embargo, si designa al lado Uno como propietario, la relación no será bidireccional (el lado inverso "muchos" no tendrá conocimiento de su "propietario"). Esto puede ser deseable para encapsulación / acoplamiento suelto:

// "One" Customer owns the associated orders by storing them in a customer_orders join table
public class Customer {
    @OneToMany(cascade = CascadeType.ALL)
    private List<Order> orders;
}

// if the Customer owns the orders using the customer_orders table,
// Order has no knowledge of its Customer
public class Order {
    // @ManyToOne annotation has no "mappedBy" attribute to link bidirectionally
}

La única solución de mapeo bidireccional es que el lado "muchos" posea su puntero al "uno" y use el atributo "mappedBy" @OneToMany. Sin el atributo "mappedBy", Hibernate esperará una doble asignación (la base de datos tendría tanto la columna de unión como la tabla de unión, que es redundante (generalmente no deseable)).

// "One" Customer as the inverse side of the relationship
public class Customer {
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
    private List<Order> orders;
}

// "many" orders each own their pointer to a Customer
public class Order {
    @ManyToOne
    private Customer customer;
}
Steve Jones
fuente
2
En su ejemplo unidireccional, JPA espera que exista una tabla extra customer_orders. Con JPA2, puede usar la anotación @JoinColumn (que parece usar a menudo) en el campo de pedidos del Cliente para denotar la columna de clave externa de la base de datos en la tabla de pedidos que se debe usar. De esta manera, tiene una relación unidireccional en Java mientras sigue teniendo una columna de clave foránea en la tabla Orden. Entonces, en el mundo de Objetos, el Pedido no sabe acerca del Cliente, mientras que en el mundo de la base de datos, el Cliente no sabe sobre el Pedido.
Henno Vermeulen
1
Para ser súper completo, puede mostrar el caso bidireccional donde el cliente es el lado propietario de la relación.
HDave
35

La entidad que tiene la tabla con clave foránea en la base de datos es la entidad propietaria y la otra tabla, señalada, es la entidad inversa.

Venu
fuente
30
aún más simple: el propietario es la tabla con la columna FK
jacktrades
2
Simple y buena explicación. Cualquier lado puede hacerse propietario. Si usamos mappedBy en Order.java, en el campo Cliente <Eliminar mappedby de Customer.java>, se creará una nueva tabla similar a Order_Customer que tendrá 2 columnas. ORDER_ID y CUSTOMER_ID.
HakunaMatata
14

Reglas simples de relaciones bidireccionales:

1.Para las relaciones bidireccionales de muchos a uno, el lado múltiple es siempre el lado propietario de la relación. Ejemplo: 1 habitación tiene muchas personas (una persona pertenece a una sola habitación) -> el lado propietario es la persona

2.Para las relaciones bidireccionales uno a uno, el lado propietario corresponde al lado que contiene la clave foránea correspondiente.

3.Para las relaciones bidireccionales de muchos a muchos, cualquier lado puede ser el lado propietario.

La esperanza puede ayudarte.

bloque Ken
fuente
¿Por qué necesitamos tener un propietario y un inverso? Ya tenemos los conceptos significativos de un lado y muchos lados y no importa quién sea el propietario en situaciones de muchos a muchos. ¿Cuáles son las consecuencias de la decisión? Es difícil creer que alguien tan ingenuo como un ingeniero de bases de datos haya decidido acuñar estos conceptos redundantes.
Dan Cancro
3

Para dos clases de entidad Cliente y Pedido, hibernate creará dos tablas.

Posibles casos:

  1. mappedBy no se usa en Customer.java y Order.java Class entonces->

    En el lado del cliente, se creará una nueva tabla [name = CUSTOMER_ORDER] que mantendrá la asignación de CUSTOMER_ID y ORDER_ID. Estas son las claves principales de las tablas de clientes y pedidos. En el lado de la orden, se requiere una columna adicional para guardar la asignación de registro Customer_ID correspondiente.

  2. mappedBy se usa en Customer.java [Como se indica en la declaración del problema] Ahora no se crea la tabla adicional [CUSTOMER_ORDER]. Solo una columna en la tabla de pedidos

  3. mappedby se usa en Order.java. Ahora hibernate creará una tabla adicional. [name = CUSTOMER_ORDER] La tabla de pedidos no tendrá una columna adicional [Customer_ID] para la asignación.

Cualquier lado puede convertirse en propietario de la relación. Pero es mejor elegir el lado xxxToOne.

Efecto de codificación -> Solo el lado propietario de la entidad puede cambiar el estado de la relación. En el ejemplo a continuación, la clase BoyFriend es propietaria de la relación. incluso si Girlfriend quiere separarse, no puede.

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "BoyFriend21")
public class BoyFriend21 {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Boy_ID")
    @SequenceGenerator(name = "Boy_ID", sequenceName = "Boy_ID_SEQUENCER", initialValue = 10,allocationSize = 1)
    private Integer id;

    @Column(name = "BOY_NAME")
    private String name;

    @OneToOne(cascade = { CascadeType.ALL })
    private GirlFriend21 girlFriend;

    public BoyFriend21(String name) {
        this.name = name;
    }

    public BoyFriend21() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BoyFriend21(String name, GirlFriend21 girlFriend) {
        this.name = name;
        this.girlFriend = girlFriend;
    }

    public GirlFriend21 getGirlFriend() {
        return girlFriend;
    }

    public void setGirlFriend(GirlFriend21 girlFriend) {
        this.girlFriend = girlFriend;
    }
}

import org.hibernate.annotations.*;
import javax.persistence.*;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.ArrayList;
import java.util.List;

@Entity 
@Table(name = "GirlFriend21")
public class GirlFriend21 {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Girl_ID")
    @SequenceGenerator(name = "Girl_ID", sequenceName = "Girl_ID_SEQUENCER", initialValue = 10,allocationSize = 1)
    private Integer id;

    @Column(name = "GIRL_NAME")
    private String name;

    @OneToOne(cascade = {CascadeType.ALL},mappedBy = "girlFriend")
    private BoyFriend21 boyFriends = new BoyFriend21();

    public GirlFriend21() {
    }

    public GirlFriend21(String name) {
        this.name = name;
    }


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public GirlFriend21(String name, BoyFriend21 boyFriends) {
        this.name = name;
        this.boyFriends = boyFriends;
    }

    public BoyFriend21 getBoyFriends() {
        return boyFriends;
    }

    public void setBoyFriends(BoyFriend21 boyFriends) {
        this.boyFriends = boyFriends;
    }
}


import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.Arrays;

public class Main578_DS {

    public static void main(String[] args) {
        final Configuration configuration = new Configuration();
         try {
             configuration.configure("hibernate.cfg.xml");
         } catch (HibernateException e) {
             throw new RuntimeException(e);
         }
        final SessionFactory sessionFactory = configuration.buildSessionFactory();
        final Session session = sessionFactory.openSession();
        session.beginTransaction();

        final BoyFriend21 clinton = new BoyFriend21("Bill Clinton");
        final GirlFriend21 monica = new GirlFriend21("monica lewinsky");

        clinton.setGirlFriend(monica);
        session.save(clinton);

        session.getTransaction().commit();
        session.close();
    }
}

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.List;

public class Main578_Modify {

    public static void main(String[] args) {
        final Configuration configuration = new Configuration();
        try {
            configuration.configure("hibernate.cfg.xml");
        } catch (HibernateException e) {
            throw new RuntimeException(e);
        }
        final SessionFactory sessionFactory = configuration.buildSessionFactory();
        final Session session1 = sessionFactory.openSession();
        session1.beginTransaction();

        GirlFriend21 monica = (GirlFriend21)session1.load(GirlFriend21.class,10);  // Monica lewinsky record has id  10.
        BoyFriend21 boyfriend = monica.getBoyFriends();
        System.out.println(boyfriend.getName()); // It will print  Clinton Name
        monica.setBoyFriends(null); // It will not impact relationship

        session1.getTransaction().commit();
        session1.close();

        final Session session2 = sessionFactory.openSession();
        session2.beginTransaction();

        BoyFriend21 clinton = (BoyFriend21)session2.load(BoyFriend21.class,10);  // Bill clinton record

        GirlFriend21 girlfriend = clinton.getGirlFriend();
        System.out.println(girlfriend.getName()); // It will print Monica name.
        //But if Clinton[Who owns the relationship as per "mappedby" rule can break this]
        clinton.setGirlFriend(null);
        // Now if Monica tries to check BoyFriend Details, she will find Clinton is no more her boyFriend
        session2.getTransaction().commit();
        session2.close();

        final Session session3 = sessionFactory.openSession();
        session1.beginTransaction();

        monica = (GirlFriend21)session3.load(GirlFriend21.class,10);  // Monica lewinsky record has id  10.
        boyfriend = monica.getBoyFriends();

        System.out.println(boyfriend.getName()); // Does not print Clinton Name

        session3.getTransaction().commit();
        session3.close();
    }
}
HakunaMatata
fuente
1

Relaciones de tabla versus relaciones de entidad

En un sistema de base de datos relacional, solo puede haber tres tipos de relaciones de tabla:

  • uno a muchos (a través de una columna de clave externa)
  • uno a uno (a través de una clave primaria compartida)
  • muchos a muchos (a través de una tabla de enlaces con dos claves externas que hacen referencia a dos tablas principales separadas)

Entonces, una one-to-manyrelación de tabla tiene el siguiente aspecto:

Relación de tabla <code> one-to-many </code>

Tenga en cuenta que la relación se basa en la columna Clave externa (por ejemplo, post_id) en la tabla secundaria.

Entonces, hay una sola fuente de verdad cuando se trata de administrar una one-to-manyrelación de tabla.

Ahora, si toma una relación de entidad bidireccional que se asigna en la one-to-manyrelación de tabla que vimos anteriormente:

Asociación bidireccional de entidades <code> One-To-Many </code>

Si observa el diagrama anterior, puede ver que hay dos formas de gestionar esta relación.

En la Postentidad, tiene la commentscolección:

@OneToMany(
    mappedBy = "post",
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();

Y, en el PostComment, la postasociación se asigna de la siguiente manera:

@ManyToOne(
    fetch = FetchType.LAZY
)
@JoinColumn(name = "post_id")
private Post post;

Entonces, tiene dos lados que pueden cambiar la asociación de la entidad:

  • Al agregar una entrada en la commentscolección secundaria, se post_commentdebe asociar una nueva fila con la postentidad principal a través de supost_id columna.
  • Al establecer la postpropiedad de la PostCommententidad, la post_idcolumna también debe actualizarse.

Debido a que hay dos formas de representar la columna Clave externa, debe definir cuál es la fuente de la verdad cuando se trata de traducir el cambio de estado de asociación en su modificación de valor de columna de clave externa equivalente.

MappedBy (también conocido como el lado inverso)

El mappedByatributo indica que el @ManyToOnelado está a cargo de administrar la columna Clave externa, y que la colección se usa solo para recuperar las entidades secundarias y en cascada los cambios de estado de la entidad primaria a los elementos secundarios (por ejemplo, eliminar el elemento primario también debería eliminar las entidades secundarias).

Se llama el lado inverso porque hace referencia a la propiedad de entidad secundaria que administra esta relación de tabla.

Sincronice ambos lados de una asociación bidireccional

Ahora, incluso si definió el mappedByatributo y la @ManyToOneasociación del lado secundario maneja la columna Clave externa, aún necesita sincronizar ambos lados de la asociación bidireccional.

La mejor manera de hacerlo es agregar estos dos métodos de utilidad:

public void addComment(PostComment comment) {
    comments.add(comment);
    comment.setPost(this);
}

public void removeComment(PostComment comment) {
    comments.remove(comment);
    comment.setPost(null);
}

Los métodos addCommenty removeCommentaseguran que ambos lados estén sincronizados. Entonces, si agregamos una entidad secundaria, la entidad secundaria debe apuntar al elemento primario y la entidad principal debe tener el elemento secundario contenido en la colección secundaria.

Para obtener más detalles sobre la mejor manera de sincronizar todos los tipos de asociación de entidades bidireccionales, consulte este artículo .

Vlad Mihalcea
fuente