Noob Quesion about using GuidComb

52 views
Skip to first unread message

Charles Jenkins

unread,
Sep 29, 2014, 1:14:37 PM9/29/14
to nhu...@googlegroups.com
Hi, everyone. The documentation and NH books I have lead me to believe that you should be able to create an entire object graph and then persist it with one call to Save(), assuming it's all done with in a single session and transaction.

I'm using GUIDs and the GuidComb generator for every entity in my tiny sample program. I find that each object is assigned an empty GUID (all zeroes) when it's created, and the GuidComb generator assigns the real ID upon a call to Save(). This is as I expect.

The problem is, persisting doesn't work right unless I create the entities in order and save after creating each one. If I have an object graph with empty GUIDs, I get various exceptions depending on where the call to Save() occurs.

What follows is lengthy, so I'll start by asking my questions here:

  • What am I doing wrong so that NH doesn't work as advertised?
  • If I'm not doing anything wrong, would it be legit to make my entities get their GuidComb ID's upon creation, rather than having an empty GUID up until the Save()?

Here's an example:

    private void RecreateBtn_Click( object sender, EventArgs e ) {

     
// Use NHibernate to delete everything from the database

     
using ( var sess = RemoteSessionHelper.SessionFactory.OpenSession() ) {
       
using ( var tran = sess.BeginTransaction() ) {
         
var list = sess
             
.QueryOver<Company>()
             
.List();
         
foreach ( var company in list ) {
            sess
.Delete( company );
         
}
          tran
.Commit();
       
}
     
}

     
// Now re-add the sample data

     
using ( var sess = RemoteSessionHelper.SessionFactory.OpenSession() ) {
       
using ( var tran = sess.BeginTransaction() ) {

         
var company = new Company() {
           
OfficialName = "Rossum Corporation",
           
Nickname = "Rossum",
           
City = "Cerritos",
           
State = "CA",
           
Country = "US"
         
};

         
//sess.Save( company );

         
var contact = new Contact() {
           
Name = "Adelle DeWitt",
           
Address1 = "[On File]",
           
City = "Los Angeles",
           
State = "CA",
           
Country = "US",
           
PostCode = "[On File]"
         
};

          company
.AddContact( contact );

         
//sess.Save( contact );

          sess
.Save( company );
          tran
.Commit();
       
}
     
}


See the two Save() calls I've commented out? Both are necessary. If I leave either one commented out, I'll get a stale object exception or a non-unique ID exception (because two objects have zeroed GUIDS) or a DAO exception based on a foreign-key constraint (if company.CompanId is a real GUID because it was saved but contact.ContactId is still empty). If I uncomment them so there are three Save() calls for these two objects, everything works and the proper rows appear in the database.

Here are related sources that might answer any questions you have about what I'm doing:

namespace DemoData {

 
public interface IRowVersionedObject {

   
int RowVersion { get; set; }

 
}
}

namespace DemoData {

 
public interface ITimestampedObject {

   
DateTime Timestamp { get; set; }

 
}
}

namespace DemoData.Domain {

 
public class Company : ITimestampedObject, IRowVersionedObject {

   
//--------------------------------------
   
#region Properties
   
//--------------------------------------

   
public virtual Guid CompanyId { get; protected set; }

   
public virtual string OfficialName { get; set; }

   
public virtual string City { get; set; }

   
public virtual string State { get; set; }

   
public virtual string Country { get; set; }

   
public virtual string Nickname { get; set; }

   
public virtual ISet<Contact> Contacts { get; protected set; }

   
public virtual ISet<Tag> Tags { get; protected set; }

   
public virtual DateTime Timestamp { get; set; }

   
public virtual int RowVersion { get; set; }

   
#endregion Properties

   
//--------------------------------------
   
#region Public Methods
   
//--------------------------------------

   
public Company() {
     
Contacts = new HashedSet<Contact>();
     
Tags = new HashedSet<Tag>();
   
}

   
public virtual Company AddContact( Contact contact ) {
      contact
.SetCompany( this );
     
return this;
   
}

   
public virtual Company AssociateTo( Tag tag ) {
      tag
.AssociateTo( this );
     
return this;
   
}

   
public override string ToString() {
     
return string.Format( "{0} ({1})", OfficialName, CompanyId );
   
}

   
#endregion Public Methods

 
}
}

namespace DemoData.Domain {

 
public class Contact : ITimestampedObject, IRowVersionedObject {

   
//--------------------------------------
   
#region Properties
   
//--------------------------------------

   
public virtual Guid ContactId { get; protected set; }

   
public virtual Company Company { get; set; }

   
public virtual string Name { get; set; }

   
public virtual string Address1 { get; set; }

   
public virtual string Address2 { get; set; }

   
public virtual string City { get; set; }

   
public virtual string State { get; set; }

   
public virtual string PostCode { get; set; }

   
public virtual string Country { get; set; }

   
public virtual ISet<Tag> Tags { get; protected set; }

   
public virtual DateTime Timestamp { get; set; }

   
public virtual int RowVersion { get; set; }

   
#endregion Properties

   
//--------------------------------------
   
#region Public Methods
   
//--------------------------------------

   
public Contact() {
     
Tags = new HashedSet<Tag>();
   
}

   
public virtual Contact SetCompany( Company company ) {
      company
.Contacts.Add( this );
     
this.Company = company;
     
return this;
   
}

   
public virtual Contact AssociateTo( Tag tag ) {
      tag
.AssociateTo( this );
     
return this;
   
}

   
public override string ToString() {
     
return string.Format( "{0} ({1})", Name, ContactId );
   
}

   
#endregion Public Methods

 
}
}

namespace DemoData.MySql.Mapping {

 
static public class Helpers {

   
/// <summary>
   
/// Map RowVersion consistently for concurrency versioning.
   
/// </summary>
   
/// <remarks>
   
/// The field is named oddly to avoid using a keyword as its name.
   
/// </remarks>

   
static public void MapRowVersion<T>( ClassMap<T> mapping ) where T : IRowVersionedObject {
      mapping
.Version( x => x.RowVersion )
       
.Column( "RowVer" )
       
.Generated
       
.Always()
       
.UnsavedValue( "null" );
   
}

   
/// <summary>
   
/// Map Timestamp to colmn "TmStamp" for reconciliation timestamping
   
/// </summary>
   
/// <remarks>
   
/// The field is named oddly to avoid using a keyword as its name, and to avoid
   
/// reference to the sql type "timestamp," which we don't want to use at all
   
/// </remarks>

   
static public void MapTimestamp<T>( ClassMap<T> mapping ) where T : ITimestampedObject {
      mapping
.Map( x => x.Timestamp )
       
.Column( "TmStamp" )
       
.CustomSqlType( "DATETIME" );
   
}

 
}

}

namespace DemoData.MySql.Mapping {

 
public class CompanyMap : ClassMap<Company> {

   
public CompanyMap() {

     
Table( COMPANY_TABLE_NAME );

     
Id( x => x.CompanyId );

     
Map( x => x.OfficialName ).Length( Globals.GENERAL_NAME_LENGTH ).Not.Nullable();
     
Map( x => x.City ).Length( Globals.GENERAL_NAME_LENGTH ).Not.Nullable();
     
Map( x => x.State ).Length( Globals.STATE_LENGTH ).Nullable();
     
Map( x => x.Country ).Length( Globals.COUNTRY_LENGTH ).Not.Nullable();
     
Map( x => x.Nickname ).Length( Globals.GENERAL_NAME_LENGTH ).Not.Nullable();

     
HasMany( x => x.Contacts )
       
.Inverse()
       
.Cascade.AllDeleteOrphan();

     
HasManyToMany( x => x.Tags )
       
.Table( TagMap.COMPANY_TAG_TABLE_NAME )
       
.ParentKeyColumn( "CompanyId" )
       
.ChildKeyColumn( "TagId" )
       
.Cascade.All();

     
Helpers.MapTimestamp<Company>( this );
     
Helpers.MapRowVersion<Company>( this );

   
}

   
// To prevent compatability problems, use lowercase table names on all platforms
   
// MySQL options file setting: lower_case_table_names = 1

   
public const string COMPANY_TABLE_NAME = "company";

 
}

}

namespace DemoData.MySql.Mapping {

 
public class ContactMap : ClassMap<Contact> {

   
public ContactMap() {

     
Table( CONTACT_TABLE_NAME );

     
Id( x => x.ContactId );

     
References( x => x.Company ).Not.Nullable();

     
Map( x => x.Name ).Length( Globals.GENERAL_NAME_LENGTH ).Not.Nullable();
     
Map( x => x.Address1 ).Length( Globals.GENERAL_NAME_LENGTH ).Not.Nullable();
     
Map( x => x.Address2 ).Length( Globals.GENERAL_NAME_LENGTH ).Nullable();
     
Map( x => x.City ).Length( Globals.GENERAL_NAME_LENGTH ).Not.Nullable();
     
Map( x => x.State ).Length( Globals.STATE_LENGTH ).Nullable();
     
Map( x => x.PostCode ).Length( Globals.POST_CODE_LENGTH ).Nullable();
     
Map( x => x.Country ).Length( Globals.COUNTRY_LENGTH ).Not.Nullable();

     
HasManyToMany( x => x.Tags )
       
.Table( TagMap.CONTACT_TAG_TABLE_NAME )
       
.ParentKeyColumn( "ContactId" )
       
.ChildKeyColumn( "TagId" )
       
.Cascade.All();

     
Helpers.MapTimestamp<Contact>( this );
     
Helpers.MapRowVersion<Contact>( this );

   
}

   
// To prevent compatability problems, use lowercase table names on all platforms
   
// MySQL options file setting: lower_case_table_names = 1

   
public const string CONTACT_TABLE_NAME = "contact";

 
}

}

namespace DemoData.MySql {

 
public class MyForeignKeyConvention : ForeignKeyConvention {

   
protected override string GetKeyName( Member property, Type type ) {
     
if ( property == null ) {
       
return type.Name + "Id";
     
}
     
return property.Name + "Id";
   
}

 
}
}

namespace DemoData.MySql {

 
/// <summary>
 
/// This convention changes how the primary keys are named in the database
 
/// and how NHibernate creates them.
  /// </summary>

 
public class MyIdConvention : IIdConvention {

   
public void Apply( IIdentityInstance instance ) {
      instance
.GeneratedBy.GuidComb();
      instance
.Column( instance.EntityType.Name + "Id" );
   
}

 
}

}






Ricardo Peres

unread,
Sep 29, 2014, 4:39:11 PM9/29/14
to nhu...@googlegroups.com
Hi,

Some remarks:
1) you don't need two Save calls if you configure cascading from one entity's property to the other entity (company to contact); try "all";
2) the way you are deleting objects is vary bad, in performance terms; see, for example, http://weblogs.asp.net/ricardoperes/deleting-entities-in-nhibernate;
3) if you don't need GuidComb (and I guess not) you can use other Guid flavors for a slight performance improvement - GuidComb ids are ordered temporally.

