Hibernate Bi-Directional Many to Many association creates duplicates - java

My question is pretty similar to this one Hibernate Bi-Directional ManyToMany Updates with Second Level cache
I've class as shown below
#Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
#Entity
public class A{
private int id;
private List<B> listB;
...
#Cache (usage = CacheConcurrencyStrategy.TRANSACTIONAL)
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, targetEntity = B.class)
#JoinTable(name = "A_B", joinColumns = { #JoinColumn(name = "a_id") },
inverseJoinColumns = { #JoinColumn(name = "b_id") })
public List<B> getListB() {
return listB ;
}
}
#Cache (usage = CacheConcurrencyStrategy.TRANSACTIONAL)
#Entity
public class B{
private int id;
private List<A> listA;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, targetEntity = A.class)
#JoinTable(name = "A_B", joinColumns = { #JoinColumn(name = "b_id") }, inverseJoinColumns = { #JoinColumn(name = "a_id") })
public List<a> getListA() {
return listA ;
}
public void addToA(A a) {
a.getListB().add(this);
}
}
As expected in a Many to Many relation, I've been updating both sides of the bi-directional relation .
Now the problem I face is, duplicate entries popup when I try to add/update a item in the collection. The following is the code I use to persist the entity...
b.getA().clear() ;
...
...
b.getListA().add(A) ;
b.addToA(A) ;
em.saveOrUpdate(b) ;
The following are the queries fired by Hibernate which are obtained from the logs.
delete from A_B where b_id=?
insert into A_B (b_id, a_id) values (?, ?)
insert into A_B (b_id, a_id) values (?, ?)
delete from A_B where a_id=?
insert into A_B (a_id, b_id) values (?, ?)
insert into A_B (a_id, b_id) values (?, ?)
insert into A_B (a_id, b_id) values (?, ?)
insert into A_B (a_id, b_id) values (?, ?)
Where am I going wrong here ?
How to get rid of the duplicates that are being inserted ?
Cache is being flushed properly but the duplicate entries are the only problem !

This is a classic!
The problem is that both of your mappings are owners, when one should be the owner, and one should be inverse. Because both are owners, changes to either will result in insertions to the database; with one owner and one inverse, there will only be one set of insertions.
You should be able to rewrite B::getListA as:
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "listB")
public List<A> getListA()
And have everything work.
Note that only the owning side as the #JoinTable annotation. As a rule, any given bit of the database is mapped in exactly one place in a JPA application. If you ever find yourself mapping something twice, take a long hard look to see if there's a better way to do it.
Incidentally, you don't need the targetEntity attributes; a JPA provider can work that out from the generics.

Related

