Many-To-Many Mapping Table

Hibernate creates two tables in a many to many relationship

Take a look to the following link to know how to map a ManyToMany relation in a suitable way. But basically, you can do:

public class Product {
...

@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name="product_customer"
joinColumns=@JoinColumn(name="product_id"),
inverseJoinColumns=@JoinColumn(name="customer_id")
)
private Set<Customer> customers = new LinkedHashSet<>();

...
}

And:

public class Customer extends BaseEntity {

...
@ManyToMany(mappedBy = "customers")
private Set<Product> products = new LinkedHashSet<>();

...
}

As @Kavithakaran mentioned in a comment of his answer, you can use @ManyToMany(mappedBy = ... once you identify the "owner of the relation".

Many to Many inside Many to Many Table

This is actually a good question that is worth some research and experimenting. There are many ways to do the mapping. Coming up with a better design would actually depend on your application needs. But here is how I think would be an effective way to implement the mapping:

I'll have 3 separate entities for Order, Product and Address.

We will not implement the usual many-to-many relationship between the 2 entities, Order and Product, where each side has a collection of the other. Instead, I'll create another entity to represent the relationship between Order and Product, and let's name it ProductOrder. Here's how their relationships are mapped:

  • Order has one-to-many relationship with ProductOrder.
  • ProductOrder has many-to-one relationship with Order.
  • Product has one-to-many relationship with ProductOrder.
  • ProductOrder has many-to-one relationship with Product.

ProductOrder's primary key will be composed of the primary key of Order and primary key of Product - so this will be a composite key. Therefore we will need to use @IdClass to map composite keys.

Now, here's the trick to achieving the many-to-many within a many-to-many relationship:

ProductOrder has many-to-many relationship with Address.

See sample codes for each entity mentioned above:

ORDER ENTITY

@Entity
@Table(name = "ORDERS")
public class Order {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ORDER_ID")
private Long id;

private int quantity;

@OneToMany(mappedBy = "order")
private List<ProductOrder> productOrderList = new ArrayList<ProductOrder>();
...
}

PRODUCT ENTITY

@Entity
@Table(name="PRODUCT")
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "PRODUCT_ID")
private Long id;

private String name;

@OneToMany(mappedBy = "product")
private List<ProductOrder> productOrderList = new ArrayList<ProductOrder>();
...
}

ADDRESS ENTITY

@Entity
@Table(name="ADDRESS")
public class Address {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ADDRESS_ID")
private Long id;

private String state;

@ManyToMany(mappedBy = "addressList")
private List<ProductOrder> productOrderList = new ArrayList<ProductOrder>();
...
}

PRODUCTORDER ENTITY

@Entity
@Table(name="PRODUCT_ORDER")
@IdClass(ProductOrderId.class)
public class ProductOrder {

@Id
@ManyToOne
@JoinColumn(name="ORDER_ID")
private Order order;

@Id
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product;

@ManyToMany
@JoinTable(name="PRODUCT_ORDER_ADDRESS",
joinColumns={@JoinColumn(name="ORDER_ID", referencedColumnName="ORDER_ID"),
@JoinColumn(name="PRODUCT_ID", referencedColumnName="PRODUCT_ID")},
inverseJoinColumns=@JoinColumn(name="ADDRESS_ID", referencedColumnName="ADDRESS_ID"))
private List<Address> addressList = new ArrayList<Address>();
...
}

@IdClass for ProductOrder entity

public class ProductOrderId {

private Long order;
private Long product;
...
}

Here's a sample code for creating the entities and persisting them:

    EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Order order = new Order();
order.setQuantity(10);
em.persist(order);

Product product = new Product();
product.setName("Coffee");
em.persist(product);

Address address = new Address();
address.setState("CA");
em.persist(address);

ProductOrder productOrder = new ProductOrder();
productOrder.setOrder(order);
productOrder.setProduct(product);

productOrder.getAddressList().add(address);
address.getProductOrderList().add(productOrder);

em.persist(productOrder);