RP

Roman Lescano

unread,
Sep 29, 2014, 11:09:45 PM9/29/14
to nhu...@googlegroups.com
Hey Ricardo,
      I always thought that guid.comb was better than guid strategy if you want to improve performance.
Like this post link said, Guid.comb turns out to have a lesser fragmentation in the table when you compare it guid strategy.

Would you mind to explain further your point 3) ?

Thanks in advance!

Roman



RP

--
You received this message because you are subscribed to the Google Groups "nhusers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to nhusers+u...@googlegroups.com.
To post to this group, send email to nhu...@googlegroups.com.
Visit this group at http://groups.google.com/group/nhusers.
For more options, visit https://groups.google.com/d/optout.



--

Román

Ricardo Peres

unread,
Sep 30, 2014, 12:28:17 AM9/30/14
to nhu...@googlegroups.com
Yes, I was only talking about the actually generation of Guids using the two algorithms. GuidComb has to maintain ordering on top of a regular Guid generation algorithm, so it will take slightly more, I reckon. As for the physical storage, yes, you are right, the clustered primary key is not fragmented. Perhaps that's more significant, yes.

RP

Charles Jenkins

unread,
Oct 1, 2014, 11:37:22 AM10/1/14
to nhu...@googlegroups.com
Ricardo,

