ManyToMany - delete entries in a Join table - java

I have two entities and they have many-to-many relationship, what I want to achieve is when I delete one entity, then all entries referencing it in a join table should be removed as well. How do I do that?
So far I have:
#JoinTable(name = "interviewer_technology",
joinColumns = {
#JoinColumn(name = "interviewer_id", referencedColumnName = "id")
},
inverseJoinColumns = {
#JoinColumn(name = "technology_id", referencedColumnName = "id")
})
#ManyToMany(fetch = FetchType.LAZY)
private Collection<Technology> technologies;
in the owning entity and:
#ManyToMany(mappedBy = "technologies", fetch = FetchType.LAZY)
private Collection<Interviewer> interviewers;
in the inverse one, but when I try to delete the inverse one, I get the error that there are fields in join table referencing it. How can I tackle this problem?

first include in your #ManyToMany annotations
cascade=CascadeType.ALL.
After that make sure the calling class has a valid transaction going on and that your target entity is attached to your PersistenceContext at the moment it's being deleted, if not so, you might have to do a merge for the entity, check also if the resulting SQLs are being executed by your DB:
Sample delete method of Business Class:
#TransactionAttribute(TransactionAttributeType.REQUIRED)
public void delete(T entity) throws ServiceException {
try {
if (!entityManager.contains(entity)) {
entity = entityManager.merge(entity);
}
entityManager.remove(entity);
} catch (Exception se) {
se.printStackTrace();
throw new ServiceException("Error while deleting from DB " +
entity.getClass().getSimpleName() + " id: " + entity.getId());
}
}

Related

Why does persisting entity twice avoid errors around cascading for entity with join columns

Why does this unit test fail if i do not perform the setup of the entity Role in two steps (two persists).
The error being:
java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: io.osram.olt.extension.jpa.Role#16daa399.
private Role addRoleWithId(String roleId){
Role myRole = new Role();
myRole.setRoleId(roleId);
myRole.setRealmId("my");
myRole.setDescription("role-description-0");
myRole.setExternalCreator(true);
myRole.setName("role-name-0");
em.persist(myRole); //<--- Without this persisting the role fails with the error above.
//Setup joins:
myRole.setAContext(getApplications().get(0));
myRole.setAnotherContext(getTenants().get(0));
em.persist(myRole);
return myRole;
}
...
The Role Entity:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ANOTHER_CONTEXT_ID")
private AnotherContext anotherContext;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ACONTEXT_ID")
private AContext aContext;
...
public Role setAContext(AContext aContext) {
this.aContext = aContext;
if(aContext != null) {
aContext.addRole(this);
}
return this;
}
public Role setAnotherContext(AnotherContext anotherContext) {
this.anotherContext = anotherContext;
if(anotherContext != null){
anotherContext.addRole(this);
}
return this;
}
...
The AContext and AnotherContext both contain similar relations towards role:
#OneToMany
#JoinTable(
name="OLT_ROLES_ACONTEXT",
joinColumns = #JoinColumn( name="ACONTEXT_ID"),
inverseJoinColumns = #JoinColumn( name="ROLE_ID")
)
private Set<Role> roles = new HashSet<Role>();
It seems by creating the object in two steps I can avoid using cascading.
In your setAContext and setAnotherContext methods, you are trying to set the Role object which is not yet persisted.
So It's clear that it will not work without em.persist(myRole); before you set contexts since you have not specified CaseCadeType.PERSIST.
The default setting for cascading is cascade NONE , which causes the relationships in the persisted entity not to be persisted by default.
the corollary is that if you try to persist an entity without cascade.PERSIST to its relationship while the relationship is not managed , you will get the above exception.
An exception of the corollary is that if the entity you are persisting is the owner of the relation and the attribute in the relation is already in the database, yo will be able to persist it.
One small thing that I noticed in your mapping : It's a double unidirectional, one with a join column and the reverse with a join table, so is this intended?

Delete hibernate entity which is referenced by #ManyToMany in other entity

