Hibernate Books

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 11 posts ] 
Author Message
 Post subject: Help: need to persist a collection of components
PostPosted: Thu Sep 20, 2007 9:26 am 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
Hello guys.

This time i'll try to give all the info on what i believe is a bug on how nhibernate works (again, it might also be that i'm completly lost in how it works, but that's why i'm posting it here).

lets start with the database. I have the following tables (SQL Server 2005 express):

CREATE TABLE [dbo].[Aluno](
[IdAluno] [int] IDENTITY(1,1) NOT NULL,
[Nome] [varchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
CONSTRAINT [PK_Aluno] PRIMARY KEY CLUSTERED
(
[IdAluno] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]


CREATE TABLE [dbo].[Disciplina](
[IdDisciplina] [int] IDENTITY(1,1) NOT NULL,
[IdAluno] [int] NOT NULL,
[Nome] [varchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
CONSTRAINT [PK_Disciplina] PRIMARY KEY CLUSTERED
(
[IdDisciplina] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]


The idea is that each row on the table Aluno (which, btw, means Student in english) has 0 or more entries on the table Disciplina (i believe that the translation of Disciplina is subject).

My classes look like this:

namespace NHibernateTests
{
public class Subject
{
private string _name;

public string Name
{
get { return _name; }
set { _name = value; }
}
}
}

namespace NHibernateTests
{
public class Student
{
private int _id;
private string _name;

private IList<Subject> _subjects = new List<Subject>();

public int Id
{
get { return _id; }
set { _id = value; }
}

public string Name
{
get { return _name; }
set { _name = value; }
}

public IList<Subject> Subjects
{
get { return _subjects; }
set { _subjects = value; }
}
}
}

Nothing too fancy (btw, in the real word i'd return a readonly collection from the subjects property, but in this demo i'm just returning the _subjects field).

In my case, Subject is really a component and i don't intend to follow the life time of each instance of that class (this was a decision i've made since i needed to model a one-to-many relation between an entity - in this case, Student - and one or more components).

So, after reading the existing docs on the nhibernate site, I've ended up with the following hbm file:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping
xmlns="urn:nhibernate-mapping-2.2"
schema="dbo">
<class name="NHibernateTests.Student,NHibernateTests"
table="Aluno" lazy="false">
<id name="Id" column="IdAluno" unsaved-value="0">
<generator class="identity" />
</id>

<property name="Name" column="Nome" />
<bag name="Subjects" cascade="all" lazy="false" table="Disciplina" >
<key column="IdAluno" />
<composite-element class="NHibernateTests.Subject,NHibernateTests">
<property name="Name" column="Nome" />
</composite-element>
</bag>

</class>
</hibernate-mapping>


ok, maybe i got it wrong, but to me, this file says something like this:

"Each instance of type studend is persisted on the Aluno table. It has a Name property, which is mapped to column Nome in that table. There's also a collection of elements of type Subject maintained on the Subjects property of the object. Each element in this collection is a component (that is what i think the <composite-element> is for) and each should be persisted on the Disciplina table. The relation between the Disciplina table and the "main" Aluno table is performed through the IdAluno field - which gets its value from the Id element of the Student class"

first thing: is this description completly wrong?

Ok, supposing that it's correct, then i want to create a new student with one or more subjects and persist it to the database by using a transaction. here's the code I've tried to run to achieve that:

//auxiliary method that creates a new subject
private static Subject AddSubject(string p)
{
Subject sub = new Subject();
sub.Name = p;

return sub;
}


static void Main(string[] args)
{
Configuration cfg = new Configuration();
cfg.Configure(DB_PATH); //path to config file
ISessionFactory factory = cfg.BuildSessionFactory();
ISession session = factory.OpenSession();

Student std = new Student();
std.Name = "Luis";

std.Subjects.Add(AddSubject("Port"));
std.Subjects.Add(AddSubject("Eng"));

ITransaction tran = session.BeginTransaction();
session.Save(std);

tran.Rollback();
session.Flush();
}

Ok, i start by getting the necessary info for creating an ISession. Then, a new "dummy" student is created with 2 subjects and i call the Save method to indicate my willingness to save the new object that i've just created.

Now, instead of commiting the transaction, i roll it back and flush the session. My question is: what should happen after flushing the data?

Since i really don't know nhibernate, i might be completly wrong, but i expected to see 0 rows in both tables. Unfortunately, the subject objects maintained by the std objects are inserted during the flush method call.

i really expected to see the SQL instructions for inserting items on the table Aluno and Disciplina written on the log after calling the Save method. Unfortunately, nhibernate only generates the sql for inserting items on the Aluno table and even though it shows it's trying to cascade the insert, there really isn't any entry regarding any of the instances maintained on the Subjects proeprty.

Interestingly, when i call flush, it seems like it recreates the elements on the collection and only then it persits those instances to the database, leaving me with 2 entries on the Disciplina table and none on the Aluno table (note that this is possible because i didn't explicitly set the IAluno on table Disciplina as a foreign key).

Now, I thought this was a bug and opened an entry onthe issue tracker, which was quickly closed by saying that "you don't know how nhibernate works". Yes, this is correct, but are you saying that a save operation made over an object in this scenario shouldn't be cascaded to each component maintained on a list that is owned by that object? This can't be true.

for instance, imagine a test. in that case, before calling rollback, i'd get a reference to the connectio object used by nhibernate and i'd perform a select over the table to get the element that has been added. if the behavior i've described here is the "correct" behavior (which i really don't think it is) then that means that i'd get the "temporary" student object but wouldn't be able to get any subject from the Disciplina table since it wasn't really inserted to the db.

in fact, even if you don't think about the test, this can't be the correct behavior. if it is, then what's the purpose of the transaction?
btw, i've transformed my subject class in an entity with bi-directional relation and in that case, cascading works as expected.

so, can anyone tell me why this works like this?

thanks!

P.S.: there's a sample on the issue tracker with code that shows this problem

http://jira.nhibernate.org/browse/NH-1141


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 1:54 pm 
Contributor
Contributor

Joined: Wed May 11, 2005 4:59 pm
Posts: 1766
Location: Prague, Czech Republic
Here is one of the few basic rules of NHibernate: every change you do to your objects is reflected in the database as late as possible.

You can force changes to happen by calling Flush(), otherwise they will happen when you commit a transaction.

And by the way, rolling back a transaction and then calling Flush() on a session is bad style. Even calling Flush() explicitly is something you shouldn't really do, use transactions instead and commit the transaction when you're done.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 3:36 pm 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
Hello Sergey.

let me start by thanking you for help.

now, to the important things...

i agree with what you've said about calling flush. i think that my main point in the previous post is understanding why does the collection insertions occur outside of the transaction that was used for inserting the main object in the Aluno table...that's what i'm not getting right now.

maybe i'm not reading the log correctly....

thanks again


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 3:58 pm 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
Hello again. for the sake of completion, i'm just puttin ghere what i see in the log when i perform the (wrong) flush call. i'm just putting it here to explain what i'm still no getting about the call of the save method. here's the important part:

NHibernate.Impl.SessionImpl..ctor(:0) - opened session
NHibernate.Transaction.AdoTransaction.Begin(:0) - begin
NHibernate.Connection.DriverConnectionProvider.GetConnection(:0) - Obtaining IDbConnection from Driver
NHibernate.Impl.SessionImpl.DoSave(:0) - saving [NHibernateTests.Student#<null>]
NHibernate.Impl.SessionImpl.ExecuteInserts(:0) - executing insertions
NHibernate.Engine.Cascades.Cascade(:0) - processing cascades for: NHibernateTests.Student
NHibernate.Engine.Cascades.Cascade(:0) - done processing cascades for: NHibernateTests.Student
NHibernate.Impl.WrapVisitor.ProcessArrayOrNewCollection(:0) - Wrapped collection in role: NHibernateTests.Student.Subjects
NHibernate.Persister.Entity.AbstractEntityPersister.Insert(:0) - Inserting entity: NHibernateTests.Student (native id)
NHibernate.Impl.BatcherImpl.LogOpenPreparedCommand(:0) - Opened new IDbCommand, open IDbCommands: 1
NHibernate.Impl.BatcherImpl.Generate(:0) - Building an IDbCommand object for the SqlString: INSERT INTO dbo.Aluno (Nome) VALUES (?); select SCOPE_IDENTITY()
NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate(:0) - Dehydrating entity: [NHibernateTests.Student#<null>]
NHibernate.Type.NullableType.NullSafeSet(:0) - binding 'Luis' to parameter: 0
NHibernate.Impl.BatcherImpl.LogCommand(:0) - INSERT INTO dbo.Aluno (Nome) VALUES (@p0); select SCOPE_IDENTITY(); @p0 = 'Luis'
NHibernate.Impl.BatcherImpl.LogOpenReader(:0) - Opened IDataReader, open IDataReaders: 1
NHibernate.Type.NullableType.NullSafeGet(:0) - returning '14' as column:
NHibernate.Persister.Entity.AbstractEntityPersister.GetGeneratedIdentity(:0) - Natively generated identity: 14
NHibernate.Driver.NHybridDataReader.Dispose(:0) - running NHybridDataReader.Dispose()
NHibernate.Impl.BatcherImpl.LogCloseReader(:0) - Closed IDataReader, open IDataReaders :0
NHibernate.Impl.BatcherImpl.LogClosePreparedCommand(:0) - Closed IDbCommand, open IDbCommands: 0
NHibernate.Engine.Cascades.Cascade(:0) - processing cascades for: NHibernateTests.Student
NHibernate.Engine.Cascades.CascadeCollection(:0) - cascading to collection: NHibernateTests.Student.Subjects
NHibernate.Engine.Cascades.Cascade(:0) - done processing cascades for: NHibernateTests.Student
NHibernate.Transaction.AdoTransaction.Rollback(:0) - rollback
NHibernate.Transaction.AdoTransaction.Rollback(:0) - running AdoTransaction.Dispose()

NHibernate.Impl.ConnectionManager.AggressiveRelease(:0) - aggressively releasing database connection
NHibernate.Connection.ConnectionProvider.CloseConnection(:0) - Closing connection
NHibernate.Impl.SessionImpl.AfterTransactionCompletion(:0) - transaction completion
NHibernate.Impl.SessionImpl.FlushEverything(:0) - flushing session
NHibernate.Engine.Cascades.Cascade(:0) - processing cascades for: NHibernateTests.Student
NHibernate.Engine.Cascades.CascadeCollection(:0) - cascading to collection: NHibernateTests.Student.Subjects
NHibernate.Engine.Cascades.Cascade(:0) - done processing cascades for: NHibernateTests.Student
NHibernate.Impl.SessionImpl.FlushEntities(:0) - Flushing entities and processing referenced collections
NHibernate.Impl.FlushVisitor.ProcessCollection(:0) - Processing collection for role NHibernateTests.Student.Subjects
NHibernate.Impl.SessionImpl.UpdateReachableCollection(:0) - Collection found: [NHibernateTests.Student.Subjects#14], was: [<unreferenced>]
NHibernate.Impl.SessionImpl.FlushCollections(:0) - Processing unreferenced collections
NHibernate.Impl.SessionImpl.FlushCollections(:0) - scheduling collection removes/(re)creates/updates
NHibernate.Impl.SessionImpl.FlushEverything(:0) - Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects
NHibernate.Impl.SessionImpl.FlushEverything(:0) - Flushed: 1 (re)creations, 0 updates, 0 removals to 1 collections
NHibernate.Impl.Printer.ToString(:0) - listing entities:
NHibernate.Impl.Printer.ToString(:0) - NHibernateTests.Student{Id=14, Name=Luis, Subjects=[Subject{Name=Port}, Subject{Name=Eng}]}
NHibernate.Impl.SessionImpl.Execute(:0) - executing flush
NHibernate.Impl.SessionImpl.Execute(:0) - registering flush begin
NHibernate.Persister.Collection.AbstractCollectionPersister.Recreate(:0) - Inserting collection: [NHibernateTests.Student.Subjects#14]
NHibernate.Impl.BatcherImpl.LogOpenPreparedCommand(:0) - Opened new IDbCommand, open IDbCommands: 1
NHibernate.Impl.BatcherImpl.Generate(:0) - Building an IDbCommand object for the SqlString: INSERT INTO dbo.Disciplina (IdAluno, Nome) VALUES (?, ?)
NHibernate.Type.NullableType.NullSafeSet(:0) - binding '14' to parameter: 0
NHibernate.Type.NullableType.NullSafeSet(:0) - binding 'Port' to parameter: 1
NHibernate.Impl.BatcherImpl.LogCommand(:0) - INSERT INTO dbo.Disciplina (IdAluno, Nome) VALUES (@p0, @p1); @p0 = '14', @p1 = 'Port'
NHibernate.Connection.DriverConnectionProvider.GetConnection(:0) - Obtaining IDbConnection from Driver
NHibernate.Type.NullableType.NullSafeSet(:0) - binding '14' to parameter: 0
NHibernate.Type.NullableType.NullSafeSet(:0) - binding 'Eng' to parameter: 1
NHibernate.Impl.BatcherImpl.LogCommand(:0) - INSERT INTO dbo.Disciplina (IdAluno, Nome) VALUES (@p0, @p1); @p0 = '14', @p1 = 'Eng'
NHibernate.Persister.Collection.AbstractCollectionPersister.Recreate(:0) - done inserting collection: 2 rows inserted
NHibernate.Impl.BatcherImpl.LogClosePreparedCommand(:0) - Closed IDbCommand, open IDbCommands: 0
NHibernate.Impl.ConnectionManager.AfterStatement(:0) - skipping aggressive-release due to flush cycle
NHibernate.Impl.ConnectionManager.FlushEnding(:0) - registering flush end
NHibernate.Impl.ConnectionManager.AggressiveRelease(:0) - aggressively releasing database connection
NHibernate.Connection.ConnectionProvider.CloseConnection(:0) - Closing connection
NHibernate.Impl.SessionImpl.PostFlush(:0) - post flush


now, the point i'm trying to drive home is that i belive that the insertions for the collection elements should be before the NHibernate.Transaction.AdoTransaction.Rollback(:0) - rollback line (ie, why am i not seeing the db commands being prepared for the insert of each element in the collection). i'm probably wrong, but i'd like to know why.

thanks again.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 4:17 pm 
Hibernate Team
Hibernate Team

Joined: Tue Jun 13, 2006 11:29 pm
Posts: 315
Location: Calgary, Alberta, Canada
Let's see if I can explain this; here is the essence of the code you gave earlier:

Code:
1:  ISession sess = OpenSession();
2: 
3:  Subject subjectA = new Subject("A");
4:  Subject subjectB = new Subject("B");
5:  Student student = new Student();
6:  student.Subjects.Add(subjectA);
7:  student.Subjects.Add(subjectB);
8: 
9:  ITransaction tran = session.BeginTransaction();
10: session.Save(student);
11:
12: tran.Rollback();
13: session.Flush();


On line 10, the student would have got inserted into the databaes; but the subject would not have (due to how NHibernate works). When you rollback on line 12, the inserted student is removed. When you call Flush() on line 13 after rolling back your transaction on line 12 (which is bad practice as Sergey said), NHibernate flush the subjects to database. Since you are not in a transaction anymore on line 13, the Subjects stays in the database.

Bottomline? Two cardinal rules:
  1. Close and discard the ISession if you rollback a transaction
  2. Close and discard the ISession if it throws an exception

_________________
Karl Chu


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 5:04 pm 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
hello Karl.

yeah, i'll do that, but i must say that i'm still not totally convinced about this step. for instance, if i had a data insertion test, it would fail miserably. let me explain why.

Imagine that after line 10 i needed to see if the data was correctly inserted on the db. then i might just run a select command for the Aluno table (entity Student). it would work since i'd get the row for the main entity object. however, i would not get any subjects because those elements are only generated (where generated means that an insert command is built and run over the existing transaction) when you call commit.

not sure on what you think, but this does not sound right to me...

bottom line: ok, doing what you say makes it work but i still have lots of reservations regarding the way the insert statements are generated. btw, i think that one of my experiences consisted in transforming the subject component in an entity and when i did that, everything run as i expected (i might be wrong, since i've tried it and looked at the log, but then thought that using that approach would be completly wrong since i'd be sacrificing my domain model just to make nhibernate work)


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 5:32 pm 
Hibernate Team
Hibernate Team

Joined: Tue Jun 13, 2006 11:29 pm
Posts: 315
Location: Calgary, Alberta, Canada
labreu wrote:
Imagine that after line 10 i needed to see if the data was correctly inserted on the db. then i might just run a select command for the Aluno table (entity Student). it would work since i'd get the row for the main entity object. however, i would not get any subjects because those elements are only generated (where generated means that an insert command is built and run over the existing transaction) when you call commit.


If data consistency is important to you, you should make sure your transaction isolation level is set to read-committed. In that case, the Student entity (row) would not be visible outside of the NHibernate transaction until the transaction is committed; at that point, your Subjects would be inserted. If you have that much concern that Subjects must be inserted immediately after the Student is saved, ISession.Flush() is there for that very reason.

Generally speaking, you can trust NHibernate to do the right thing. Speaking from my own experiences with NHibernate, I would not be concerned about what you are concerned about here. I don't know what else to say.

_________________
Karl Chu


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 5:44 pm 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
hello again.

hum...but i'm running the select over the same connection that has the opened transaction. my objective is to get that row. for instance, the idea is to run a test like this:

begin tran
insert into db
get new id
perform select using the connection that has the tran opened
load the object
see if it has the expected values
roll back tran

ok, now this might seem like extra workm but i'm just trying to ensure that the insertion run ok and that eventual calculated values that will only be calculated on the db are correct. now, since the elements of the collection aren't persisted, if i try to run a test that looks like the previous, then it'll fail because the elements defined on the collection were not inserted on the db...

maybe i'm not explaining myself correctly...


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 20, 2007 6:08 pm 
Hibernate Team
Hibernate Team

Joined: Tue Jun 13, 2006 11:29 pm
Posts: 315
Location: Calgary, Alberta, Canada
labreu wrote:
hello again.

hum...but i'm running the select over the same connection that has the opened transaction. my objective is to get that row. for instance, the idea is to run a test like this:

begin tran
insert into db
get new id
perform select using the connection that has the tran opened
load the object
see if it has the expected values
roll back tran

ok, now this might seem like extra workm but i'm just trying to ensure that the insertion run ok and that eventual calculated values that will only be calculated on the db are correct. now, since the elements of the collection aren't persisted, if i try to run a test that looks like the previous, then it'll fail because the elements defined on the collection were not inserted on the db...

maybe i'm not explaining myself correctly...


Here is how I would write the test you described; I have done this many times:
  • Open a session
  • Begin tran
  • Insert into db
  • Flush the session (is flush something evil that you just don't want to do?)
  • Clear the session [ISession.Clear()]
  • Load the object
  • ... or run some ADO.NET code inside the NH-managed connection and transaction (see below)
  • See if it has the expected values
  • Roll back tran
  • Close and discard session


To run an ADO.NET command inside the NH transaction, do this:
Code:
// ...
ITransaction tx = session.BeginTransaction();
IDbCommand cmd = session.Connection.CreateCommand();
cmd.CommandText = "select * from whatever";
tx.Enlist(cmd);
cmd.Execute();
// ... other stuff that follows

_________________
Karl Chu


Top
 Profile  
 
 Post subject:
PostPosted: Fri Sep 21, 2007 3:47 am 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
hello again Karl.

ok, i'll try that approach. I was under the impression that flushing shouldn't be done in the middle of a transaction, but again, i might be wrong.

thanks again and i'll come back to report my findings...


Top
 Profile  
 
 Post subject:
PostPosted: Fri Sep 21, 2007 4:16 am 
Beginner
Beginner

Joined: Wed Aug 29, 2007 8:23 am
Posts: 31
hello again.

I've tried your approach and it's working on my demo sample. i was wrong since i thought that calling flush would commit all the ongoing transactions, but this seems not to be the case.

however, i still maintain my opinion that calling shouldn't be needed since it should generate the sql insert statements for the component collection items at the same time as it generates the code for the main object.

anyway, thanks for all the help!


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 11 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.