Cannot delete child in one-to-many relationship - java

When I put inverse=true into set, nothing gets deleted. When I don't, and I remove MealIngredient from set, then Hibernate tries to set null, it fails and exception is thrown:
[SQLITE_CONSTRAINT] Abort due to constraint violation (MealIngredients.mealId may not be NULL)
Here are XML mappings:
<class name="restaurant.meal.Meal" table="Meals">
<id name="id" type="integer">
<column name="id" not-null="true" unique="true"/>
<generator class="increment"/>
</id>
<!-- some other, simple properties -->
<set name="ingredientsSet" cascade="all" lazy="false">
<key>
<column name="mealId" not-null="true" />
</key>
<one-to-many class="restaurant.meal.MealIngredient" />
</set>
</class>
<class name="restaurant.meal.MealIngredient" table="MealIngredients">
<composite-id name="id" class="restaurant.meal.MealIngredient$Id">
<key-property name="ingredientId" />
<key-property name="mealId" />
</composite-id>
<many-to-one name="ingredient" class="restaurant.storage.Ingredient" insert="false" update="false" lazy="false">
<column name="ingredientId" not-null="true" />
</many-to-one>
<many-to-one name="meal" class="restaurant.meal.Meal" insert="false" update="false" lazy="false">
<column name="mealId" not-null="true" />
</many-to-one>
<!-- other properties -->
</class>
Yes, the relationship between Meal and Ingredient is many-to-many with join table MealIngredient (and yes, I have to map MealIngredient as well, because of additional columns in that table).
This question did not help me, neither did this.
Edit:
Only inserting works with current mapping, update just generates another row in MealIngredient table.
Edit 2: hashCode and equals implementations:
MealIngredient$Id: (uses Apache commons-lang EqualsBuilder and HashCodeBuilder)
#Override
public boolean equals(Object o) {
if(!(o instanceof Id))
return false;
Id other = (Id) o;
return new EqualsBuilder()
.append(this.getMealId(), other.getMealId())
.append(this.getIngredientId(), other.getIngredientId())
.isEquals();
}
#Override
public int hashCode() {
return new HashCodeBuilder()
.append(this.getMealId())
.append(this.getIngredientId())
.hashCode();
}
MealIngredient:
#Override
public boolean equals(Object o)
{
if(!(o instanceof MealIngredient))
return false;
MealIngredient other = (MealIngredient) o;
return this.getId().equals(other.getId());
}
#Override
public int hashCode()
{
return this.getId().hashCode();
}
I checked log and although I don't know what Hibernate does under the hood, but it does make the insert into MealIngredient:
15:42:53,122 TRACE IntegerType:172 - returning '5' as column: quantity3_
Hibernate:
insert
into
MealIngredients
(quantity, ingredientId, mealId)
values
(?, ?, ?)
15:42:53,131 TRACE IntegerType:133 - binding '16' to parameter: 1
15:42:53,131 TRACE IntegerType:133 - binding '5' to parameter: 2
15:42:53,131 TRACE IntegerType:133 - binding '1' to parameter: 3
And when Iā€‚remove MealIngredient from Meal.ingredientsSet, Hibernate makes update and tries to set mealId to null:
Hibernate:
update
MealIngredients
set
quantity=?
where
ingredientId=?
and mealId=?
15:48:57,529 TRACE IntegerType:126 - binding null to parameter: 1
15:48:57,529 TRACE IntegerType:133 - binding '1' to parameter: 2
15:48:57,531 TRACE IntegerType:133 - binding '1' to parameter: 3
15:48:57,535 WARN JDBCExceptionReporter:77 - SQL Error: 0, SQLState: null
15:48:57,535 ERROR JDBCExceptionReporter:78 - [SQLITE_CONSTRAINT] Abort due to constraint violation (MealIngredients.quantity may not be NULL)

