Also many-to-many associations are usually referenced as a bad design solution, they are widely used in almost all modern database-centric applications (especially in those built around the existing legacy database). The very common scenario is a case when you not only need to handle a many-to-many association but also hold some additional property on the association.

In a J2EE world, Hibernate still remains the most popular ORM tool (or the JPA provider, if you wish). But implementing such a scenario with Hibernate Annotations is not so simple as you might imagine.

First of all, in the good old spirit of the Hibernate community, there is almost no documentation about many-to-many associations and composite keys (two paragraphs stating that there is @ManyToMany and @EmbeddedId annotations are not considered a documentation).

And the second hope of every open-source tool consumer, the world wide web community, provides almost no help: everybody are referencing the same post by Marcel Panse, written in April 2006. Though Marsel’s post is very clean and descriptive, it become outdated and the solution provided there is simply not working with the latest versions of Hibernate Annotations.

So I had to reinvent the wheel by my own, based on Marsel’s sample. Below is the solution working with Hibernate Annotations 3.3.1.GA.

The database part is the same: we have three tables (item, product and product_item), two POJO classes, and two classes for a many-to-many association and its primary key. The main difference from Marsel’s solution is that I’m not using any kind of “fake” properties on ProductItem in order to reference Item and Product, but just a plain transient properties delegating to ProductItemPk.

@Entity
@Table(name = "item")
public class Item {
 
    private Integer id;
    private String name;
    private List<ProductItem> productItems = new LinkedList<ProductItem>();
 
    public Item() {
    }
 
    @Id
    @GenericGenerator(name = "generator", strategy = "increment")
    @GeneratedValue(generator = "generator")
    @Column(name = "item_id", nullable = false)
    public Integer getId() {
        return this.id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    @Column(name = "name")
    public String getName() {
        return this.name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.item")
    public List<ProductItem> getProductItems() {
        return this.productItems;
    }
 
    public void setProductItems(List<ProductItem> productItems) {
        this.productItems = productItems;
    }
}
@Entity
@Table(name = "product")
public class Product {
 
    private Integer id;
    private String name;
    private List<ProductItem> productItems = new LinkedList<ProductItem>();
 
    public Product() {
    }
 
    @Id
    @GenericGenerator(name = "generator", strategy = "increment")
    @GeneratedValue(generator = "generator")
    @Column(name = "product_id", nullable = false)
    public Integer getId() {
        return this.id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    @Column(name = "name")
    public String getName() {
        return this.name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.product")
    public List<ProductItem> getProductItems() {
        return this.productItems;
    }
 
    public void setProductItems(List<ProductItem> productItems) {
        this.productItems = productItems;
    }
}

Note the two important points: ProductItem has two transient properties for Product and Item (since an unidirectional relationships will be meaningless here) and a use of @AssociationOverrides annotation to specify the database columns

@Entity
@Table(name = "product_item")
@AssociationOverrides({
@AssociationOverride(name = "pk.item", joinColumns = @JoinColumn(name = "item_id")),
@AssociationOverride(name = "pk.product", joinColumns = @JoinColumn(name = "product_id"))
        })
public class ProductItem {
 
    private ProductItemPk pk = new ProductItemPk();
 
    @EmbeddedId
    private ProductItemPk getPk() {
        return pk;
    }
 
    private void setPk(ProductItemPk pk) {
        this.pk = pk;
    }
 
    @Transient
    public Item getItem() {
        return getPk().getItem();
    }
 
    public void setItem(Item item) {
        getPk().setItem(item);
    }
 
    @Transient
    public Product getProduct() {
        return getPk().getProduct();
    }
 
    public void setProduct(Product product) {
        getPk().setProduct(product);
    }
 
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        ProductItem that = (ProductItem) o;
 
        if (getPk() != null ? !getPk().equals(that.getPk()) : that.getPk() != null) return false;
 
        return true;
    }
 
    public int hashCode() {
        return (getPk() != null ? getPk().hashCode() : 0);
    }
}
@Embeddable
public class ProductItemPk implements Serializable {
 
    private Item item;
    private Product product;
 
    @ManyToOne
    public Item getItem() {
        return item;
    }
 
    public void setItem(Item item) {
        this.item = item;
    }
 
    @ManyToOne
    public Product getProduct() {
        return product;
    }
 
    public void setProduct(Product product) {
        this.product = product;
    }
 
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        ProductItemPk that = (ProductItemPk) o;
 
        if (item != null ? !item.equals(that.item) : that.item != null) return false;
        if (product != null ? !product.equals(that.product) : that.product != null)
            return false;
 
        return true;
    }
 
    public int hashCode() {
        int result;
        result = (item != null ? item.hashCode() : 0);
        result = 31 * result + (product != null ? product.hashCode() : 0);
        return result;
    }
}

Hope this helps.

Be Sociable, Share!