Hibernate/JPA manyTomany with extra column

Publié le 25/05/2012, par Jean-Baptiste Cazaux dans Java, Tutoriel, Valtech | 2 Commentaires

Récemment j’ai refait un peu de JPA/Hibernate et j’ai eu besoin de mapper une relation many-to-many qui nécessite de stocker une information supplémentaire. Pour être concret, j’ai un objet Recette, un objet Ingredient et je souhaite associer une recette à un ingrédient. Ce qui est particulier dans mon cas c’est que j’ai envie de d’enregistrer en plus la quantité de cet ingrédient qui est utilisé dans ma recette de cuisine.

Rien de bien extraordinaire jusque là et je me souvenais avoir déjà géré ce genre de cas. Oui mais voilà, après quelques recherches sur les forums je n’ai trouvé aucune solution clé en main qui ne nécessite pas de parcourir les dizaines de commentaires et de les tester un à un.

Voici la solution que j’ai retenue, je suis bien sur ouvert à toute proposition permettant d’améliorer mon code ! Je ne présenterai ici que les mappings, je vous propose de retrouver l’intégralité des cas d’utilisations sur mon github (avec les Tests Unitaires et surtout le debug hibernate pour voir les requêtes qui passent).

L’idée générale est de découper le many to many en deux relations many-to-one/one-to-many avec l’utilsation d’un objet d’association qui va porter l’information que l’on souhaite rajouter (ici la quantité).

NB: je me sers de lombok pour générer les getter, setter et equals/hashcode de mes classes.

La classe Recipe:

package fr.valtech.many2many.domain;
 
import java.util.HashSet;
import java.util.Set;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
 
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
 
@Entity
@Table(name = "recipe")
@EqualsAndHashCode(of = { "id", "title" }, callSuper = false)
public class Recipe extends AbstractEntity {
 
  @Id
  @Column(name = "recipe_id")
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Getter
  @Setter
  private Integer id;
 
  @Getter
  @Setter
  private String title;
 
  @Getter
  @Setter
  @OneToMany(fetch = FetchType.EAGER, mappedBy = "pk.recipe", cascade = CascadeType.ALL, orphanRemoval = true)
  private Set ingredients = new HashSet();
 
  public void addIngredient(RecipeIngredient i) {
    i.setRecipe(this);
    ingredients.add(i);
  }
}

La classe Ingredient:

package fr.valtech.many2many.domain;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
 
@Entity
@EqualsAndHashCode(of = { "id", "label" }, callSuper = false)
public class Ingredient extends AbstractEntity {
 
    @Id
    @Column(name = "ingredient_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Getter
    @Setter
    private Integer id;
 
    @Getter
    @Setter
    private String label;
 
}

La classe RecipeIngredient qui fait l’association:

package fr.valtech.many2many.domain;
 
import java.io.Serializable;
 
import javax.persistence.AssociationOverride;
import javax.persistence.AssociationOverrides;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
 
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
 
@Entity
@Table(name = "recipe_ingredient")
@AssociationOverrides({
        @AssociationOverride(name = "pk.recipe", joinColumns = @JoinColumn(name = "recipe_id", insertable = false, updatable = false)),
        @AssociationOverride(name = "pk.ingredient", joinColumns = @JoinColumn(name = "ingredient_id", insertable = false, updatable = false)) })
@EqualsAndHashCode(of = { "pk", "amount" }, callSuper = false)
public class RecipeIngredient implements Serializable {
 
    @Getter
    @Setter
    @Column(nullable = false)
    private String amount;
 
    @Getter
    @Setter
    @EmbeddedId
    private RecipeIngredientId pk = new RecipeIngredientId();
 
    @Transient
    public Recipe getRecipe() {
        return getPk().getRecipe();
    }
 
    public void setRecipe(Recipe recipe) {
        getPk().setRecipe(recipe);
    }
 
    @Transient
    public Ingredient getIngredient() {
        return getPk().getIngredient();
    }
 
    public void setIngredient(Ingredient ingredient) {
        getPk().setIngredient(ingredient);
    }
 
    public RecipeIngredient() {
    }
 
    public RecipeIngredient(Ingredient ingredient, String amount) {
        setIngredient(ingredient);
        this.amount = amount;
    }
 
}

Et enfin la composite primary key de la classe d’association:

package fr.valtech.many2many.domain;
 
import java.io.Serializable;
 
import javax.persistence.CascadeType;
import javax.persistence.Embeddable;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
 
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
 
@Embeddable
@EqualsAndHashCode(of = { "recipe", "ingredient" }, callSuper = false)
public class RecipeIngredientId implements Serializable {
 
    @Getter
    @Setter
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Recipe recipe;
 
    @Getter
    @Setter
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Ingredient ingredient;
 
}

Les “astuces” que j’ai mis un moment à comprendre sont:

  • dans la classe RecipeIngredientId il faut absolument mettre les relations en LAZY pour éviter un stackOverFlow;
  • dans la classe Recipe, la relation OneToMany doit absolument comporter le orphanRemoval = true, sinon la mise à jour et la suppression des RecipeIngredient se passent mal !

Bon tout n’est pas parfait et il faut gérer quelques trucs “à la main”.
Il faut nécessairement persister en base explicitement l’ingrédient que l’on veut utiliser dans une recette que l’on souhaite elle-même persister.

    /**
     * Parcourt l'ensemble des ingrédient pour voir si ils existe déjà en base ou non.
     * @param recipe
     */
    private void reatachIngredients(Recipe recipe) {
 
        for (Iterator it = recipe.getRecipeIngredients()
                .iterator(); it.hasNext();) {
            RecipeIngredient ri = it.next();
            Ingredient ingredient = ri.getIngredient();
            if (ingredient.getId() != null && ingredient.getId() != 0) {
                Ingredient reference = getEntityManager().getReference(
                        Ingredient.class, ingredient.getId());
                ri.setIngredient(reference);
            } else {
                getEntityManager().persist(ingredient);
            }
        }
    }

Il faut écrire une requête JPQL pour récupérer la Recette dans son intégralité, si on veut par exemple la serialiser après (certaines relations avaient été mises en LAZY, notamment sur la PK).

        Query q = getEntityManager().createQuery(
                "select r from Recipe as r "
                        + "left join fetch r.recipeIngredients as ri "
                        + "left join fetch ri.pk as pk "
                        + "left join fetch pk.ingredient "
                        + "where r.id = :recipeId");
 
        q.setParameter("recipeId", recipeId);
 
        Recipe recipe = null;
        try {
            recipe = (Recipe) q.getSingleResult();
        } catch (NoResultException nre) {
            getLogger().info("no result found");
            return null;
        }

Le code complet ici: https://github.com/jbcazaux/many2many


JPA pour uniformiser la persistance Java ?

Publié le 12/10/2007, par Romain Linsolas dans Architecture | Ajouter un commentaire

Avec l’arrivée des EJB3, Sun a mis en place une nouvelle API, « Java Persistence API » également connue sous le sigle JPA. JPA est une spécification qui définit un framework de persistance, introduite dans la JSR-220 (nom officiel de la spécification EJB3.0).Les frameworks de persistance, également connus sous le nom d’ORM (Object Relationnal Mapping), ont pour objectif de fournir un mapping objet/relationnel entre les SGBDR et les applications Java. Les plus répandus sur le marché sont Hibernate ou encore TopLink. JPA a tenu compte de l’ensemble de ces frameworks pour standardiser l’univers de la persistance en Java.

Lire la suite »