views:

2085

answers:

8

Hello,

I'm currently using NHibernate as my data access layer, using Fluent NHibernate to create the mapping files for me. I have two classes, TripItem and TripItemAttributeValue, which have a many-to-many relation between them.

The mapping is as follows:

public class TripItemMap : ClassMap<TripItem2>
{
    public TripItemMap()
    {
        WithTable("TripItemsInt");
        NotLazyLoaded();

        Id(x => x.ID).GeneratedBy.Identity().WithUnsavedValue(0);
        Map(x => x.CreateDate, "CreatedOn").CanNotBeNull();
        Map(x => x.ModifyDate, "LastModified").CanNotBeNull();

        /* snip */

        HasManyToMany<TripItemAttributeValue>(x => x.Attributes).AsBag()
            .WithTableName("TripItems_TripItemAttributeValues_Link")
            .WithParentKeyColumn("TripItemId")
            .WithChildKeyColumn("TripItemAttributeValueId")
            .LazyLoad();
    }
}

public class TripItemAttributeValueMap : ClassMap<TripItemAttributeValue>
{
    public TripItemAttributeValueMap()
    {
        WithTable("TripItemAttributeValues");

        Id(x => x.Id).GeneratedBy.Identity();
        Map(x => x.Name).CanNotBeNull();

        HasManyToMany<TripItem2>(x => x.TripItems).AsBag()
            .WithTableName("TripItems_TripItemAttributeValues_Link")
            .WithParentKeyColumn("TripItemAttributeValueId")
            .WithChildKeyColumn("TripItemId")
            .LazyLoad();
    }
}

At some point in my application I fetch existing attributes from the database, add them to tripItem.Attributes, then save the tripItem object. In the end, the TripItems_TripItemAttributeValues_Link never gets any new records, resulting in the relations not being persisted.

If it helps, these are the mapping files generated by Fluent NHibernate for these classes:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="true" assembly="ETP.Core" namespace="ETP.Core.Domain">
  <class name="TripItem2" table="TripItemsInt" xmlns="urn:nhibernate-mapping-2.2" lazy="false">
    <id name="ID" column="ID" type="Int32" unsaved-value="0">
      <generator class="identity" />
    </id>
    <property name="CreateDate" column="CreatedOn" type="DateTime" not-null="true">
      <column name="CreatedOn" />
    </property>
    <property name="ModifyDate" column="LastModified" type="DateTime" not-null="true">
      <column name="LastModified" />
    </property>
    <bag name="Attributes" lazy="true" table="TripItems_TripItemAttributeValues_Link">
      <key column="TripItemId" />
      <many-to-many column="TripItemAttributeValueId" class="ETP.Core.Domain.TripItemAttributeValue, ETP.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </bag>
  </class>
</hibernate-mapping>

and

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="true" assembly="ETP.Core" namespace="ETP.Core.Domain">
  <class name="TripItemAttributeValue" table="TripItemAttributeValues" xmlns="urn:nhibernate-mapping-2.2">
    <id name="Id" column="Id" type="Int32">
      <generator class="identity" />
    </id>
    <property name="Name" column="Name" length="100" type="String" not-null="true">
      <column name="Name" />
    </property>
    <bag name="TripItems" lazy="true" table="TripItems_TripItemAttributeValues_Link">
      <key column="TripItemAttributeValueId" />
      <many-to-many column="TripItemId" class="ETP.Core.Domain.TripItem2, ETP.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </bag>
  </class>
</hibernate-mapping>

What am I doing wrong here ?

+2  A: 

I'm not sure how you do it with Fluent NHibernate but you need to set the Cascade option on the bag (TripItems). As usual, Ayende's got a useful post about cascade options.

From a quick Google, I'd suggest you try:

HasManyToMany<TripItem2>(x => x.TripItems).AsBag()
        .WithTableName("TripItems_TripItemAttributeValues_Link")
        .WithParentKeyColumn("TripItemAttributeValueId")
        .WithChildKeyColumn("TripItemId")
        .LazyLoad()
/*-->*/ .Cascade.All(); /*<-- this is the bit that should make it work */
David Kemp
+1  A: 

David Kemp has it right: you want to add a cascade to your bag.

I've always hand-edited (and hand-created) the mapping files, so my natural inclination is to put it there. You can do that as follows:

<bag name="TripItems" lazy="true" table="TripItems_TripItemAttributeValues_Link" cascade="all">
  <key column="TripItemAttributeValueId" />
  <many-to-many column="TripItemId" class="ETP.Core.Domain.TripItem2, ETP.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bag>

I've found that keeping my classes 'pure' and keeping everything to do with nHibernate in the .hbm.xml file keeps my solution cleaner. That way, if there's a new ORM software I want to use, I just replace the mapping files, and don't rewrite the classes. We use our Unit tests to test the classes and give testability to the xml, though I do kind of like Fluent NHibernate's methods.

Jeff
A: 

