Asignación de tabla de asociación de muchos a muchos con columnas adicionales

130

Mi base de datos contiene 3 tablas: las entidades de usuario y servicio tienen una relación de muchos a muchos y se unen con la tabla SERVICE_USER de la siguiente manera:

USUARIOS - SERVICIO_USUARIO - SERVICIOS

La tabla SERVICE_USER contiene una columna BLOQUEADA adicional.

¿Cuál es la mejor manera de realizar tal mapeo? Estas son mis clases de entidad

@Entity
@Table(name = "USERS")
public class User implements java.io.Serializable {

private String userid;
private String email;

@Id
@Column(name = "USERID", unique = true, nullable = false,)
public String getUserid() {
return this.userid;
}

.... some get/set methods
}

@Entity
@Table(name = "SERVICES")
public class CmsService implements java.io.Serializable {
private String serviceCode;

@Id
@Column(name = "SERVICE_CODE", unique = true, nullable = false, length = 100)
public String getServiceCode() {
return this.serviceCode;
}
.... some additional fields and get/set methods
}

Seguí este ejemplo http://giannigar.wordpress.com/2009/09/04/m ... using-jpa / Aquí hay un código de prueba:

User user = new User();
user.setEmail("e2");
user.setUserid("ui2");
user.setPassword("p2");

CmsService service= new CmsService("cd2","name2");

List<UserService> userServiceList = new ArrayList<UserService>();

UserService userService = new UserService();
userService.setService(service);
userService.setUser(user);
userService.setBlocked(true);
service.getUserServices().add(userService);

userDAO.save(user);

El problema es que hibernate persiste User object y UserService one. Sin éxito con el objeto CmsService

Traté de usar EAGER fetch - sin progreso

¿Es posible lograr el comportamiento que espero con la asignación proporcionada anteriormente?

¿Quizás haya alguna forma más elegante de mapear tabla de unión de muchos a muchos con una columna adicional?

archie_by
fuente

Respuestas:

192

Dado que la tabla SERVICE_USER no es una tabla de unión pura, pero tiene campos funcionales adicionales (bloqueados), debe asignarla como una entidad y descomponer la asociación de muchos a muchos entre el Usuario y el Servicio en dos asociaciones de OneToMany: un usuario tiene muchos servicios de usuario, y un servicio tiene muchos servicios de usuario.

No nos ha mostrado la parte más importante: el mapeo y la inicialización de las relaciones entre sus entidades (es decir, la parte con la que tiene problemas). Así que te mostraré cómo debería ser.

Si hace que las relaciones sean bidireccionales, debería tener

class User {
    @OneToMany(mappedBy = "user")
    private Set<UserService> userServices = new HashSet<UserService>();
}

class UserService {
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "service_code")
    private Service service;

    @Column(name = "blocked")
    private boolean blocked;
}

class Service {
    @OneToMany(mappedBy = "service")
    private Set<UserService> userServices = new HashSet<UserService>();
}

Si no coloca ninguna cascada en sus relaciones, debe persistir / guardar todas las entidades. Aunque solo se debe inicializar el lado propietario de la relación (aquí, el lado UserService), también es una buena práctica asegurarse de que ambos lados estén en coherencia.

User user = new User();
Service service = new Service();
UserService userService = new UserService();

user.addUserService(userService);
userService.setUser(user);

service.addUserService(userService);
userService.setService(service);

session.save(user);
session.save(service);
session.save(userService);
JB Nizet
fuente
2
Solo para agregar ... Si bien esta es, en mi opinión, la mejor manera (siempre prefiero mapear lo que posee el FK como entidad por razones de rendimiento), de hecho, no es la única forma. También puede asignar los valores de la tabla SERVICE_USER como un componente (lo que JPA llama un elemento incrustable) y usar uno @ElementCollectionde (o ambos) las entidades de Usuario y Servicio.
Steve Ebersole
66
¿Qué pasa con la clave principal de la tabla UserService? Debe ser la combinación de claves externas de usuario y servicio. ¿Eso está mapeado?
Jonas Gröger
24
Yo no lo haría así. Las teclas compuestas son dolorosas, ineficientes, e Hibernate recomienda no usar teclas compuestas. Simplemente use una ID generada automáticamente como para cualquier otra entidad, y la vida será mucho más simple. Para garantizar la unicidad de [userFK, serviceFK], utilice una restricción única.
JB Nizet
1
@GaryKephart: haga su propia pregunta, con su propio código y su propio mapeo.
JB Nizet
1
@gstackoverflow: Hibernate 4 no cambia nada al respecto. Realmente no veo cómo eso es poco elegante.
JB Nizet
5

Busco una forma de asignar una tabla de asociación de muchos a muchos con columnas adicionales con hibernación en la configuración de archivos xml.

Suponiendo que tiene dos tablas 'a' y 'c' con una asociación de muchos a muchos con una columna llamada 'extra'. Porque no encontré ningún ejemplo completo, aquí está mi código. Espero que ayude :).

Primero, aquí están los objetos Java.

public class A implements Serializable{  

    protected int id;
    // put some others fields if needed ...   
    private Set<AC> ac = new HashSet<AC>();

