How to cascade Save with CompositeId in NHibernate?

189 views
Skip to first unread message

Mindaugas Pielikis

unread,
Sep 14, 2009, 9:53:07 AM9/14/09
to nhusers
Hi,

I've asked this question in stackoverflow (http://stackoverflow.com/
questions/1420498/how-to-cascade-save-with-compositeid-in-nhibernate),
but didn't get the right answer.

In short the question is. Is it possible to cascade Save operation for
referenced entities which are referenced by composite-id?

THE DB:

A(id, Name)
B(id, Name)
AB(AId, BId) references A and B

CLASSes:

public class A
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<AB> LinkToB { get; private set; }
}

public class B
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}

public class AB
{
public virtual A A { get; set; }
public virtual B B { get; set; }
public override bool Equals(object obj) { /* routine */ }
public override int GetHashCode() { /* routine */ }
}

THE TEST:

var a = new A { Name = "a1" };
var b = new B { Name = "b1" };
a.LinkToB.Add(new AB { A = a, B = b });
//session.SaveOrUpdate(b); < I don't like this call :)
session.SaveOrUpdate(a); // I want to persist all three objects using
one call. Is it possible?

MAPPINGS:

A hbm:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-
access="property" auto-import="true" default-cascade="none" default-
lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" name="NHibernateTest.A,
NHibernateTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
table="`A`">
<id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="Id" />
<generator class="identity" />
</id>
<property name="Name" type="System.String, mscorlib,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="Name" />
</property>
<bag cascade="all" inverse="true" name="LinkToB">
<key>
<column name="AId" />
</key>
<one-to-many class="NHibernateTest.AB, NHibernateTest,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bag>
</class>
</hibernate-mapping>

B hbm:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-
access="property" auto-import="true" default-cascade="none" default-
lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" name="NHibernateTest.B,
NHibernateTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
table="`B`">
<id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="Id" />
<generator class="identity" />
</id>
<property name="Name" type="System.String, mscorlib,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="Name" />
</property>
</class>
</hibernate-mapping>

AB hbm:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-
access="property" auto-import="true" default-cascade="none" default-
lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" name="NHibernateTest.AB,
NHibernateTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
table="`AB`">
<composite-id mapped="false" unsaved-value="undefined">
<key-many-to-one name="A" class="NHibernateTest.A,
NHibernateTest, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null">
<column name="AId" />
</key-many-to-one>
<key-many-to-one name="B" class="NHibernateTest.B,
NHibernateTest, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null">
<column name="BId" />
</key-many-to-one>
</composite-id>
</class>
</hibernate-mapping>

Thank you!

Anne Epstein

unread,
Sep 14, 2009, 10:32:42 PM9/14/09
to nhu...@googlegroups.com
Wow. uh, you have a lot of extraneous stuff in those mappings. 

Anyway, cascades are kind of like the SaveOrUpdate command-i.e. NHibernate decides what to do.  But in order to decide what to do, NHibernate needs info about whether this is a new or existing entity, which normally happens though the id.  When you have an entity in which Nibernate is in control of the id (you don't set it), NHibernate can track this fine-it does the work for you.  When you've got an enity with a composite id (or more generally an assigned id) NHibernate isn't in charge of the id-you are. So, since NHibernate can't use its usual methods to know if an entity is new, if you want NHibernate to do this kind of thing for you, you need to give it another way.

Enter: the Version column http://www.nhforge.org/doc/nh/en/index.html#mapping-declaration-version -this is NOT a column for you, it's for NHibernate. Don't use this column in your class or in your business logic. Add the column to your table, to your mapping, and poof, cascading, as well as SaveOrUpdate should work.

Hope that helps.

Mindaugas Pielikis

unread,
Sep 15, 2009, 3:46:18 AM9/15/09
to nhusers
thanks for reply!

> Wow. uh, you have a lot of extraneous stuff in those mappings.

sorry for these massive mapping files, they were generated by
Fluent :)

