Hibernate version:
NHibernate - Build 1.2.0.GA
Can reproduce with builds 1.2.1.GA and 2.0.0.Beta2
Using .Net 2.0
Name and version of the database you are using:
Oracle 10g
Intro
First of all, thanks & cudos to all NHibernate developers for creating such an awesome library. It’s so much nicer to use an ORM then to hand-code SQL stored procedures and writing UI code against data sets. We have successfully mapped 100+ classes in our application with only a few issues, such as this one. Myself as well as my fellow team members did some searches online to find an answer, but without success. Even though our project is completed, I want to get to the bottom of it so we have better knowledge in future projects. I debugged NHibernate to find the cause of the issue and found some possible problems. I also suggest 2 solutions even though I’m not sure if they are valid and how to implement them correctly. If this issue needs to be posted in your issue tracking system, I would gladly do so.
Cheers,
Martijn
Problem Description
Steps to reproduce:
- Create a class with a mapping that contains the “map” collection type
- The “map” has its values defined as a composite-element
- The composite-element has properties
- The properties of the composite-element may be null
- Open a session. Add an entry to the “map” and persist the container object to the database. Close the session.
- Open a session. Add a second entry to the “map” of which the properties are null. Persist the container object to the database. Close the session.
- Start a session. Remove the second entry from the “map”. Persist the container object to the database. Close the session.
Expected result:
- The second entry has been removed from the database.
Actual result:
- The second entry has not been removed from the database.
Analysis
- The composite element is a component type object
- A component type object is not loaded if its properties are null (see Hibernate.ComponentType – Hydrate() method)
- Entries in the map of which the composite element is null will have a value of null. In other words, the key exists but the value is null.
- NHibernate’s map implementation, PersistentGenericMap, does not add entries to the list of objects to be deleted when the value is null. See GetDeletes() which contains the following check: if (e.Value != null && !map.ContainsKey(key))
- When removing all entries in the map, the entries are correctly deleted. This is because NHibernate follows a different code path when a collection has become empty.
I see 2 possible solutions.
Solution #1: Make sure components are always loaded for map entries.
I have tried this by changing the last line of the Hibernate.ComponentType – Hydrate() method from “return notNull ? values : null;” to “return values;”. This resolved my problem, but I noticed that I had to change my Unit test to never expect a null value. Similarly, this change may break existing applications built with NHibernate. Obviously, more care should be taken in implementing this solution.
Solution #2: Change PersistentGenericMap - GetDeletes() to add entries to the list of object to be deleted, even when the value is null.
I tried this by changing the check in GetDeletes() from “if (e.Value != null && !map.ContainsKey(key))” to “if (!map.ContainsKey(key))”. This resolved my problem but it may break existing code, especially since the value is used when the indexIsFormula parameter is true. However, I do not understand the use of the indexIsFormula parameter at this moment.
Example
To investigate this issue, I created a simple project with a Student class, a Major class and a Subject class. The Major class joins Student and Subject: in the database, Major is mapped to a table called MAJOR with foreign keys STUDENT_ID and SUBJECT_ID, which form its composite primary key. There is also an additional NOTE column in the MAJOR table which is mapped a the Note field. In the Student’s mapping file, the Majors relationship is mapped using the map collection type.
Student class.
Code:
using System.Collections.Generic;
namespace NHibernateDemo.DomainModel
{
public class Student
{
private int _id;
private IDictionary<Subject, Major> _majors = new Dictionary<Subject, Major>();
public virtual int Id
{
get
{
return _id;
}
}
public virtual IDictionary<Subject, Major> Majors
{
get
{
return _majors;
}
}
}
}
Subject class.
Code:
using System.Collections.Generic;
namespace NHibernateDemo.DomainModel
{
public class Subject
{
private int _id;
private string _title;
public int Id
{
get
{
return _id;
}
}
public string Title
{
get
{
return _title;
}
set
{
_title = value;
}
}
}
}
Major class
Code:
namespace NHibernateDemo.DomainModel
{
public class Major
{
private string _note;
public string Note
{
get
{
return _note;
}
set
{
_note = value;
}
}
}
}
Student mapping.
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="true" default-cascade="save-update">
<class name="NHibernateDemo.DomainModel.Student, NHibernateDemo" table="STUDENT">
<id name="Id" column="STUDENT_ID" type="int" access="nosetter.camelcase-underscore">
<generator class="sequence" >
<param name="sequence">STUDENT_ID_SEQUENCE</param>
</generator>
</id>
<map name="Majors"
cascade="all"
inverse="false"
access="field.camelcase-underscore"
fetch="select"
lazy="true"
batch-size="100"
table="MAJOR">
<key column="STUDENT_ID" foreign-key="STUDENT_ID" />
<index-many-to-many column="SUBJECT_ID" class="NHibernateDemo.DomainModel.Subject, NHibernateDemo" />
<composite-element class="NHibernateDemo.DomainModel.Major, NHibernateDemo">
<property name="Note" column="NOTE" />
</composite-element>
</map>
</class>
</hibernate-mapping>
Subject mapping.
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false" default-cascade="none">
<class name="NHibernateDemo.DomainModel.Subject, NHibernateDemo" table="SUBJECT">
<id name="Id" column="SUBJECT_ID" type="int" access="nosetter.camelcase-underscore">
<generator class="assigned" />
</id>
<property name="Title" column="TITLE" />
</class>
</hibernate-mapping>
Test code -- requires NUnit.
Code:
using System;
using NHibernateDemo.DomainModel;
using NHibernate;
using NHibernate.Cfg;
using NUnit.Framework;
namespace NHibernateDemo
{
[TestFixture]
public class Tests
{
private ISessionFactory _sessionFactory;
[TestFixtureSetUp]
public void SetUp()
{
// Load hibernate.cfg.xml.
Configuration configuration = new Configuration();
configuration.Configure("hibernate.cfg.xml");
// Create session factory.
_sessionFactory = configuration.BuildSessionFactory();
}
[Test]
public void RemoveMajor()
{
int studentId = 3;
// Set major.
{
ISession session = _sessionFactory.OpenSession();
Student student = session.Get<Student>(studentId);
Subject subject1 = session.Get<Subject>(1);
Subject subject2 = session.Get<Subject>(2);
// Create major objects.
Major major1 = new Major();
major1.Note = "";
Major major2 = new Major();
major2.Note = "";
// Set major objects.
student.Majors[subject1] = major1;
student.Majors[subject2] = major2;
session.Flush();
session.Close();
}
// Remove major for subject 2.
{
ISession session = _sessionFactory.OpenSession();
Student student = session.Get<Student>(studentId);
Subject subject2 = session.Get<Subject>(2);
// Remove major.
student.Majors.Remove(subject2);
session.Flush();
session.Close();
}
// Get major for subject 2.
{
ISession session = _sessionFactory.OpenSession();
Student student = session.Get<Student>(studentId);
Subject subject2 = session.Get<Subject>(2);
// Major for subject 2 should have been removed.
try
{
Assert.IsFalse(student.Majors.ContainsKey(subject2));
}
catch(AssertionException exc)
{
Console.WriteLine("***** Major for subject 2 should have been removed.");
}
session.Close();
}
// Remove all - NHibernate will now succeed in removing all.
{
ISession session = _sessionFactory.OpenSession();
Student student = session.Get<Student>(studentId);
student.Majors.Clear();
session.Flush();
session.Close();
}
}
}
}
Work arounds
Our work around has been to map many-to-many relationships that have additional attributes as as a one-to-many bag. The object contained in the bag have their own, sequence generated ID, even though this is not necessary from a database point of view. In our database, we still create a composite primary key but added an additional column to contain the sequence generated ID.
For many-to-many relationships that have no additional attributes, we used a many-to-many bag. This is an OK solution, because there is no need to add an additional column in the database. However, it does add the need to change the container class’s interface to simplify accessing data by keys, which would have been free in case of a dictionary-based mapping.