em.getTransaction().commit();

Here's how the schema was generated in MySQL database:

Hibernate: 
create table ADDRESS (
ADDRESS_ID bigint not null auto_increment,
state varchar(255),
primary key (ADDRESS_ID)
)
Hibernate:
create table ORDERS (
ORDER_ID bigint not null auto_increment,
quantity integer not null,
primary key (ORDER_ID)
)
Hibernate:
create table PRODUCT (
PRODUCT_ID bigint not null auto_increment,
name varchar(255),
primary key (PRODUCT_ID)
)
Hibernate:
create table PRODUCT_ORDER (
ORDER_ID bigint,
PRODUCT_ID bigint,
primary key (ORDER_ID, PRODUCT_ID)
)
Hibernate:
create table PRODUCT_ORDER_ADDRESS (
ORDER_ID bigint not null,
PRODUCT_ID bigint not null,
ADDRESS_ID bigint not null
)
Hibernate:
alter table PRODUCT_ORDER
add constraint FK_sl39bwx60xjbvoiujpaes74ty
foreign key (ORDER_ID)
references ORDERS (ORDER_ID)
Hibernate:
alter table PRODUCT_ORDER
add constraint FK_n0i7uxq6rxsc0mcred1cds4m9
foreign key (PRODUCT_ID)
references PRODUCT (PRODUCT_ID)
Hibernate:
alter table PRODUCT_ORDER_ADDRESS
add constraint FK_kad6crei9lgrv1nuuuff42vs8
foreign key (ADDRESS_ID)
references ADDRESS (ADDRESS_ID)
Hibernate:
alter table PRODUCT_ORDER_ADDRESS
add constraint FK_hpx0e467dvpqi5i6kxmujns2b
foreign key (ORDER_ID, PRODUCT_ID)
references PRODUCT_ORDER (ORDER_ID, PRODUCT_ID)

How to join intermediate table in ManyToMany mappings in the hibernate

Short answer:

you should divide your many to many association to two many to one association and use @Embedded annotation to define compound key for your intermediate entity
look at below link for detail

https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/

Long answer:

Actually when you use @ManyToMany annotation to establish many to many association in hibernate it creates another table to establish ManyToMany association behind the scene this table is defined with @JoinTable annotation in your code, query to that table is completely managed by hibernate to achieve many to many association and you don't have entity which is mapped to that table. It is fine unless you need extra column in your intermediate table(here STUDENT_COURSE) or you want to execute some specific query to that table in a way that you behave with your other entity and you want to access the Id of your Course and Student table in CourseStudent table. because when hibernate creates intermediate table behind the scene you don't have entity in your program that maps to that table and therefore you can't have other column for that table and you can't behave to that table as an entity in your program. if you need extra column you can divide your @ManyToMany association to two @ManyToOne association. Here you should have another entity with name StudentCourse which has two @ManyToOne association one to Student entity and the other one to Course entity. However when you are defining your many to many association this way you should consider data integriry for instance the combination of student_id and course_id in your student_course table is unique and actually it should be a compound key in your student_course table. Fortunately there are some annotations in hibernate and jpa that provide this capability. Writing all the code for this sitiuation in this answer is redundant because there is already a good example in the link below .

https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/

Mapping many-to-many association table with extra column(s)

Since the SERVICE_USER table is not a pure join table, but has additional functional fields (blocked), you must map it as an entity, and decompose the many to many association between User and Service into two OneToMany associations : One User has many UserServices, and one Service has many UserServices.

You haven't shown us the most important part : the mapping and initialization of the relationships between your entities (i.e. the part you have problems with). So I'll show you how it should look like.

If you make the relationships bidirectional, you should thus have

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

If you don't put any cascade on your relationships, then you must persist/save all the entities. Although only the owning side of the relationship (here, the UserService side) must be initialized, it's also a good practice to make sure both sides are in coherence.

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

Why do I need the third table for many to many mapping ? Why can't I just use two tables?