> Anyway, cascades are kind of like the SaveOrUpdate command-i.e. NHibernate
> decides what to do. But in order to decide what to do, NHibernate needs
> info about whether this is a new or existing entity, which normally happens
> though the id.

But in my case there is no problem to persist entity with
composite-id, it persists (almost) without any problem. The problem is
that referenced type, which is part of the id, is not cascading. The
referenced type is usual entity with usual id. I don't really
understand why NHibernate couldn't look at the referenced entity and
figure
out if it is transient or not. It looks very natural and shouldn't be
that hard to implement IMHO... Or am I missing something obvious? :)

bye,
Mindaugas

Anne Epstein

unread,
Sep 15, 2009, 11:13:47 AM9/15/09
to nhu...@googlegroups.com
" I don't really understand why NHibernate couldn't look at the referenced entity and figure out if it is transient or not."

As I said, try adding a <version> to the AB class.  I can certainly understand that it seems like you shouldn't *have* to do that, but without this, NHibernate as currently implemented (unless this has changed in 2.1 and I missed it) CAN'T determine by itself if an assigned/composite keyed entity is transient. (If you've heard that assigned or composite keys or BAD with NH, this is one reason... not that you can't use them, but they are less ... convenient) You won't have to change any of your actual code or classes or manage any detection of transience yourself, just add a column to your table and add the version element to your mappings. Note you will have to put some value in the column <version> uses for any existing records-put the same exact date in every single existing record if you like... but just don't leave it null or you'll get some errors.

Mindaugas Pielikis

unread,
Sep 15, 2009, 5:35:24 PM9/15/09
to nhusers
> As I said, try adding a <version> to the AB class.  I can certainly
> understand that it seems like you shouldn't *have* to do that, but without
> this, NHibernate as currently implemented (unless this has changed in 2.1
> and I missed it) CAN'T determine by itself if an assigned/composite keyed
> entity is transient.

Thanks again for the tips Anne. I added the version column and added
the mapping (I am using 2.1.0.4000 version of NHibernate)

<version name="Version" type="timestamp">
<column name="Ver" />
</version>

but it didn't solve my problem. I still get persistence exception. I
googled for information about version column and found that it was
used mostly for optimistic locking operation and didn't find nothing
about relation of version column to assigned id's or cascades... Maybe
you could direct me to the right source?

However I found the suitable solution/workaround. I simply removed
composite-id mapping and replaced it with two many-to-one mappings
respectively and added cascading there. Also I added the Id column to
the AB table and mapped it on the AB class new Id property. So now
it's working almost as I wanted it to.

Do you know maybe a better way to map many-to-many relation using
intermediate link class?

bye,
Mindaugas

Anne Epstein

unread,
Sep 15, 2009, 11:57:21 PM9/15/09
to nhu...@googlegroups.com
Re: Version, take a look at this:
http://devlicio.us/blogs/mike_nichols/archive/2008/07/29/when-flushing-goes-bad-assigned-ids-in-nhibernate.aspx this is about assigned ids, but composite ids are a special case of assigned ids, and have all the assigned id issues, plus some special issues of their own.

however, I think you did the right thing:
Composite ids are a hassle in NH, and I would not recommend going with them for a new table. There's just a lot more you have to watch out for, a lot more that can go wrong. I think the way you're doing it is fine.

One thing you may not know about is that if your intermediate table is a pure link table (which appears to be the case with what you have here), you don't have to map it as a separate class-you can put a many-to-many bag or set on your A class and your B class, and no AB class needed, which you might find a bit cleaner (though the AB table would still be there, behind the scenes).  However, if you need other things in the link table, then you'll need to keep your AB class.
see: http://www.nhforge.org/doc/nh/en/index.html#collections-ofvalues
In particular, look at http://www.nhforge.org/doc/nh/en/index.html#collections-idbag - I think this mapping could fit your new table layout well, and be nice and efficient as well.
Reply all
Reply to author
Forward
0 new messages