Hibernate automatically deleting lines in table without expressly being told so? (#ManyToMany association, Spring JPA)

I am working on a Spring JPA, microservices-based project where I have two entities, UserEntity and RegionEntity, associated via a many-to-many relationship.
Below the (simplified) code for the two entities:
#EqualsAndHashCode(of = {"id"})
#Data
#Entity
#Table(name = "users")
#Audited
public class UserEntity implements Serializable {
#Id
#Column(name = "userId", columnDefinition = "int", unique = true, nullable = false)
private Long id;
#NotAudited
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "users_regions",
joinColumns = #JoinColumn(name = "userId", columnDefinition = "int", nullable = false, updatable = false),
inverseJoinColumns = #JoinColumn(name = "regionId", columnDefinition = "varchar(50)", nullable = false, updatable = false))
private Set<RegionEntity> regions = new HashSet<>();
}
#EqualsAndHashCode(exclude = {"users"})
#Entity
#Table(name = "regions")
#Data
#Audited
public class RegionEntity {
#Id
#Column(name = "name", columnDefinition = "varchar(50)", unique = true, nullable = false)
private String name;
#ManyToMany(mappedBy = "regions", fetch = FetchType.EAGER)
private Set<UserEntity> users = new HashSet<>();
}
Now, I have a route for persisting instances of a third entity, CallEntity, that makes use of both the previous entities. It may be useful to know that CallEntity has also a many-to-many association with RegionEntity, and a many-to-one with user.
Here's in short the method invoked by the route.
public CallDTO myMethod(String userName, /* ... more stuff */) {
UserProjection userProj = userConsumer.findByUserName(userName, UserBasicProjection.class); // from another repository, retrieve a user projection
UserEntity user = userRepository.findById(user.getId()).orElse(null); // use that projection to initialise the user
Set<RegionEntity> userRegions = user != null ? user.getRegions() : null;
CallEntity newCall = new CallEntity();
/ * some actions, mainly setting values for newCall using userRegions and user... */
CallEntity call = callRepository.save(newCall);
return callMapper.toDTO(call);
}
The problem is that Hibernate automatically deletes the lines related to the user-region pair in the associated table when saving the CallEntity object. So for example, if the user has an id of 1234 and has regions A, B and C associated,
user | region
-------------
1234 | A
-------------
1234 | B
-------------
1234 | C
-------------
then all three lines will be deleted in the table after this line of code:
CallEntity call = callRepository.save(newCall);
is executed.
This is the Hibernate version currently in use:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.3.12.Final</version>
</dependency>
This is the console printout after execution:
Hibernate:
/* get current state package-name.UserEntity */ select
userent_.userId
from
user userent_
where
userent_.userId=?
Hibernate:
/* get current state package-name.RegionEntity */ select
regione_.name
from
region regione_
where
regione_.name=?
Hibernate:
/* insert package-name.CallEntity
*/ insert
into
call
(id, userid, regionid)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
last_insert_id()
Hibernate:
/* delete collection package-name.UserEntity.regions */ delete
from
users_regions
where
userId=?
Hibernate:
/* insert collection
row package-name.CallEntity.userRegions */ insert
into
call_region
(id, name)
values
(?, ?)
I found that if I change the direction of the relationship (the owning entity being, in this case, RegionEntity) the problem disappears. However, this is by no means a viable solution, as it would impact other parts of the project.
Also, I found that a similar question was asked before on this site (ManyToMany assoicate delete join table entry), but unfortunately the answer was not satisfactory. I tried adding and using convenience methods to correctly establish the association (as the answer from the linked question suggests), but that just didn't work.
Any help would be immensely appreciated.
Thank you.
Finally, the problem was solved by wrapping the set of regions in a copy.
That is, by replacing this line:
Set userRegions = user != null ? user.getRegions() : null;
with:
Set userRegions = userxpc != null ? Set.copyOf(userxpc.getRegions()) : null;

How to prevent JPA query related entity

Firstly, let me thank to who want to give the answer,
I am a new person using JPA for now, suppose that I have two tables, table A and B, for table A, we have the column
id_A,
id_B,
etc...
For table B, we have
id_B
And we have the entity classes,
#Data
class A {
#Id
#Column(name = "ID_A")
private String id;
#ManyToOne
#JoinColumn(name = "ID_B", referencedColumnName = "ID_B")
private B b;
}
#Data
class B {
#Id
#Column(name = "ID_B")
private String id;
#JsonManagedReference
#OneToMany(mappedBy = "b", cascade = CascadeType.ALL)
#BatchSize(size = BATCH_SIZE)
private List<A> as;
}
Now, in order to only use the id of B, I firstly select all the relevant entity of A by ARepository.findAllByIds().
with the above query, I want to get all the ID_Bs, but when I do it by looping the result of the above query - a.getB, I noticed that it will get the B from Database again.
I know that we can fetch the A and B together by EAGER or EntityGraph, etc...
I am just wondering, is there any a way that I can only get the id_b from the ARepository.findAllByIds()?
To answer your question I think you will have to annotate your findAllByIds method with something like...
#Query(value = "SELECT a, b.id FROM A a LEFT JOIN a.b b where a.id in :ids") then your Bs will be loaded within the same query as the As

Many to many relationship for same type entity

I have an entity as below. I am curious if it is possible to create a relationship as I will be describing with the example:
I am creating 2 Person entities Michael and Julia.
I am adding Julia to Michael's friends set.
After that I am retrieving Michael as a JSON response and Julia is available in the response. But when I am retrieving Julia, her friends set is empty. I want to create the bidirectional friendship relation by saving just one side of the friendship. I would like to get Michael on Julia's friends set without doing any other operations. I think that it must be managed by Hibernate. Is it possible and how should I do it?
#ToString(exclude = "friends") // EDIT: these 2 exclusion necessary
#EqualsAndHashCode(exclude = "friends")
public class Person{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name",unique = true)
private String name;
#JsonIgnoreProperties("friends") // EDIT: will prevent the infinite recursion
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "FRIENDSHIP",
joinColumns = #JoinColumn(name = "person_id",
referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "friend_id",
referencedColumnName = "id"))
private Set<Person> friends;
Here is my service layer code for creating a friendship:
#Override
public Person addFriend(String personName, String friendName)
throws FriendshipExistsException, PersonNotFoundException {
Person person = retrieveWithName(personName);
Person friend = retrieveWithName(friendName);
if(!person.getFriends().contains(friend)){
person.getFriends().add(friend);
return repository.save(person);
}
else{
throw new FriendshipExistsException(personName, friendName);
}
}
Related Question:
N+1 query on bidirectional many to many for same entity type
Updated the source code and this version is working properly.
// Creating a graph to help hibernate to create a query with outer join.
#NamedEntityGraph(name="graph.Person.friends",
attributeNodes = #NamedAttributeNode(value = "friends"))
class Person {}
interface PersonRepository extends JpaRepository<Person, Long> {
// using the named graph, it will fetch all friends in same query
#Override
#EntityGraph(value="graph.Person.friends")
Person findOne(Long id);
}
#Override
public Person addFriend(String personName, String friendName)
throws FriendshipExistsException, PersonNotFoundException {
Person person = retrieveWithName(personName);
Person friend = retrieveWithName(friendName);
if(!person.getFriends().contains(friend)){
person.getFriends().add(friend);
friend.getFriends().add(person); // need to setup the relation
return repository.save(person); // only one save method is used, it saves friends with cascade
} else {
throw new FriendshipExistsException(personName, friendName);
}
}
If you check your hibernate logs, you will see:
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into friendship (person_id, friend_id) values (?, ?)
Hibernate: insert into friendship (person_id, friend_id) values (?, ?)

