140 votes

Mappage d'une table d'association many-to-many avec une ou plusieurs colonnes supplémentaires

Ma base de données contient 3 tables : Les entités User et Service ont une relation many-to-many et sont jointes à la table SERVICE_USER comme suit :

UTILISATEURS - SERVICE_USER - SERVICES

La table SERVICE_USER contient une colonne supplémentaire BLOCKED.

Quelle est la meilleure façon de réaliser une telle cartographie ? Voici mes classes d'entités

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

J'ai suivi cet exemple http://giannigar.wordpress.com/2009/09/04/m ... utilisant-jpa/ Voici un peu de code de test :

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

Le problème est qu'Hibernate persiste l'objet User et l'objet UserService. Pas de succès avec l'objet CmsService

J'ai essayé d'utiliser EAGER fetch - aucun progrès

Est-il possible d'obtenir le comportement que j'attends avec la cartographie fournie ci-dessus ?

Peut-être existe-t-il une manière plus élégante de mettre en correspondance une table de jointure many to many avec une colonne supplémentaire ?

203voto

JB Nizet Points 250258

Comme la table SERVICE_USER n'est pas une table de jointure pure, mais qu'elle comporte des champs fonctionnels supplémentaires (bloqués), vous devez la mapper en tant qu'entité et décomposer l'association many to many entre User et Service en deux associations OneToMany : Un utilisateur a plusieurs services d'utilisateur, et un service a plusieurs services d'utilisateur.

Vous ne nous avez pas montré la partie la plus importante : le mappage et l'initialisation des relations entre vos entités (c'est-à-dire la partie qui vous pose problème). Je vais donc vous montrer à quoi cela devrait ressembler.

Si vous rendez les relations bidirectionnelles, vous devriez donc avoir

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 vous ne mettez pas de cascade sur vos relations, alors vous devez persister/sauvegarder toutes les entités. Bien que seul le côté propriétaire de la relation (ici, le côté UserService) doive être initialisé, c'est aussi une bonne pratique de s'assurer que les deux côtés sont en cohérence.

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

2 votes

Juste pour ajouter.. Bien que ce soit à mon avis la meilleure façon (je préfère toujours mapper la chose qui possède le FK comme une entité pour des raisons de performance), ce n'est pas en fait la seule façon. Vous pouvez également mapper les valeurs de la table SERVICE_USER en tant que composant (ce que JPA appelle un embeddable) et utiliser une méthode de type @ElementCollection de l'une ou l'autre (ou des deux) entités Utilisateur et Service.

6 votes

Qu'en est-il de la clé primaire de la table UserService ? Elle devrait être la combinaison des clés étrangères user et service. Est-ce que c'est cartographié ?

24 votes

Je ne le ferais pas. Les clés composites sont pénibles, inefficaces et Hibernate recommande de ne pas utiliser de clés composites. Utilisez simplement un ID généré automatiquement comme pour toute autre entité, et la vie sera beaucoup plus simple. Pour assurer l'unicité de [userFK, serviceFK] , utilisez une contrainte unique.

10voto

biera Points 432

Il existe un bon tutoriel (avec des exemples) sur le mapping Many-To-Many. Les deux cas sont inclus - tableau d'association sans/avec colonne(s) supplémentaire(s).

http://en.wikibooks.org/wiki/Java_Persistence/ManyToMany

6voto

Zhar Points 412

Je cherche un moyen de mapper une table d'association many-to-many avec une ou plusieurs colonnes supplémentaires avec la configuration d'hibernate dans des fichiers xml.

En supposant que j'ai deux tables 'a' et 'c' avec une association many to many avec une colonne nommée 'extra'. Parce que je n'ai pas trouvé d'exemple complet, voici mon code. J'espère que cela vous aidera :).

Tout d'abord, voici les objets 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;
    }

}

Maintenant, nous devons créer la table d'association. La première étape consiste à créer un objet représentant une clé primaire complexe (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;
    }
}

Maintenant, créons l'objet d'association lui-même.

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

À ce stade, il est temps de mapper toutes nos classes avec la configuration xml d'Hibernate.

A.hbm.xml et C.hxml.xml (quiete la même).

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

Et ensuite le fichier de mapping de l'association, 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>

Voici l'exemple de code à tester.

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

2voto

ingitaly Points 81

Comme indiqué précédemment, avec JPA, pour avoir la possibilité d'avoir des colonnes supplémentaires, vous devez utiliser deux associations OneToMany, au lieu d'une seule relation ManyToMany. Vous pouvez également ajouter une colonne avec des valeurs autogénérées ; de cette façon, elle peut fonctionner comme clé primaire de la table, si nécessaire.

Par exemple, le code d'implémentation de la classe supplémentaire devrait ressembler à ceci :

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

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X