(I'm retrying a post that never appeared yesterday.)

I don't understand remark 1 yet. As you can see in the CompanyMap object, Cascade All is configured in the Company -> Contact mapping. Where else did you mean I should put it?

Ricardo Peres

unread,
Oct 1, 2014, 6:22:26 PM10/1/14
to nhu...@googlegroups.com

Hi, Charles!

I apologize, I didn’t see your mappings because Google Groups was hiding part of the message - I had to click a "show trimmed message" link that I hadn't noticed.

Don't see anything obvious. Perhaps some other community member who is more skilled than I have can help. I'll have a look anyway and will let you know.

RP

Ricardo Peres

unread,
Oct 4, 2014, 2:16:19 AM10/4/14
to nhu...@googlegroups.com
Hi,

I replied to you privately with a slightly modified working sample.
Some notes:

- You didn't include code for the Tag and TagMap classes; shame on you! ;-)
- The Timestamp properties were not being set; are you going to use some listener for that? If so, you didn't mention it;
- The Version was using an UnsavedValue of "NULL", which doesn't really make sense - the field and the underlying column are both mapped as int, which is never null - so I removed it and used DefaultValue;

Everything works as expected, in my machine, at least!

RP

Charles Jenkins

unread,
Oct 6, 2014, 9:02:57 AM10/6/14
to nhu...@googlegroups.com
Ricardo,

Thank you very much. To be clear for anyone who searches the archives for information about similar difficulties, my entire problem was the default value of “NULL” instead of “0” for a database-generated integer field.

The field in question is my RowVersion property, mapped to a database field named RowVer which is auto-generated by the database (actually, set by a trigger). I didn’t think its default value mattered after telling NH it was generated by the database, but obviously the lesson I’ve learned is that all non-nullable values have to be legal, even if they’re not correct, before save operations will work properly.

To answer your question about Timestamp, its value starts out as whatever crazy date the default for DateTime is, but it’s discarded upon write. Just like with RowVersion, I’m using a MySQL trigger to set it when the row gets created or updated. Once I took what you taught me and changed my helper methods to these:

    static public void MapRowVersion<T>( ClassMap<T> mapping ) where T : IRowVersionedObject {

      mapping.Version( x => x.RowVersion )

        .Column( "RowVer" )

        .UnsavedValue( "0" )

        .Generated.Always();

    }


    static public void MapTimestamp<T>( ClassMap<T> mapping ) where T : ITimestampedObject {

      mapping.Map( x => x.Timestamp )

        .Column( "TmStamp" )

        .CustomSqlType( "DATETIME" )

        .Generated.Always();

    }


I was successfully and repeatably able to create a more complex object graph and persist it with a single Save():

      using ( var sess = RemoteSessionHelper.OpenSession() ) {

        using ( var tran = sess.BeginTransaction() ) {


          var evil = new Tag() { Text = "evil" };


          var company = new Company() {

            OfficialName = "Rossum Corporation",

            Nickname = "Rossum",

            City = "Cerritos",

            State = "CA",

            Country = "US"

          };


          company.Tags.Add( evil );

          company.Tags.Add( new Tag() { Text = "scientific" } );

          company.Tags.Add( new Tag() { Text = "conglomerate" } );


          var contact = new Contact() {

            Name = "Adelle DeWitt",

            Address1 = "[On File]",

            City = "Los Angeles",

            State = "CA",

            Country = "US",

            PostCode = "[On File]"

          };


          contact.Tags.Add( evil );

          contact.Tags.Add( new Tag() { Text = "miss-lonely-heart" } );


          company.AddContact( contact );


          sess.Save( company );

          tran.Commit();

        }

      }


Wonderful! Thanks again :-)

-- 

Charles

--
You received this message because you are subscribed to a topic in the Google Groups "nhusers" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/nhusers/H1693w-OFE0/unsubscribe.
To unsubscribe from this group and all its topics, send an email to nhusers+u...@googlegroups.com.

Ricardo Peres

unread,
Oct 6, 2014, 12:33:57 PM10/6/14
to nhu...@googlegroups.com
Great! Glad I could help! ;-)

RP
Reply all
Reply to author
Forward
0 new messages