I believe the explanation you're looking for is here. Well, sort of. Don't read his explanation, it confuses me. His examples are excellent though.
So, anyways, I think you want to do one of the following:
inverse=false and remove the mealIngredient from your ingredients
collection and then save the Meal
inverse=true and have to null the meal instance variable in MealIngredient and save the MealIngredient
EDIT: The issue with inserts instead of updates is probably due to the fact that you have not over-ridden hashcode and equals. If you're using Eclipse, I believe it can do it for you, but you must tell it to use both properties of your composite key when it auto generates the methods. Per Hibernate documentation chapter 5:
The persistent class must override equals() and hashCode() to
implement composite identifier equality. It must also implement
Serializable.

Unfortunately, it seems that Hibernate does not work well with composite primary keys. I had to add extra ID column into many-to-many join tables (like my MealIngredient) and work with that.
After I use extra ID as primary key, inserting/updating/deleting works as expected (even with cascade set to delete-orphan, cascade deleting works!).
I provide final mappings for entities Meal and MealIngredient, for future reference. I hope this will help others, when they stumble upon many-to-many relationships with additional properties/columns in join table.
<class name="restaurant.meal.Meal" table="Meals">
<id name="id" type="integer">
<column name="id" not-null="true" unique="true"/>
<generator class="increment"/>
</id>
<!-- additional properties -->
<set name="ingredientsSet" table="MealIngredients" cascade="all-delete-orphan" lazy="false" inverse="true">
<key update="true">
<column name="mealId" not-null="true" />
</key>
<one-to-many class="restaurant.meal.MealIngredient" />
</set>
</class>
<class name="restaurant.meal.MealIngredient" table="MealIngredients">
<id name="id" type="integer">
<column name="id" not-null="true" unique="true"/>
<generator class="increment"/>
</id>
<many-to-one name="ingredient" column="ingredientId" not-null="true" class="restaurant.storage.Ingredient" lazy="false" />
<many-to-one name="meal" column="mealId" not-null="true" class="restaurant.meal.Meal" lazy="false" />
<!-- additional properties -->
</class>

Related

hibernate deletes list relation when updating other list containing same objects

In my application I have a setup like the following:
class A {
String id;
List<C> list;
}
class B {
String id;
List<C> list;
}
class C {
String id;
}
which is mapped in hibernate like
<class name="A" lazy="false">
<id name="id">
<generator class="org.hibernate.id.UUIDGenerator"/>
</id>
<list name="list" lazy="false" mutable="false">
<key column="listA_id"/>
<list-index column="listA_index"/>
<many-to-many class="C"/>
</list>
</class>
<class name="B" lazy="false">
<id name="id">
<generator class="org.hibernate.id.UUIDGenerator"/>
</id>
<list name="list" lazy="false" mutable="false">
<key column="listB_id"/>
<list-index column="listB_index"/>
<many-to-many class="C"/>
</list>
</class>
<class name="C" lazy="false">
<id name="id">
<generator class="org.hibernate.id.UUIDGenerator"/>
</id>
</class>
All 5 tables (one for each class and 2 for the many-many-relations) are created as expected. The problem appears when I execute some code like the following:
A a = new A();
B b = new B();
C c1 = new C();
C c2 = new C();
C c3 = new C();
<hibernatesession>.save(c1);
<hibernatesession>.save(c2);
<hibernatesession>.save(c3);
a.list.add(c1);
a.list.add(c2);
<hibernatesession>.save(a);
b.list.add(c1);
b.list.add(c3);
<hibernatesession>.save(b);
I followed the resulting SQL in a HSQL-log-file and found everything to be fine up until the last save. Saving b will result in the association between a and c1 being deleted from the many-many-tabel A_list. Fetching the objects a and b anew from the database will result in c1 and c3 being in b.list but only c2 in a.list.
Observations:
* There is no cascading in the game.
* switching the order of saving a/b will result in c1 getting deleted from the list of whoever gets saved first.
* Both lists are mapped with indexes.
* Both lists are mapped with different key/index-column names.
* hibernate deletes exactly those elements common to the lists
Can somebody explain me this behavior of hibernate and knows how to realize a respective mapping correctly?
Thx in advance!!!
Salsolatragus
Try adding a many-to-many mapping to Class C so that it is a bidirectional relationship. Perhaps by being explicit about the "parent" relationships, it will know to keep more than one relationship rather than delete the first one as you have explained.
<class name="C" lazy="false">
<id name="id">
<generator class="org.hibernate.id.UUIDGenerator"/>
</id>
<list name="listA" lazy="false" mutable="false" inverse="true">
<key column="listC_id"/>
<many-to-many class="A"/>
</list>
<list name="listB" lazy="false" mutable="false" inverse="true">
<key column="listC_id"/>
<many-to-many class="B"/>
</list>
</class>
Fixed the bug. Lessons learned: If you have strange correlations between certain Collections in hibernate, check first if you're accidentally using the same Java instance for them... A couple of hours pairDebugging thru hibernate at last led us to the right guess. Tricky thing. Problem solved.
Thx for you hint Brad!