hibernate createAlias with clause generates wrong query

I have the following tables:
A: id
B: id, text
AB: aID, bID
I want to joib A and B where B.text contains the word 'cat'.
This is the hibernate query I do:
Criteria c = session.createCriteria(TableA.class, "A");
c.createAlias("A.bs", "B", JoinType.INNER_JOIN, Restrictions.like("b.text", "%cat%"));
c.setProjection(Projections.property("id"));
The generated query is:
Select id
FROM A a
INNER JOIN AB ab ON a.id=ab.aID AND (b.text like ?)
INNER JOIN B b ON b.id=ab.bID AND (b.text like ?)
For some reason AND (b.text like ?) appears in both inner joins. I far as I understand its supposed to be only in the second on.
This causes the following exception:
java.sql.SQLException: No value specified for parameter 2
I guess it's happening because it has only one parameters and two '?'.
What am I missing?
EDIT:
Adding the persistent classes:
#Entity
#Table(name="A")
Class A {
#Id
#Column(name="id", length=255)
protected String id;
#OneToMany
#JoinTable(name="AB", joinColumns = #JoinColumn( name="aID"), inverseJoinColumns = #JoinColumn( name="bID"))
#LazyCollection(LazyCollectionOption.FALSE)
protected List<B> bs;
}
#Entity
#Table(name="B")
Class B {
#Id
#Column(name="id", length=255)
protected String id;
#Column(name="text", length=255)
protected String text;
}
I think you need to create an alias
c.createAlias("A.bs", "b", JoinType.INNER_JOIN, Restrictions.like("b.text", "%cat%"));
Updated
I think It is a Hibernate bug. You can fix it by using a restriction in the where clause
c.createAlias("A.bs", "b", JoinType.INNER_JOIN);
c.add(Restrictions.like("b.text", "%cat%"));
or don't use join table and do association by foreign key in B.

#ElementCollection, #CollectionTable and Enum - Strange delete/insert behavior

#Entity
public class User{
#ElementCollection
#Enumerated(EnumType.STRING)
#CollectionTable(name = "SEC_USER_ROLES",
joinColumns =
#JoinColumn(name = "USER_ID", referencedColumnName = "ID"))
#Column(name = "ROLE_NAME")
private List<Role> roles;
[...]
}
public enum Role {
ROLE_SUPER_ADMIN,
ROLE_ADMIN,
ROLE_ARB,
ROLE_AP;
[...]
}
With this mapping, when I try do delete one ROLE, for example ROLE_ARB, it always ends up with deleting the role and inserting it once again.
DELETE FROM SEC_USER_ROLES WHERE ((USER_ID = ?) AND (ROLE_NAME = ?))
bind => [9451, ROLE_ADMIN]
INSERT INTO SEC_USER_ROLES (USER_ID, ROLE_NAME) VALUES (?, ?)
bind => [9451, ROLE_ADMIN]
I tried to solve the problem with #OrderColumn (name="USER_ID") but then the mapping of the User_id is not correct.
Any idea would be appreciated.
The Roles are represented as selectManyCheckbox
The ManagedBean prepares the entity (User)
...
List<String> selectedroles = this.getSelectedItems();
List<Role> newroles = new ArrayList<Role>();
if (selectedroles != null) {
for (String r : selectedroles) {
newroles.add(Role.valueOf(r));
}
getEntity().setRoles(newroles);
...
security.save(getEntity());
and the EJB makes updates if it is an existing Entity
EntityManager em;
...
this.em.merge(user);
So when someone deselects all (previous selected) Roles there is always one Role left in the database which is not deleted, because of the delete/insert behavior I described before.
#OrderColumn solved the problem

Categories

Resources