    public A(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

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

    public Set<AC> getAC() {
        return ac;
    }

    public void setAC(Set<AC> ac) {
        this.ac = ac;
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        final int prime = 97;
        int result = 1;
        result = prime * result + id;
        return result;
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof A))
            return false;
        final A other = (A) obj;
        if (id != other.getId())
            return false;
        return true;
    }

}

public class C implements Serializable{

    protected int id;
    // put some others fields if needed ...    

    public C(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

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

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        final int prime = 98;
        int result = 1;
        result = prime * result + id;
        return result;
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof C))
            return false;
        final C other = (C) obj;
        if (id != other.getId())
            return false;
        return true;
    }

}

Ahora, tenemos que crear la tabla de asociación. El primer paso es crear un objeto que represente una clave primaria compleja (a.id, c.id).

public class ACId implements Serializable{

    private A a;
    private C c;

    public ACId() {
        super();
    }

    public A getA() {
        return a;
    }
    public void setA(A a) {
        this.a = a;
    }
    public C getC() {
        return c;
    }
    public void setC(C c) {
        this.c = c;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((a == null) ? 0 : a.hashCode());
        result = prime * result
                + ((c == null) ? 0 : c.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ACId other = (ACId) obj;
        if (a == null) {
            if (other.a != null)
                return false;
        } else if (!a.equals(other.a))
            return false;
        if (c == null) {
            if (other.c != null)
                return false;
        } else if (!c.equals(other.c))
            return false;
        return true;
    }
}

Ahora creemos el objeto de asociación en sí.

public class AC implements java.io.Serializable{

    private ACId id = new ACId();
    private String extra;

    public AC(){

    }

    public ACId getId() {
        return id;
    }

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

    public A getA(){
        return getId().getA();
    }

    public C getC(){
        return getId().getC();
    }

    public void setC(C C){
        getId().setC(C);
    }

    public void setA(A A){
        getId().setA(A);
    }

    public String getExtra() {
        return extra;
    }

    public void setExtra(String extra) {
        this.extra = extra;
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        AC that = (AC) o;

        if (getId() != null ? !getId().equals(that.getId())
                : that.getId() != null)
            return false;

        return true;
    }

    public int hashCode() {
        return (getId() != null ? getId().hashCode() : 0);
    }
}

En este punto, es hora de asignar todas nuestras clases con la configuración hibernate xml.

A.hbm.xml y C.hxml.xml (calla lo mismo).

<class name="A" table="a">
        <id name="id" column="id_a" unsaved-value="0">
            <generator class="identity">
                <param name="sequence">a_id_seq</param>
            </generator>
        </id>
<!-- here you should map all others table columns -->
<!-- <property name="otherprop" column="otherprop" type="string" access="field" /> -->
    <set name="ac" table="a_c" lazy="true" access="field" fetch="select" cascade="all">
        <key>
            <column name="id_a" not-null="true" />
        </key>
        <one-to-many class="AC" />
    </set>
</class>

<class name="C" table="c">
        <id name="id" column="id_c" unsaved-value="0">
            <generator class="identity">
                <param name="sequence">c_id_seq</param>
            </generator>
        </id>
</class>

Y luego el archivo de mapeo de asociación, a_c.hbm.xml.

<class name="AC" table="a_c">
    <composite-id name="id" class="ACId">
        <key-many-to-one name="a" class="A" column="id_a" />
        <key-many-to-one name="c" class="C" column="id_c" />
    </composite-id>
    <property name="extra" type="string" column="extra" />
</class>

Aquí está el ejemplo de código para probar.

A = ADao.get(1);
C = CDao.get(1);

if(A != null && C != null){
    boolean exists = false;
            // just check if it's updated or not
    for(AC a : a.getAC()){
        if(a.getC().equals(c)){
            // update field
            a.setExtra("extra updated");
            exists = true;
            break;
        }
    }

    // add 
    if(!exists){
        ACId idAC = new ACId();
        idAC.setA(a);
        idAC.setC(c);

        AC AC = new AC();
        AC.setId(idAC);
        AC.setExtra("extra added"); 
        a.getAC().add(AC);
    }

    ADao.save(A);
}
Zhar
fuente
2

Como se dijo anteriormente, con JPA, para tener la oportunidad de tener columnas adicionales, debe usar dos asociaciones OneToMany, en lugar de una sola relación ManyToMany. También puede agregar una columna con valores autogenerados; de esta manera, puede funcionar como la clave principal de la tabla, si es útil.

Por ejemplo, el código de implementación de la clase adicional debería verse así:

@Entity
@Table(name = "USER_SERVICES")
public class UserService{

    // example of auto-generated ID
    @Id
    @Column(name = "USER_SERVICES_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long userServiceID;



    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "SERVICE_ID")
    private Service service;



    // example of extra column
    @Column(name="VISIBILITY")    
    private boolean visibility;



    public long getUserServiceID() {
        return userServiceID;
    }


    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public Service getService() {
        return service;
    }

    public void setService(Service service) {
        this.service = service;
    }

    public boolean getVisibility() {
        return visibility;
    }

    public void setVisibility(boolean visibility) {
        this.visibility = visibility;
    }

}
ingitaly
fuente