Hibernate: Inconsistent primary key generation due to one-to-one relation

I have two one-to-one relations here between a class called "MailAccount" and the classes "IncomingServer" and "OutgoingServer".
(It's a Java application running on Tomcat and Ubuntu server edition).
The mapping looks like this:
MailAccount.hbm.xml
<hibernate-mapping package="com.mail.account">
<class name="MailAccount" table="MAILACCOUNTS" dynamic-update="true">
<id name="id" column="MAIL_ACCOUNT_ID">
<generator class="native" />
</id>
<one-to-one name="incomingServer" cascade="all-delete-orphan">
</one-to-one>
<one-to-one name="outgoingServer" cascade="all-delete-orphan">
</one-to-one>
</class>
</hibernate-mapping>
IncomingMailServer.hbm.xml
<hibernate-mapping>
<class name="com.IncomingMailServer" table="MAILSERVER_INCOMING" abstract="true">
<id name="id" type="long" access="field">
<column name="MAIL_SERVER_ID" />
<generator class="native" />
</id>
<discriminator column="SERVER_TYPE" type="string"/>
<many-to-one name="mailAccount" column="MAIL_ACCOUNT_ID" not-null="true" unique="true" />
<subclass name="com.ImapServer" extends="com.IncomingMailServer" discriminator-value="IMAP_SERVER" />
<subclass name="com.Pop3Server" extends="com.IncomingMailServer" discriminator-value="POP3_SERVER" />
</class>
</hibernate-mapping>
OutgoingMailServer.hbm.xml
<hibernate-mapping>
<class name="com.OutgoingMailServer" table="MAILSERVER_OUTGOING" abstract="true">
<id name="id" type="long" access="field">
<column name="MAIL_SERVER_ID" />
<generator class="native" />
</id>
<discriminator column="SERVER_TYPE" type="string"/>
<many-to-one name="mailAccount" column="MAIL_ACCOUNT_ID" not-null="true" unique="true" />
<subclass name="com.SmtpServer" extends="com.OutgoingMailServer" discriminator-value="SMTP_SERVER" />
</class>
</hibernate-mapping>
The class hierarchy looks like this:
public class MailAccount{
IncomingMailServer incomingServer;
OutgoingMailServer outgoingServer;
}
public class MailServer{
HostAddress hostAddress;
Port port;
}
public class IncomingMailServer extends MailServer{
// ...
}
public class OutgoingMailServer extends MailServer{
// ...
}
public class ImapServer extends IncomingMailServer{
// ...
}
public class Pop3Server extends IncomingMailServer{
// ...
}
public class SmtpServer extends OutgoingMailServer{
// ...
}
Now, here comes the problem:
Although most of the time my application runs well, there seems to be one situation in which email servers get deleted, but the corresponding account doesn't and that's when this call is made:
session.delete(mailAccountInstance);
In a one-to-one relation in Hibernate, the primary keys between mail account and its servers must be equal, if not, the relation completely gets out of sync:
Example:
Imagine, the tables are filled with data like this:
Table "MailAccount" (Current auto_increment value: 2)
MAIL_ACCOUNT_ID NAME
0 Account1
1 Account2
Table "IncomingMailServer" (Current auto_increment value: 2)
MAIL_SERVER_ID MAIL_ACCOUNT_ID
0 0
1 1
Now, image the account with ID=1 gets deleted and new accounts get added. The following then SOMETIMES happens:
Table "MailAccount" (Current auto_increment value: 3)
MAIL_ACCOUNT_ID NAME
0 Account1
1 Account2
2 Account3
Table "IncomingMailServer" (Current auto_increment value: 2)
MAIL_SERVER_ID MAIL_ACCOUNT_ID
0 0
1 2
This completely messes up my database consistency.
How can I avoid this?
If you want a shared primary key, you can use the native id generator only once. You create the mail account first, which will generate its own id, but when you create the Incoming- or OutgoingMailServer, these need to take their id from the mailAccount property.
So you need the "foreign" generator:
<class name="OutgoingMailServer">
<id name="id" column="MAIL_SERVER_ID">
<generator class="foreign">
<param name="property">mailAccount</param>
</generator>
</id>
<one-to-one name="mailAccount" not-null="true" constrained="true"/>
<class>
You don't need a MAIL_ACCOUNT_ID column, since it will always be identical to the MAIL_SERVER_ID anyway.
Quite basic follow the reference about bidirectional one-to-one association on a primary key.

Hibernate is updating instead of inserting, why?

I'm starting my adventure with Hibernate, so please be patient :)
I want to make mapping for two tables, for example A and B. The relation beetwen A and B is one-to-many.
I wrote this hbm.xml file:
<hibernate-mapping package="something">
<class name="A" table="A">
<id name="id" type="int" column="ID">
<generator class="native" />
</id>
<set name="setInA" sort="natural" cascade="all" lazy="false">
<key column="ANOTHER_ID"/>
<one-to-many class="B" />
</set>
</class>
<class name="B" table="B">
<id name="anotherId" type="int" column="ANOTHER_ID">
<generator class="native" />
</id>
</class>
</hibernate-mapping>
Of course I made also POJO classes A and B.
And now, when I try to do:
A a = new A();
Set<B> set = new TreeSet<B>();
set.add(new B());
a.setSetInA(set);
session.save(a);
Hibernate inserts new row to table A, but (what is the worst) is not inserting new row to B table, but only makes SQL Update on not existing row in B.
Can tell me anyone why it is happening? What I made wrong?
You should either persist B's objects firstly, or use Cascade option.
You can use Cascade without using annotations:
<set name="setInA" sort="natural" cascade="all" lazy="false" cascade="all">
<key column="ANOTHER_ID"/>
<one-to-many class="B" />
</set>
This will ensure that collection of B instances is inserted when you insert A.
Found this question while searching for causes of the same symptoms in my system. cascade="all" did not help.
In my case I solved this by adding a mapping to the list element, in this example class B.
Please note that the enclosing class (A in this example) also was versioned. Hibernate might require that versioning (used for optimistic locking) must be enabled for all nested elements. I haven't found any documentation to support this theory.

How to Stop Hibernate From Trying to Update one-to-many Over Join Table

Ok so I'm having bit of a problem with my Hibernate mappings and getting the desired behavior.
Basically what I have is the following Hibernate mapping:
<hibernate-mapping>
<class name="com.package.Person" table="PERSON" schema="MYSCHEMA" lazy="false">
<id name="personId" column="PERSON_ID" type="java.lang.Long">
<generator class="sequence">
<param name="sequence">PERSON_ID_SEQ</param>
</generator>
</id>
<property name="firstName" type="string" column="FIRST_NAME">
<property name="lastName" type="string" column="LAST_NAME">
<property name="age" type="int" column="AGE">
<set name="skills" table="PERSON_SKILL" cascade="all-delete-orphan">
<key>
<column name="PERSON_ID" precision="12" scale="0" not-null="true"/>
</key>
<many-to-many column="SKILL_ID" unique="true" class="com.package.Skill"/>
</set>
</class>
</hibernate-mapping>
<hibernate-mapping>
<class name="com.package.Skill" table="SKILL" schema="MYSCHEMA">
<id name="skillId" column="SKILL_ID" type="java.lang.Long">
<generator class="sequence">
<param name="sequence">SKILL_ID_SEQ</param>
</generator>
</id>
<property name="description" type="string" column="DESCRIPTION">
</class>
</hibernate-mapping>
So lets assume that I have already populated the Skill table with some skills in it. Now when I create a new Person I want to associate them with a set of skills that already exist in the skill table by just setting the ID of the skill. For example:
Person p = new Person();
p.setFirstName("John");
p.setLastName("Doe");
p.setAge(55);
//Skill with id=2 is already in the skill table
Skill s = new Skill()
s.setSkillId(2L);
p.setSkills(new HashSet<Skill>(Arrays.asList(s)));
PersonDao.saveOrUpdate(p);
If I try to do that however I get an error saying:
WARN (org.slf4j.impl.JCLLoggerAdapter:357) - SQL Error: 1407, SQLState: 72000
ERROR (org.slf4j.impl.JCLLoggerAdapter:454) - ORA-01407: cannot update ("MYSCHEMA"."SKILL"."DESCRIPTION") to NULL
ERROR (org.slf4j.impl.JCLLoggerAdapter:532) - Could not synchronize database state with session
org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
The reason I am getting this error I think is because Hibernate sees that the Skill with Id 2 has 'updated' its description to null (since I never set it) and tries to update it. But I don't want Hibernate to update this. What I want it to do is insert the new Person p and insert a record into the join table, PERSON_SKILL, that matches p with the skill in the SKILL table with id=2 without touching the SKILL table.
Is there anyway to achieve this behavior?
Instead of creating the Skill object yourself:
//Skill with id=2 is already in the skill table
Skill s = new Skill()
s.setSkillId(2L);
p.setSkills(new HashSet<Skill>(Arrays.asList(s)));
You should be retrieving it from the Hibernate Session:
Skill s = (Skill) session.get(Skill.class, 2L);
p.setSkills(new HashSet<Skill>(Arrays.asList(s)));
This way the Session thinks that the skill contained in p.skills is persistent, and not transient.
This may be possible if you don't cascade all-delete-orphan which is explicitely telling hibernate to cascade the changes.
But the right way would be IMO to load load the desired Skill entity from the database and to add it to the set of skills of the Person.

Legacy mapping with hibernate

For my current project I have to map a legacy database using hibernate, but I'm running into some problems.
The database is setup using one 'entity' table, which contains common properties for all domain objects. Properties include (among others) creation date, owner (user), and a primary key which is subsequently used in the tables for the domain objects.
A simple representation of the context is as such:
table entity
- int id
- varchar owner
table account
- int accountid (references entity.id)
table contact
- int contactid (references entity.id)
- int accountid (references account.accountid)
My problem exhibits itself when I try to add a collection mapping to my account mapping, containing all contacts belonging to the account. My attempts boil down to the following:
<hibernate-mapping>
<class name="Contact" table="entity">
<id name="id" column="id">
<generator class="native" />
</id>
<join table="contact">
<key column="contactid"/>
<!-- more stuff -->
</join>
</class>
</hibernate-mapping>
<hibernate-mapping>
<class name="Account" table="entity">
<id name="id" column="id">
<generator class="native" />
</id>
<bag name="contacts" table="contact">
<key column="accountid" />
<one-to-many class="Contact"/>
</bag>
<join table="account">
<key column="accountid"/>
<!-- more stuff -->
</join>
</class>
</hibernate-mapping>
However, when I try to fetch the account I get an SQL error, stating that the entity table does not contain a column called accountid. I see why this is happening: the mapping tries to find the accountid column in the entity table, when I want it to look in the contact table. Am I missing something obvious here, or should I approach this problem from another direction?
This looks to me like you actually need to be mapping an inheritance, using the Table Per Subclass paradigm.
Something like this:
<class name="entity" table="entity">
<id name="id" column="id">
...
</id>
<joined-subclass name="contact" table="contact">
<key column="contactid"/>
</joined-subclass>
<joined-subclass name="account" table="account">
<key column="accountid"/>
</joined-subclass>
</class>
That's approximate by the way - it's described in detail in section 9.1.2 of the Hibernate documentation (just in case you can't find it, it's called "Table per subclass").
Cheers
Rich

Categories