I want to delete Recipe (using spring data DAO) but I got SQL exception: org.postgresql.util.PSQLException: ERROR: update or delete on table "recipe" violates foreign key constraint "fkacys689tmdmfggtf4thdoc83k" on table "favourite_recipes"
Detail: Key (id)=(76823) is still referenced from table "favourite_recipes".
My entities:
#Entity
public class Account {
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "favourite_recipes",
joinColumns = #JoinColumn(name = "account_id"),
inverseJoinColumns = #JoinColumn(name = "recipe_id"))
private Set<Recipe> favouriteRecipes = new HashSet<>(0);
...
}
#Entity
public class Recipe {
...
}
How to remove recipe instance?
You need to handle the cascade type, by default is set to ALL.
For example you can work around the contraints like this:
#ManyToMany(cascade = CascadeType.DETACH)
more info : cascade type docs
in you need to delete from the owning entity side which is the Account.
So first remove the recipe from recipe list in Account and save the account, then remove the recipe itself.
As Amer Qarabsa metioned I had to remove recipe from Account.
I added new field in Recipe to get bidirectional mapping
#ManyToMany(cascade = CascadeType.MERGE, mappedBy = "favouriteRecipes")
private Set<Account> recipeLovers = new HashSet<>(0);
Code in service class to remove recipe from all accounts + clear lovers in recipe (recipe and recipeId variables are not initialized here)
Set<Account> recipeLovers = recipe.getRecipeLovers();
recipeLovers.forEach(account ->
account.getFavouriteRecipes()
.removeIf(r -> r.getId() == recipeId));
recipeLovers.clear();
recipeDao.delete(recipe);

Double-foreign-key in Hibernate when mapping the same entity twice

