Map does not delete keys if value of the key is null

Description

Name and version of the database you are using:
Oracle 10g

I will try to create & submit a test case for this, but I already have included some test code below.

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.

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.

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

namespace NHibernateDemo.DomainModel
{
public class Major
{
private string _note;

public string Note
{
get
{
return _note;
}
set
{
_note = value;
}
}
}
}

Student mapping.
<?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.

<?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.

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

Environment

None

Attachments

1
  • 13 Oct 2008, 11:40 PM

Activity

Show:

Fabio MauloMay 14, 2009 at 9:37 PM

Thanks to Stefan, who understand the spirit of a NH's user.

Fabio MauloMay 14, 2009 at 9:35 PM

The test pass in trunk so, if it was real, it was fixed in some moment in the past.

Stefan SteineggerOctober 13, 2008 at 11:40 PM

I can't reproduce it, everything works fine for me (Sql Server 2005). I suppose that it does not work for you because you didn't commit the transaction.

I attached the working unit test. You can try to reproduce it in your environment.

Martijn BoekerJuly 15, 2008 at 3:39 PM

If anybody wants to create a unit test for this issue, please go ahead. I'm having some issues with my development environment, so I cannot submit a test case at this time. Sorry about that.

Martijn

Fixed

Details

Assignee

Reporter

Components

Fix versions

Affects versions

Priority

Who's Looking?

Open Who's Looking?
Created July 15, 2008 at 2:23 PM
Updated May 14, 2009 at 9:37 PM
Resolved May 14, 2009 at 9:35 PM
Who's Looking?