1

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!

Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206
  • 1
    I tried to reproduce your scenario, and everything is working correctly. All transient objects (A, B, C) are created, added into theirs collections... session.Udpate(A), session.Flush() ... 4 insert statements (tables A, B, C and B-C)... other words, with the mapping you are talking about it is working. Maybe show more code – Radim Köhler Aug 18 '13 at 15:46
  • @RadimKöhler Some detail: I'm talking about when you create `A` from scratch nor update it. In the other hand, I'm not updating explicitly: it's done inside a transaction. `A` is stored using `session.Save(...)`. Anyway, will you share that code? Can you paste on pastebin.com? Maybe I can arrive to some conclusion and figure out what's wrong in my code. Thanks! :D – Matías Fidemraizer Aug 18 '13 at 15:57
  • @RadimKöhler When I said *it's done inside a transaction* I mean that the `session.Flush` is implicitly done by committing the transaction. – Matías Fidemraizer Aug 18 '13 at 16:00
  • Sorry, it was a faster hand, then my thoughts. I mean session.Add() – Radim Köhler Aug 18 '13 at 16:08
  • I am using the xml mapping. would that be suitable for you? – Radim Köhler Aug 18 '13 at 16:09
  • @RadimKöhler Yeah, no problem, it's almost the same! Other option is just answering my question and you can post them the sources as code samples! – Matías Fidemraizer Aug 18 '13 at 16:13
  • 1
    I will show you what I have... give me few mintues ;) – Radim Köhler Aug 18 '13 at 16:14
  • @RadimKöhler Nice! Thank you in advance for your effort :) – Matías Fidemraizer Aug 18 '13 at 16:17

2 Answers2

1

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

Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206
0

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

Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Thanks again for your effort as I said in my question's comments. Hmmm, actually you reproduced the scenario very well, this is the exact case. By the way, give me some time so I can find differences and make some tries... Thanks for sharing the whole code!! :-) – Matías Fidemraizer Aug 18 '13 at 16:34
  • If that could help anyhow... great. NHibernate is a real diamond ;) – Radim Köhler Aug 18 '13 at 16:35
  • My case is basically your sample, but I can't understand why the m:n relation isn't persisted. I've checked that once I call `Session.Save` on the `A` entity equivalent, collections are turned into `PersistentSet`. I mean, NHibernate recognized them as persistent, thus, the relation should be persisted... !! – Matías Fidemraizer Aug 18 '13 at 19:27
  • Just to be sure, that the issue is not elsewhere, if you call session.Save(C) just after session.Save(A), would it be persisted? I guess that mapping in your case is ok, but the problem could be hidden somewhere else. E.g. the C unsaved-value="0"... – Radim Köhler Aug 19 '13 at 05:13
  • Well, in fact I'm using Guid.Comb and the unsaved value is the Guid.Empy. I'll try what you said `Session.Save(C)` and let's see what happens.. – Matías Fidemraizer Aug 19 '13 at 05:40
  • Great, but even if this won't help... We know that this scenario is working (well I am saying it, and my local sample shows that to me). So the issue should not be the cascading setting, but something, which makes NHibernate to treat the C as NOT transient, already persisted. Another thing is, that I played with INT not GUID... – Radim Köhler Aug 19 '13 at 05:45
  • Another strange fact: the SQL profiler shows that, just before the inserts are executed, NHibernate tries to check with a SELECT query if one of the tags are in the `B_C` table! It seems that the mapping works partially......... – Matías Fidemraizer Aug 19 '13 at 05:51
  • `C` is always persisted. The problem is the m:n relationship. I mean, `C` instances are correctly inserted into the DB, but they're not associated with `B` and viceversa. – Matías Fidemraizer Aug 19 '13 at 05:54
  • I see... now. So the C is persisted. I thought that even the C is not persisted... – Radim Köhler Aug 19 '13 at 05:57
  • Yeah. Now I understand something. The whole `SELECT` is executed if `C` is present in the database, so NHib tries to fetch the m:n relationship on `C` with no luck, because it did store nothing before... – Matías Fidemraizer Aug 19 '13 at 05:57
  • Yeah, that's it, `C` was always persisted. The problem is the m:n relationship :( – Matías Fidemraizer Aug 19 '13 at 05:58
  • At this moment, I have to say I am sorry, I have no more ideas ;( All was said already above. sorry... really – Radim Köhler Aug 19 '13 at 06:00
  • I suspect that your code and mine's difference is `C` isn't created with `new` directly. In the real scenario, `C` is a more complex entity and it must be created using a factory, which internally calls a `Save` for `C`. Maybe this 2 `Save` calls could be the problem? – Matías Fidemraizer Aug 19 '13 at 06:01
  • I understand that, I'm just like you, trying to figure out the underlying problem... – Matías Fidemraizer Aug 19 '13 at 06:03
  • You are making it more and more interesting ;) I totally understand that your scenario is more complicated. Maybe something like a partial Flush after C creation, session.Clear() and reload of the C instance ... could be next try. (Inside one transaction, no commit during that process) – Radim Köhler Aug 19 '13 at 06:03
  • No luck with my try (not calling the underlying `Save` on `C`). You're right, I'm in a some complex use case. Sadly, I'll need to continue these tries tonight as this is a personal project I'm developing at home. Now's time to go to my work. Anyway, I'm going to keep you in touch with my advancements and possible solution!!! – Matías Fidemraizer Aug 19 '13 at 06:33
  • I could solve the problem!!!!!!!!!!!! Check my own answer on this question. If you're agree, I'm going to mark my own answer as the right one as it really solves the problem, but don't get me wrong: I want to credit your effort and it helped me a lot in order to, at least, validate that it wasn't a mapping problem. Thank you man! – Matías Fidemraizer Aug 19 '13 at 17:21