-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 
Author Message
 Post subject: Managing lazy one-to-many from constructor
PostPosted: Wed Jan 11, 2006 2:21 pm 
Beginner
Beginner

Joined: Mon Dec 19, 2005 4:13 am
Posts: 27
Location: Prague, Czech Republic
Hi all,

I have the problem with parent-child one-to-many asociation. My test classes are following:

Person (m_Pets 1-*> Pet, m_Name)
Pet (m_Owner *-1> Person, m_Name)

Constructor of Pet id following:

Code:
Pet(Person owner, string name) : base() {
  m_Name = name;
  m_Owner = owner;
  m_Owner.Pets.Add(this);
}


Now, I have following strange behavior please read the code and the output:

Code:
Person p = pf.FindById(12); // "pf" is some factory class, "p" already has 2 pets
Debug.WriteLine(p.Pets.Count); // output is "2"
Pet x = new Pet(p, "Azor");
Debug.WriteLine(p.Pets.Count); // output is "3"
xf.FindAll(); // "xf" finds all instances of "Pet" using query by critera, automatic flush occure before
Debug.WriteLine(p.Pets.Count); // output is "3"


It works fine. The second try, using explicit Save method on ISession (rollback before):

Code:
Person p = pf.FindById(12); // "pf" is some factory class, "p" already has 2 pets
Debug.WriteLine(p.Pets.Count); // output is "2"
Pet x = new Pet(p, "Azor");
Debug.WriteLine(p.Pets.Count); // output is "3"
currentSession.Save(x); // explicitly saving new Pet
Debug.WriteLine(p.Pets.Count); // output is "3"


It is again fine, until I remove first and second Debug.WriteLine (rollback before):

Code:
Person p = pf.FindById(12); // "pf" is some factory class, "p" already has 2 pets
Pet x = new Pet(p, "Azor");
currentSession.Save(x); // explicitly saving new Pet
Debug.WriteLine(p.Pets.Count); // output is "4"


Now, you can see the wrong number of pets in the person collection. "Azor" is twice here. In database, everything is OK. I'll try to smumarize my problem:

When I manage many-to-one collection in the constructor of "one" side by adding "this" to collection on "many" side, and only when this collection is lazy and it was not loaded from database before, and then explicitly save new "one" side using the "Session.Save", in the collection of many side I then have the new item twice. The collection is mapped as bag and inverse. If it is mapped with cascade "save-update" and the new item is flushed automatically to database, everything is fine.

Do someone know where can be the problem? I can send more sources if someone can examine it.
Many thanks! David[/b]


Top
 Profile  
 
 Post subject: Is it a bug? More explanation with the code
PostPosted: Thu Jan 12, 2006 7:20 am 
Beginner
Beginner

Joined: Mon Dec 19, 2005 4:13 am
Posts: 27
Location: Prague, Czech Republic
Ok,

I spent more hours with the problem and now I am quite sure it is a bug, or I am doing something totaly wrong. I realized that problem is not when managing asociation in constructor, but from anywhere. The problem occure after adding new child into parent, if the bidirectional many-to-one association is lazy and was not loaded from the database before saving the child. I im posting some clear code without any specific lines, classes code and mappings.

Here is the code just for preparing data, I need to create a parent with one child to test:

Code:
...CreateSession();
Parent par = new Parent();
Child chd = new Child();
chd.Name = "First one";
chd.Parent = par;
par.Childrens.Add(chd);
...CurrentSession.Save(par);
Debug.WriteLine(...CurrentSession.GetIdentifier(par)); // output is "5"
...CurrentSession.Transaction.Commit();


Now, here is the code that works fine, it is because I load lazy collection using "Debug.WriteLine(par.Childrens.Count);":

Code:
...CreateSession();
Parent par = (Parent)...CurrentSession.Load(typeof(Parent), 5);
Debug.WriteLine(par.Childrens.Count); // output is "1"
Child chd = new Child();
chd.Name = "Second one";
chd.Parent = par;
par.Childrens.Add(chd);
Debug.WriteLine(par.Childrens.Count); // output is "2"
...CurrentSession.Save(chd); // I need to know the identifier, so I must save object
int i = (int)...CurrentSession.GetIdentifier(chd); // I know the identifier
Debug.WriteLine(par.Childrens.Count); // output is "2"
...CurrentSession.Transaction.Rollback();


And finally, the code which works in a bad way. I rollbacked all changes made by previous code, my database contains only one parent with one child before running the code. After adding new child to existing parent and after saving the new child, it is existing twice in the bag collection. The output of the "par.Childrens.Count" should be "2", not "3".

Code:
...CreateSession();
Parent par = (Parent)...CurrentSession.Load(typeof(Parent), 5);
Child chd = new Child();
chd.Name = "Second one";
chd.Parent = par;
par.Childrens.Add(chd);
...CurrentSession.Save(chd);
int i = (int)...CurrentSession.GetIdentifier(chd);
Debug.WriteLine(par.Childrens.Count); // output is "3"
...CurrentSession.Transaction.Rollback();


The last code just to show that everything works fine if the collection is loaded before save:

Code:
Parent par = (Parent)...CurrentSession.Load(typeof(Parent), 5);
Child chd = new Child();
chd.Name = "Second one";
chd.Parent = par;
par.Childrens.Add(chd);
Debug.WriteLine(par.Childrens.Count); // output is "2"
...CurrentSession.Save(chd);
int i = (int)...CurrentSession.GetIdentifier(chd);
Debug.WriteLine(par.Childrens.Count); // output is "2"


The code of the classes:

Code:
using System;
using System.Collections;

namespace TEST.Business.Subjects {
  public class Parent {
    private int m_Id;
    private IList m_Childrens = new ArrayList();

    public virtual int Id {
      get { return m_Id; }
    }

    public virtual IList Childrens {
      get { return m_Childrens; }
    }
  }

  public class Child {
    private int m_Id;
    private string m_Name;
    private Parent m_Parent;

    public virtual int Id {
      get { return m_Id; }
    }

    public virtual string Name {
      get { return m_Name; }
      set { m_Name = value; }
    }

    public virtual Parent Parent {
      get { return m_Parent; }
      set { m_Parent = value; }
    }
  }
}


And the mapping file:

Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping
  xmlns="urn:nhibernate-mapping-2.0"
  default-access="field.pascalcase-m-underscore"
  assembly="TEST.Business.Subjects"
  namespace="TEST.Business.Subjects"
>
  <class name="TEST.Business.Subjects.Parent" table="Parent" lazy="true">
    <id name="Id" type="Int32" column="id_Parent" unsaved-value="0" access="nosetter.pascalcase-m-underscore">
      <generator class="native" />
    </id>
    <bag name="Childrens" inverse="true" cascade="save-update"  lazy="true">
      <key column="id_Parent" />
      <one-to-many class="TEST.Business.Subjects.Child" />
    </bag>
  </class>

  <class name="TEST.Business.Subjects.Child" table="Child" lazy="true">
    <id name="Id" type="Int32" column="id_Child" unsaved-value="0" access="nosetter.pascalcase-m-underscore">
      <generator class="native" />
    </id>
    <many-to-one name="Parent" class="TEST.Business.Subjects.Parent" column="id_Parent" not-null="true" />
    <property name="Name" type="String" not-null="true" />
  </class>
</hibernate-mapping>


Please can someone help me?
Many thanks. David


Top
 Profile  
 
 Post subject:
PostPosted: Thu Jan 12, 2006 11:33 am 
Contributor
Contributor

Joined: Wed May 11, 2005 4:59 pm
Posts: 1766
Location: Prague, Czech Republic
This doesn't look like a bug to me, I can't explain it based just on the source provided, but I believe that one of the reasons is that you are using a <bag> because adding an element to an uninitialized bag does not cause it to initialize.

Also, are you sure that the native ID generator does not generate 0 for an id, causing the child to be saved twice?


Top
 Profile  
 
 Post subject:
PostPosted: Thu Jan 12, 2006 12:42 pm 
Beginner
Beginner

Joined: Mon Dec 19, 2005 4:13 am
Posts: 27
Location: Prague, Czech Republic
I am using SQL Server 2005 and using identity as Id generator. Only one item is saved correctly to the database. The problem is only in memory.

Also, this strange behavior is not occuring if association is saved by automatic flush, but only when using Save method explicitly. When association is not lazy, everything is fine.

I think it is very simple test case with typical parent-child association on typical enviroment (identity on sql server). In fact, all the required code to test it is in my previous post.


Top
 Profile  
 
 Post subject: Another question
PostPosted: Thu Jan 12, 2006 11:31 pm 
Regular
Regular

Joined: Tue Jan 03, 2006 7:21 am
Posts: 85
If I understand Sergey correctly (and that also explains the behavior) what is happening here is that it is adding to an un-initialized bag. save inserts data into DB. Now when the bag is initialized it reads the data from the DB and the data that was just entered appears twice in the bag after initialization (it was already there once before the bag was initialized).

Is this what is happening. But should not NH clear the contents of the bag before initializing.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Jan 13, 2006 8:32 am 
Contributor
Contributor

Joined: Wed May 11, 2005 4:59 pm
Posts: 1766
Location: Prague, Czech Republic
I investigated this today, and it is a bug, but it's again one of those that are hard to fix. The problem is as samujob described, you are adding an entity to an uninitialized bag and also saving it and it ends up in the bag twice.

If you don't use the identity generator, the entity will not be saved during Save (only an identifier will be generated), so there is no problem when loading the bag.

If you flush the session or initialize the bag before adding the entity there also is no problem because the delayed addition mechanism is not used in this case.

So, I can only recommend several workarounds:
- use a different identifier generator (hilo for example),
- flush the session manually after saving any entities with identity generators,
- use a set instead of a bag
- use a bag with inverse="false" (this will disable delayed additions)


Top
 Profile  
 
 Post subject:
PostPosted: Fri Jan 13, 2006 9:06 am 
Beginner
Beginner

Joined: Mon Dec 19, 2005 4:13 am
Posts: 27
Location: Prague, Czech Republic
Sergey,

many thanks for explanation. I think using the <set> will be best :-).

David


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.