It is common practice to map the same entity twice or even thrice, every time with a subset of columns needed for processing. I have found that with Hibernate 3.5.1, every time a #ManyToOne or a #OneToMany exists in two entities mapping the same table, the foreign key is created twice. This has no impact on MySQL and SQL Server, but Oracle refuses the second creation statement.
Here is an example:
#Entity
#javax.persistence.SequenceGenerator(name = "SEQ_STORE", sequenceName = "SEQ_ENTITY")
#Table(name = "ENTITIES")
class Entity {
//All columns
//And then.....
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "BRIDGE_TABLE", joinColumns = { #JoinColumn(name = "ENTITY_ID") }, inverseJoinColumns = { #JoinColumn(name = "ROLE_ID") })
#OrderBy("id DESC")
private Set<Role> roles = new HashSet<Roles>();
}
#Entity
#javax.persistence.SequenceGenerator(name = "SEQ_STORE", sequenceName = "SEQ_ENTITY")
#Table(name = "ENTITIES")
class EntityListItem {
//Only a subset of the previous columns
//And then.....
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "BRIDGE_TABLE", joinColumns = { #JoinColumn(name = "ENTITY_ID") }, inverseJoinColumns = { #JoinColumn(name = "ROLE_ID") })
#OrderBy("id DESC")
private Set<Role> roles = new HashSet<Roles>();
}
Currently, Role is designed not to be navigable to Entity (otherwise I guess there will be 4 foreign keys).
Here are the statement being issued by Hibernate:
create table BRIDGE_TABLE (ENTITY_ID number(19,0) not null, ROLE_ID varchar2(60 char) not null, primary key (ENTITY_ID, ROLE_ID)); //Creates the table
alter table BRIDGE_TABLE add constraint FK47CFB9F0B068EF3F foreign key (ENTITY_ID) references ENTITIES;
alter table BRIDGE_TABLE add constraint FK47CFB9F0B068EF3F foreign key (ENTITY_ID) references ENTITIES;
I'm not sure whether this is a Hibernate bug. We cannot currently move to Hibernate 4. Can be this fixed via code or does it need a new Hibernate version?
I have made a workaround:
Add a #ForeignKey annotation with the same FK name to both entities (e.g. #ForeignKey(name = "FK_TO_ENTITY", inverseName = "FK_TO_ROLE"))
Extend LocalSessionFactoryBean like the following:
#override
public void createDatabaseSchema() throws DataAccessException
{
logger.info("Creating database schema for Hibernate SessionFactory");
SessionFactory sessionFactory = getSessionFactory();
final Dialect dialect = ((SessionFactoryImplementor) sessionFactory).getDialect();
final LinkedHashSet<String> sql = new LinkedHashSet<String>();
for (String query : getConfiguration().generateSchemaCreationScript(dialect))
sql.add(query);
HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory);
hibernateTemplate.execute(new HibernateCallback<Void>()
{
#Override
public Void doInHibernate(Session session) throws SQLException
{
session.doWork(new Work()
{
#Override
public void execute(Connection conn) throws SQLException
{
PhoenixAnnotationSessionFactoryBean.this.executeSchemaScript(conn, sql.toArray(new String[0]));
}
});
return null;
}
});
}
Reason: the #ForeignKey annotation ensures that the FKs will have the same name, hence the SQL statements will be equal each other. The overriden LSFB will store the SQL queries needed to create the schema in a Set so that no duplicate will be allowed.

Hibernate not updating ManyToMany associations during session

I am working on a project using Hibernate 4.3.4 to access a Postgres DB. We have two entities which are linked via a ManyToMany Association.
The code and the associations currently work, in that adding an EntityB to EntityA's collection will automatically add the EntityA to the EntityB's collection once the Session is committed. However, the issue I'm having is that when I try to work on the EntityB's EntityAs, which should include the EntityA I just created, EntityA is not in that collection (It is empty). Example code is here:
#Entity
#Table(name = "entity_a")
public class EntityA {
private Set<EntityB> entityBs = new HashSet<EntityB>(0);
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "entitya_entityb",
joinColumns = { #JoinColumn(name = "entitya_id") },
inverseJoinColumns = { #JoinColumn(name = "entityb_id") })
public Set<EntityB> getEntityBs()
{
return entityBs;
}
public void setEntityBs(Set<EntityB> entityBs)
{
this.entityBs = entityBs;
}
}
#Entity
#Table(name = "entity_b")
public class EntityB {
private Set<EntityA> entityAs = new HashSet<EntityA>(0);
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "entitya_entityb",
joinColumns = { #JoinColumn(name = "entityb_id") },
inverseJoinColumns = { #JoinColumn(name = "entitya_id") })
public Set<EntityA> getEntityAs()
{
return entityAs;
}
public void setEntityAs(Set<EntityA> entityAs)
{
this.entityAs = entityAs;
}
}
/**
* HTTP REST Resource to create Entities and persist them. We do some basic logic when we create them to show the problem.
*/
#Path("/battleRhythm")
#Singleton
public class HttpResource
{
#POST
#Consumes("application/json")
public void createEntityA() {
Session hibernateSession = SessionFactory.getCurrentSession(); // SessionFactory specifics not included
hibernateSession.getTransaction().begin();
// Add an EntityB to the new EntityA
EntityA entityA0 = new EntityA();
EntityB entityB0 = new EntityB0();
entityA.getEntityBs().add(entityB0);
// Persist the new EntityA
EntityADao.getInstance().save(entityA0);
// Try to get this EntityA from EntityB
Set<EntityA> associatedEntityAs = entityB0.getEntityAs(); // Doesn't contain any EntityAs!
hibernateSession.getTransaction().commit();
}
}
Here's the question:
Can I make Hibernate automatically add the EntityA0 to EntityB0's collection when I save EntityA0, without committing the transaction? Is this possible? How?
Caveat : The example above does not fully reflect this, but we perform similar operations on both Entities, so having an "owner" in the traditional Hibernate sense (using the mappedBy = "" Attribute configuration) is not an ideal option. I don't want to try to convince everyone to only ever use EntityB.getEntityAs().add(EntityB0) in CreateEntityA(). It's too confusing.
You don't have the choice. There MUST be an owner side, and there MUST be an inverse side. And it's YOUR responsibility to maintain both sides of the association: don't expect to have B inside A's collection of Bs when you only add A to B (and vice-versa)
Now, nothing forbids you to have a methods addB(B b) inside A that adds b to A's collection of Bs, and which adds this to B's collection of As. And you can of course also have a method addA(A a) in B that does the same thing.

How to delete all associations in a Hibernate JoinTable at once?

We have the following two entities with many-to-many association:
#Entity
public class Role {
...
#ManyToMany
#JoinTable( name = "user_has_role", joinColumns = { #JoinColumn( name = "role_fk" ) }, inverseJoinColumns = { #JoinColumn( name = "user_fk" ) } )
private Set<User> userCollection;
...
}
and
#Entity
public class User {
...
//bi-directional many-to-many association to Role
#ManyToMany( mappedBy = "userCollection" )
private Set<Role> roleCollection;
...
}
If we want to truncate all data with
em.createQuery( "DELETE Role" ).executeUpdate();
we have to clear all associations in the "user_has_role" JoinTable like shown in this answer:
for ( ... )
{
A a = aDao.getObject(aId);
B b = bDao.getObject(bId);
b.getAs().remove(a);
a.getBs().remove(b);
bDao.saveObject(b);
}
Is there a way to do delete all associations in the JoinTable at once without iterating over all the data?
Maybe there is a special HQL-Command like DELETE Role.user_has_role ?
While the JPA spec clearly writes that bulk operations are not cascaded to related entities (section 4.10 Bulk Update and Delete Operations), I expect providers to deal at least with join tables. Sadly, Hibernate doesn't and this is logged in HHH-1917. Workaround: use native SQL.

Categories