I'm afraid Cascade.All() is not really the solution to my problem - it was one of the things I tried. The problem is not that the items added to the collection are not saved -- they are already in the database at the time they're being added to the collection. It's just that the entries in the link table are not being created. Furthermore I think Cascade.All() would also cause child items to be deleted, which is not desirable behavior in my scenario. I've tried using Cascade.SaveUpdate(), but as I've pointed out, this solves something that is not really my problem :-)

However, to make sure, I will retry this solution and let you know the outcome.

As for keeping the classes pure, this is 100% the case with Fluent NHibernate. The class mappings you create are C# code files that go alongside your entity classes, pretty much like .hbm.xml files would.

efdee
+2  A: 

@efdee

I was having the same problem and spent almost two days on this. I had a many to many relationship and the link table wasn't being updated either. I'm new to NHibernate, just trying to learn it so take everything I say with a grain of salt.

Well it turned out that it's not Fluent NHibernate, nor the mapping, but me not understanding how NHibernate works with many-to-many. In a many-to-many relationship if collections on both entities aren't populated, NHibernate doesn't persist data to the link table.

Let's say I have this entities in a many-to-many relationship :


partial class Contact
{
   public string ContactName {get; set;}
   public IList Locations {get; set;}

}

partial class Location
{
   public string LocationName {get; set;}
   public string LocationAddress {get;set;}
   public IList Contacts {get;set;}
}

when I add to a location to Contact.Locations, I have to make sure that the contact is also present inside location.Contacts.

so to add a location i have this method inside my Contact class.


public void AddLocation(Location location)
        {
            if (!location.Contacts.Contains(this))
            {
                location.Contacts.Add(this);
            }
            Locations.Add(location);
        }

This seems to have solved my problem, but like I said I'm just picking up NHibernate and learning it, may be there's a better way. If anyone has a better solution, please post.

This is the post that pointed me to check both collections: http://www.coderanch.com/t/217138/Object-Relational-Mapping/link-table-of-ManyToMany-annotation

Emmanuel
+1  A: 

I'm having exactly the same problem but I've been using the NHibernate.JetDriver. I tried using the recommended answer without any success. Does anyone know if the NHibernate.JetDriver has a limitation with respect to many-to-many?

Here are my hbm files in case someone happens to be interested in reviewing them for a moment:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="Ace.Docs.Core.Domain" assembly="Ace.Docs.Core">
<class name="Ace.Docs.Core.Domain.Address, Ace.Docs.Core" table="Addresses" lazy="true">
 <id name="Id" column="ID">
   <generator class="identity" />
 </id>
 <property name="Address1" column="Address1" />
 <property name="Address2" column="Address2" />
 <property name="City" column="City" />
 <property name="EmailAddress" column="EmailAddress" />
 <property name="Phone1" column="Phone1" />
 <property name="Phone2" column="Phone2" />
 <property name="PostalCode" column="PostalCode" />
 <property name="StateOrProvince" column="StateOrProvince" />
 <many-to-one name="AddressTypeMember" column="AddressTypeID" class="AddressType" />
 <bag name="HasPersonalInfo" table="Link_PersonalInfo_Addresses" lazy="true" cascade="save-update" inverse="true" >
  <key column="AddressID"></key>
  <many-to-many column="PersonalInfoID" class="PersonalInfo" />
 </bag>
</class>

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="Ace.Docs.Core.Domain" assembly="Ace.Docs.Core">
<class name="Ace.Docs.Core.Domain.PersonalInfo, Ace.Docs.Core" table="PersonalInfo" lazy="true">
 <id name="Id" column="ID">
   <generator class="identity" />
 </id>
 <property name="Prefix" column="Prefix" />
 <property name="FirstName" column="FirstName" />
 <property name="MiddleName" column="MiddleName" />
 <property name="LastName" column="LastName" />
 <property name="SIN" column="SIN" />
 <property name="Birthdate" column="Birthdate" />
 <property name="Note" column="Notes" />
 <bag name="HasAddress" table="Link_PersonalInfo_Addresses" lazy="true" cascade="save-update" inverse="true" >
  <key column="PersonalInfoID"></key>
  <many-to-many column="AddressID" class="Address" />
    </bag>
</class>

Daver
+1  A: 

I've got it and I hope this helps someone else out there. The issue is that I had inverse='true' on both bags. If you read the excerpt below you'll note that there needs to be inverse set to true on only one of the bags:

Note the use of inverse="true". Once again, this setting tells NHibernate to ignore changes made to the categories collection and use the other end of the association - the items collection - as the representation that should be synchronized with the database.

Daver
A: 

I was struggling with this as well, and came by a completely different reason for my troubles. In my example if I had an object without any many-to-many relationships I could just call saveOrUpdate, and all would be good. But if I had any many-to-many relationships, I had to make sure that my saveOrUpdate call was within a BeginTransaction and CommitTransaction. I am very new to fluent Nhibernate, so apologize if this is obvious. But it wasn't to me.

+1  A: 

Call Session.Flush() or use transaction.

Vladimir Kotov
Thanks!I had the same issue. I was calling Session.SaveOrUpdate(entity) for one side of the relationship. But this wasn't creating the link table. Calling Session.Flush() solved this.
Nic Strong
This worked for me too...how annoying!
Paul