Many-to-many association not stored in NHibernate

386 views Asked by At

I'll try to explain the problem precisely.

There's an entity A which has a one-to-many relation with B (a collection).

In addition, entity B has a many-to-many relation with an entity C.

When I first store a transient object of type A, both A and related transient B entity instances associated with A are correctly created in the database, but transient C instances associated with B aren't created.

The many-to-many association is mapped in both sides of the association as follows (mapping-by-code approach):

// Mapping for collection of Entity C on Entity B
@class.Set
(
    entityB => entityB.EntityCList,
    map =>
    {
        map.Table("EntitiesBInEntitiesC");
        map.Cascade(Cascade.All);
        map.Key(key => key.Column("EntityBId"));
        map.Inverse(false);
    },
    map => map.ManyToMany(relMap => relMap.Column("EntityCId"))
);


// Mapping for collection of Entity B on Entity C
@class.Set
(
    entityB => entityB.EntityBList,
    map =>
    {
        map.Table("EntitiesBInEntitiesC");
        map.Cascade(Cascade.All);
        map.Key(key => key.Column("EntityCId"));
        map.Inverse(true);
    },
    map => map.ManyToMany(relMap => relMap.Column("EntityBId"))
);

Also, when entity B and entity C is instantiated when they're both transient objects yet, I add the one of entity B on the collection of entity C and viceversa.

Finally, the transaction ends successfully: I mean that the entity A and B, and their association are stored in the database as expected.

Why the entity C associations aren't persisted in the database? What am I doing wrong?

  • NOTE: I'm using the latest version of NHibernate 3.x series.

  • NOTE 2: I've just profiled the SQL Server database and the inserts into the m:n table are never executed!

2

There are 2 answers

1
Matías Fidemraizer On BEST ANSWER

I'd like to answer my own question in order to share how easy was solving the problem but how hard took understanding what originated the problem.

Because I was going to add some kind of validation logic, I was adding a flush entity event listener:

configuration.SetListener(ListenerType.FlushEntity, this);

Since the model mapping was right, both Radim Köhler (the other answerer) and me were trying to figure out why the many-to-many relation wasn't being stored into the database.

Commenting the whole listener binding solved the problem.

Well, the problem isn't the whole listener binding but not calling the default flush implementation, or yeah, just not adding the whole listener if it's not really needed (my case, I was just trying some kind of interception that I'm not needing anymore!).

Either you need to provide how entity flushes or use the default one, but leaving it blank (empty event listener) will prevent some entities to correctly flush:

    // WRONG!
    public void OnFlushEntity(FlushEntityEvent @event)
    {
    }

Hopefully this answer will help others to solve similar problems...

17
Radim Köhler On

I've tried to reproduce your scenario like this: a Client (object A) having more Agreemnts (object B) referencing many Debtors (object C). I will use <bag> and IList<> but the same works for <set>

public class Client
{
    public virtual int ID { get; set; }
    public virtual IList<Agreement> Agreements { get; set; }
    ...
}

public class Agreement
{
    public virtual int ID { get; set; }
    public virtual Client Client { get; set; }
    public virtual IList<Debtor> Debtors { get; set; }
    ...
}

public class Debtor
{
    public virtual int ID { get; set; }
    public virtual IList<Agreement> Agreements { get; set; }
    ...
}

Now the simplified class mapping, but complete <bag> mapping

Client:

<class name="Client" table="[dbo].[Client]" lazy="true" >
    <id name="ID" column="[ClientId]" generator="native" />
    ...

    <bag name="Agreements" inverse="true" cascade="all" >
      <key column="AgreementId"></key>
      <one-to-many class="Agreement"/>
    </bag>

Agreement:

<class name="Agreement" table="[dbo].[Agreement]" lazy="true" >
  <id name="ID" column="[AgreementId]" generator="native" />

  <many-to-one name="Client" column="ClientId" />

  <bag name="Debtors"
    table="[dbo].[AgreementDebtor]" 
    inverse="false" cascade="all" >
    <key column="AgreementId"></key>
    <many-to-many class="Debtor" column="DebtorId" />
  </bag>

Debtor:

<class name="Debtor" table="[dbo].[Debtor]" lazy="true" >
  <id name="ID" column="[DebtorId]" generator="native" />

  <bag name="Agreements"
    table="[dbo].[AgreementDebtor]"
    inverse="true" cascade="all" >
    <key column="DebtorId"></key>
    <many-to-many class="Agreement" column="AgreementId" />
  </bag>

Having this mapping, we can call this:

var client = new Client();
var debtor = new Debtor();
var agreement = new Agreement();

agreement.Client = client;
client.Agreements.Add(agreement);

agreement.Debtors.Add(debtor);
debtor.Agreements.Add(agreement);

session.Save(client);
session.Flush();

the Inserts issued and cought by profiler:

RPC:Completed  exec sp_executesql N'INSERT INTO [dbo].[Client] ...
RPC:Completed  exec sp_executesql N'INSERT INTO [dbo].[Agreement] ...
RPC:Completed  exec sp_executesql N'INSERT INTO [dbo].[Debtor] ...
RPC:Completed  exec sp_executesql N'INSERT INTO [dbo].[AgreementDebtor] (AgreementId, DebtorId) VALUES ...

This way, with only one call to "Insert" the Client instance... all other sutff is triggered in a cascade, it should work