The third table serves as a junction table that defines the many to many relationship. In your example I assume that a Person can have multiple addresses and an address can belong to multiple People. This relationship cannot be modeled using two tables.

You may attempt to simply include a foreign key to the Address in the Person table or a foreign key to Person in the Address table. Each of these mappings would have a one side, meaning one of the particular entities corresponds with many of the other. None of these options achieve the many to many relationship and the use of a third table is required to map the more complex relationship.

In order to map as a many to many you need to be able to associate multiple instances of both entities with each other. This is traditionally done via the following:

Table A
ID_A

Table B
ID_B

Table C
ID_A
ID_B

How to avoid saving duplicates in @ManyToMany, but insert into mapping table?

If I understand well your predicament, your problem is that you are trying to insert new PostTag when saving your Post entity.

Since you are doing a cascade save due to CascadeType.ALL, your EntityManager is roughly doing this:

  • Saving Post
  • Saving PostTag which cause your exception
  • Saving Post <-> PostTag

You should

  1. Have a service (for example: PostTag findOrCreateTagByName(String)) that fetch existing PostTag by name and eventually create them. Thus returning existing PostTag.
  2. Save the Post after with the association with said existing tags.

Edit (as answer to comment):

The JPA is only a mapping to a relational database.

In your code, you only shows the mapping which says that a Post is linked to several PostTag (and that PostTag are linked to several Post).

You added a unique constraint which apply on all tags: in all your database, there must be one tag "A", one tag "B", and so on.

If you populate your object like this (I don't use lombok, so I assume a minimal constructor here):

Post post = new Post();
post.setXXX(...);
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("B"));

This means that you create two new tags named A and B.

The JPA Implementation (Hibernate, EclipseLink) are not magic: they won't fetch for you the existing tag and that where it will fail. If you violate the unicity constraint on table post_tag, this means you are inserting twice the same value. Either in the same transaction, either because the tag is already present in the table.

For example:

post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("A"));

If you did not define correctly the hashCode(), then only the object identity hashCode would be used and there would be an attempt to add (insert) two tag A.

The only thing you can do here, is to restrict the PostTag by implementing correctly hashCode()/equals so that the PostTagSet ensure unicity only for the related Post.

Let's say now that you first fetch them and have a new tag C:

Post post = new Post();
post.setXXX(...);
for (String tagName : asList("A", "B", "C")) {
post.getPostTagSet().add(tagRepository.findByName(tagName)
.orElseGet(() -> new PostTag(tagName ));
}
postRepository.save(post);

The tagRepository is simply a Spring JPA Repository - which I think you are using - and the findByName signature is:

Optional<String> findByName(String tagName);

The code will do:

  • Find tag A: it is in database as in PostTag(1, "A")
  • Find tag B: it is in database as in PostTag(2, "B")
  • Find tag C: it is not in database, create it.

This should then work because the cascade will perform a save on the Post, then on the PostTag, then on the relation Post <-> PostTag.

In term of SQL query, you should normally see something like this:

insert into post_tag (tag_id, name) (3, "C")
insert into post (post_id, ...) (<some id>, ...)
insert into post_tag_mapping (tag_id, post_id) (1, <some id>)
insert into post_tag_mapping (tag_id, post_id) (2, <some id>)
insert into post_tag_mapping (tag_id, post_id) (3, <some id>)

The other problem here is with the hashCode() and equals() provided by PostTag which ensure unicity of PostTag for one single Post:

If you use the id in hashCode() (and equals use id and name):

  • If you use the id, then the set will have PostTag(1, "A"), PostTag(2, "B") and PostTag("C")
  • When you save, PostTag("C") will have an id assigned -> PostTag(3, "C")
  • With standard HashSet, the PostTag("C") will no longer be in its valid bucket and you will fail to find it again.

This may not be problematic if you don't use the object after the set but I think it is best to first save the PostTag (assigning it an id) then add it to the set.

If you use the name in hashCode() and equals: as long as you don't update the name after insertion in the set, you won't have a problem.



Related Topics



Leave a reply



Submit