Map does not delete keys if value of the key is null
Description
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
Details
Assignee
Unassigned
UnassignedReporter
Martijn Boeker
Martijn BoekerComponents
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?
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();
}
}
}
}