The Domain
public class Person { private IList<Pet> pets = new List<Pet>(); public virtual string Name { get; set; } public virtual IEnumerable<Pet> Pets { get { return pets; } } public virtual Person Partner { get; set; } public virtual void Add(Pet pet) { pet.Owner = this; pets.Add(pet); } } public class Pet { public virtual Person Owner { get; set; } public virtual string Description { get; set; } }
One Full-mapping:
<class name="Person"> <id type="int"> <generator class="native"/> </id> <property name="Name"/> <many-to-one name="Partner" class="Person"/> <bag name="Pets" cascade="all" inverse="true" access="field.camelcase"> <key column="ownerId" on-delete="cascade"/> <one-to-many class="Pet"/> </bag> </class> <class name="Pet"> <id type="int"> <generator class="native"/> </id> <property name="Description"/> <many-to-one name="Owner" class="Person" column="ownerId"/> </class>Resuming:
- A class (Person) with two relationship
- One relation (Partner) doesn't have cascade
- One relation is a collection with all cascades
- The POID is only part of the persistence and we don’t have the ID property in the domain. This is useful especially in this case because I want run the same code changing only the id-generator-strategy, and the ID-Type, in the mapping.
The Demo
Each demo will be ran in two environments:- without set the adonet.batch_size property in the NHibernate configuration.
- setting the adonet.batch_size to 10
The main demo is the follow
using (ISession session = factory.OpenSession()) using (ITransaction tx = session.BeginTransaction()) { log.Debug("Saving"); var person = new Person {Name = "Person"}; var patner = new Person {Name = "PatnerOfPerson"}; person.Partner = patner; person.Add(new Pet {Description = "Reptile"}); person.Add(new Pet {Description = "Dog"}); person.Add(new Pet {Description = "Cat"}); session.Save(patner); session.Save(person); log.Debug("Saved"); log.Debug("Committing"); tx.Commit(); log.Debug("Committed"); }In a session+transaction I’m creating a Person, its partner, 3 pets associated to the Person; I’m saving the first the Partner (because it don’t has cascade actions) and then the Person. The log is to understand when “things” happens.
Two additional demos showing a “not recommended” usage of NHibernate (without enclose actions in one transaction) but useful to understand the behavior of generators.
The first:
public void AllInOneSession() { var animal = new Pet(); using (ISession session = factory.OpenSession()) { session.Save(animal); animal.Description = "Boa"; session.Update(animal); session.Delete(animal); } }I’m creating a Pet and Save+Update+Delete it all in the same session.
The second:
public void SaveWithoutFlushShouldDontPersist() { object savedId; using (ISession session = factory.OpenSession()) { var animal = new Pet(); savedId=session.Save(animal); animal.Description = "Boa"; } using (ISession session = factory.OpenSession()) { var found = session.Get<Pet>(savedId); if(found != null) log.Debug("A Pet was found even if the session was never flushed."); } }In the first session I’m saving a Pet without flush the session and in the second session I’m going to DB to ensure that the Pet was not saved.
In the first demo I will explain the meaning of the messages in the follow, when you see the same sequence, you can apply the same explication.
Ok, now I’m ready to begin the dance.
Using guid.comb or "guid
The “id” mapping here is:<id type="guid"> <generator class="guid.comb"/> </id>
Main demo results without use batcher:
SavingSaved
Committing
INSERT INTO Person ... 'PatnerOfPerson'
INSERT INTO Person ... 'Person'
INSERT INTO Pet ... 'Reptile'
INSERT INTO Pet ... 'Dog'
INSERT INTO Pet ... 'Cat'
Committed
Between “Saving” and “Saved” there is NO iteration with RDBMS, all thing happens during the nh-Transaction commit. In this case we are going five times to DB (one per command).
AllInOneSession results :
There is absolutely nothing nor a single log message.Save without flush results:
Nothing.Main demo results with BATCHER:
SavingSaved
Committing
Opened new IDbCommand, open IDbCommands: 1
1)Building an IDbCommand object for the SqlString: INSERT INTO Person (Name, Partner, id) VALUES (?, ?, ?)
...
2)Executing batch
Batch command: INSERT INTO Person ... 'PatnerOfPerson'
Batch command: INSERT INTO Person ... 'Person'
Closed IDbCommand
Opened new IDbCommand, open IDbCommands: 1
3)Building an IDbCommand object for the SqlString: INSERT INTO Pet (Description, ownerId, id) VALUES (?, ?, ?)
...
4)Executing batch
Batch command: INSERT INTO Pet ... 'Reptile'
Batch command: INSERT INTO Pet ... 'Dog'
Batch command: INSERT INTO Pet ... 'Cat'
Closed IDbCommand
Committed
Another time all happen during nh-transaction commit and what happen is:
- A command to insert People (note the plural) is created and prepared.
- In only one command the two persons are inserted.
- A command to insert Pets (note the plural) is created and prepared.
- In only one command the three animals are inserted.
Using “hilo”
The “id” mapping here is:<idtype="int">
<generatorclass="hilo"/>
</id>
Main demo results without use batcher:
SavingReading high value:select next_hi from hibernate_unique_key with (updlock, rowlock)
Updating high value:update hibernate_unique_key set next_hi = @p0 where next_hi = @p1
The New high value is: 2
Saved
Committing
INSERT INTO Person ... 'PatnerOfPerson'
INSERT INTO Person ... 'Person'
INSERT INTO Pet ... 'Reptile'
INSERT INTO Pet ... 'Dog'
INSERT INTO Pet ... 'Cat'
Committed
Between “Saving” and “Saved” there is one iteration with RDBMS to retrieve the High value. Note that in a fresh new RDBMS you will see one more iteration to initialize “hibernate_unique_key” table with the first high-value. If you repeat the demo inside the same session-factory instance nothing happen between “Saving”-“Saved” for long time (if I well remember the default max-low is Int16.MaxValue).
AllInOneSession results :
Nothing.Save without flush results:
Nothing.Main demo results with BATCHER:
SavingReading high value:select next_hi from hibernate_unique_key with (updlock, rowlock)
Updating high value:update hibernate_unique_key set next_hi = @p0 where next_hi = @p1
The New high value is: 2
Saved
Committing
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Person (Name, Partner, id) VALUES (?, ?, ?)
...
Executing batch
Batch command: INSERT INTO Person ... 'PatnerOfPerson'
Batch command: INSERT INTO Person ... 'Person'
Closed IDbCommand
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Pet (Description, ownerId, id) VALUES (?, ?, ?)
...
Executing batch
Batch command: INSERT INTO Pet ... 'Reptile'
Batch command: INSERT INTO Pet ... 'Dog'
Batch command: INSERT INTO Pet ... 'Cat'
Closed IDbCommand
Committed
The same behavior of “guid” (only two round-trips).
Using “identity”
As last your best friend Identity (or “native” if you are using Microsoft SQL server).The “id” mapping here is:
<id type="int"> <generator class="identity"/> </id>
And now, my friend, fasten your seat belt because I’m going to show you what happen in the roller-coaster.
Main demo results without use batcher:
SavingINSERT INTO Person ...; select SCOPE_IDENTITY(); … 'PatnerOfPerson'
INSERT INTO Person ... ; select SCOPE_IDENTITY(); … 'Person'
INSERT INTO Pet ... ; select SCOPE_IDENTITY();… 'Reptile'
INSERT INTO Pet ... ; select SCOPE_IDENTITY(); … 'Dog'
INSERT INTO Pet ... ; select SCOPE_IDENTITY(); … 'Cat'
Saved
Committing
Committed
As you can see the behavior is exactly the inverse of what you see using “guid”: here all happen during saving instead during commit. Each “INSERT INTO” ran in the same moment of session.Save(object) even if we don’t close the UnitOfWork and, to be more exactly, even if we don’t know what we will do with the UnitOfWork (we don’t know if we will close with commit or rollback). Five iteration with RDBMS are happening during UnitOfWork usage instead at the end of the UnitOfWork (exactly what we want avoid using UoW pattern).
AllInOneSession results :
INSERT INTO Pet (Description, ownerId) VALUES (@p0, @p1); select SCOPE_IDENTITY.Ups… something appear here and it is something really bad.
Save without flush results:
INSERT INTO Pet (Description, ownerId) VALUES (@p0, @p1); select SCOPE_IDENTITY()A Pet was found even if the session was never flushed.
WOW! We are having a record in the DB even if we don’t flush the session ?
Yes, nothing so strange if you are using identity-style generators.
Main demo results with BATCHER:
And now let me show you how much is useful the batcher when you are working with identity-style generators.Saving
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Person (Name, Partner) VALUES (?, ?); select SCOPE_IDENTITY()
INSERT INTO Person ...; select SCOPE_IDENTITY(); ... 'PatnerOfPerson'
Opened IDataReader, open IDataReaders: 1
...
Closed IDbCommand
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Person (Name, Partner) VALUES (?, ?); select SCOPE_IDENTITY()
INSERT INTO Person ...; select SCOPE_IDENTITY(); ...'Person'
Opened IDataReader, open IDataReaders: 1
...
Closed IDbCommand, open IDbCommands: 0
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Pet (Description, ownerId) VALUES (?, ?); select SCOPE_IDENTITY()
INSERT INTO Pet ...; select SCOPE_IDENTITY(); ...'Reptile'
Opened IDataReader, open IDataReaders: 1
...
Closed IDbCommand, open IDbCommands: 0
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Pet (Description, ownerId) VALUES (?, ?); select SCOPE_IDENTITY()
INSERT INTO Pet ...; select SCOPE_IDENTITY(); ... 'Dog'
Opened IDataReader, open IDataReaders: 1
...
Closed IDbCommand, open IDbCommands: 0
Opened new IDbCommand, open IDbCommands: 1
Building an IDbCommand object for the SqlString: INSERT INTO Pet (Description, ownerId) VALUES (?, ?); select SCOPE_IDENTITY()
INSERT INTO Pet ...; select SCOPE_IDENTITY(); ... 'Cat'
Opened IDataReader, open IDataReaders: 1
...
Closed IDbCommand, open IDbCommands: 0
Saved
Committing
Committed
Do you see ? … try to see again… Do you see ?
The target of the Batcher should be: Have better performance and less round-trips. Using identity-style generators, the benefits of the batcher are nullified and, obviously, don’t forget the rollback of the transaction because you don’t have another chance to nullify what happen in the session.Save().