Small suggestion for those who uses an integer identity value as PK.

Small suggestion for those who uses an integer identity value as PK.

Old forum URL: forums.lhotka.net/forums/t/1247.aspx


Henrik posted on Monday, September 18, 2006

Hi

 

A suggestion for those it may have interest.

 

Almost all of my classes use an integer identity value as primary key. I’ve used Rocky’s suggestion of how to generate a temporary id (see this article for more details: http://www.lhotka.net/Article.aspx?area=4&id=252e4f20-f202-4ba5-b6ff-4d629a2b7dcc)

 

However, it was a bit daunting to do this for every class, so I subclassed BusinessBase in a new IdBusinessBase, which looks like this:

 

<Serializable()> _

Public MustInherit Class IdBusinessBase(Of T As IdBusinessBase(Of T))

   Inherits BusinessBase(Of T)

 

#Region " Temporary Integer Id "

   'Shared temporary id for new objects

   Private Shared _lastTempId As Integer

 

   ''' <summary>

   ''' Returns a unique temporary integer id for an object of type T.

   ''' </summary>

   ''' <returns></returns>

   ''' <remarks>

   ''' Use this method to get a temporary integer id for a new object, if the object uses an

   ''' identity field as it's primary key in the database.

   ''' The temporary id is shared between all classes of type T and is a continuous

   ''' negative number starting from -1.

   ''' </remarks>

   Protected Function GetTemporaryId() As Integer

       SyncLock Me

          _lastTempId -= 1

       End SyncLock

       Return _lastTempId

   End Function

#End Region

End Class

 

I the constructor of your BO you assign your PK field with the return value of this method:

 

Private Sub New()

   _primaryKey = MyBase.GetTemporaryId()

End Sub

 

/Henrik

malloc1024 replied on Monday, September 18, 2006

This is one of the first things I added to my heavily modified version of the CSLA.NET 1.0 framework when it first came out.  It works out quite well; however, there is a chance that you could get a duplicate key when you mix objects that have a temp key with objects that have real keys.  I have found that this does not happen very often though.  You might want to check the value so it doesn't fall out of the range for integers. Compare it with Integer.MinValue.  If it falls out of range reset the id to -1.

Henrik replied on Monday, September 18, 2006

Thanks for your input malloc.

I hadn't thought of the id could get out of range. I'm so used to web apps where the webserver recycles the process every now and then (and thereby resetting the id back to -1). But of course in long running apps, this could happen. I'll add the check for Integer.MinValue to my code.

/Henrik

RockfordLhotka replied on Monday, September 18, 2006

Henrik:

   Protected Function GetTemporaryId() As Integer

       SyncLock Me

          _lastTempId -= 1

       End SyncLock

       Return _lastTempId

   End Function



Not to be overly picky, but you have a threading issue here.

In your SyncLock block you decrement the shared counter, which is fine. However, it is quite possible for Thread A to do this and exit the block. Then Thread B gets the lock and decrements the value again. Then both Thread A and Thread B would return _lastTempId - which would be the same value.

Either put the Return statement inside the block, or use a local temp variable to store the new value and return the temp variable's value.

Or remove the SyncLock block entirely - making the code not threadsafe, but faster. The problem with SyncLock is that there's overhead to getting that lock - so you slow down single-threaded code by using it, because that overhead cost gets paid for no benefit.

Henrik replied on Monday, September 18, 2006

Yep, you're right. I overlooked that the return statement should be inside the SyncLock block.

I'm no champ in multi-threading and thread-safety and therefore I may sometimes overuse locking. However most of my applications have two front-ends, a winform and a web. When the code runs, without synclock, in a winform, there shouldn't be any problems, since these are normally single-threaded apps (or I'm at least aware of when I'm using multithreading). But the web app, running in IIS, will be multithreaded due to simultaneous requests, won't it?

Is SyncLock the fastest way to have thread-safety or are there other methods that are faster. I recall, from my software engineering classes, there are mutex, semaphores and monitors but I haven't really dug into the .NET framework to see if they are there (they probably are) or what their performance is like.

/Henrik

Henrik replied on Monday, September 18, 2006

I looked up some of the System.Threading stuff in the .net framework help and found out that a SyncLock is actually a monitor behind the scenes. Futhermore a mutex is actually designed more for process synchronization than thread-sync.

I also found the Semaphore class, but this seems to be the slowest of all since the programmer needs to call Release for each WaitOne call, which would eventually require a Try-Finally block, which must be slower than the SyncLock mechanism.

/Henrik

 

RockfordLhotka replied on Monday, September 18, 2006

Yes, SyncLock (lock in C#) is a simple Monitor behind the scenes. So it is comparatively lightweight, but any lock objects have a performance cost and should only be used if absolutely needed.

btw, Microsoft recommends against locking against any public object - like Me or this. What you should so instead is create a private shared object against which you can lock:

Private Shared _lockObject As New Object

'...
SyncLock _lockObject


This avoids some odd edge cases that are hard to debug.

ItsDubC replied on Friday, October 27, 2006

I'm new to CSLA so forgive me if I'm overlooking something obvious, but I'm a little confused as to the purpose of having these temporary IDs.  Are these IDs assigned to objects until they are saved to the db, at which point their ID value is reassigned to the one generated by the identity column in the db?

Thanks in advance

ajj3085 replied on Friday, October 27, 2006

No problem.  Your first problem is that two new, but different objects will be considered Equal if they do not have a unique temporary id.  This is because BusinessBase overrides Equals, which compares the value from GetIdValue.. which if you dont' create temporary ids will be 0 (or whatever id you hardcoded).

This causes major problems when the objects are in a collection together.  Databinding will operate on the wrong object, delete the wrong object, etc. because it will attempt to remove the first object with the 0 id... and if all your new objects have that id, then the first one it finds is removed, even if you tried to remove the 3rd new object.

Make sense?
Andy

ItsDubC replied on Friday, October 27, 2006

This makes sense and I like this idea.  If I choose to save one of these objects however, would it be wise for DoInsertUpdate to return it's actual ID from the database after it is generated as part of the insert, and then update the object's ID with the permanent value?

I'm basically trying to figure out at what point the object's non-temporary ID gets assigned.

Again, thank you.

ajj3085 replied on Friday, October 27, 2006

Yes, after an insert operation you should update the objects key field... if you save it again, you need the valid id to ensure the update works.

Andy

DansDreams replied on Friday, October 27, 2006

Ah, I just love my GuidBusinessBase.  :)

ajj3085 replied on Friday, October 27, 2006

Ints really aren't that bad to work with... and with them taking I think 1/4 of the size, the hassle is easily worth the benefit Wink [;)]

JoeFallon1 replied on Friday, October 27, 2006

Sample code for an Insert:

DAL.ExecuteNonQuery(tr, CommandType.Text, DAO.Insert(mDescr, mName, ...))
'retrieve the newly generated Identity and update the PK in the BO so the child records will save correctly.
mKey = CInt(DAL.ExecuteScalar(tr, CommandType.Text, "SELECT Scope_Identity()"))

Joe

ajj3085 replied on Monday, October 30, 2006

Are you sure that will work?  I think those could execute in different transaction scopes..

Its probably better to do this within a stored proc, that way you're sure.

Andy

ItsDubC replied on Monday, October 30, 2006

So for example, I could have a basic stored proc such as

INSERT INTO tblCompany(Name, Phone)
VALUES (@name, @phone)
SET CompanyID = SCOPE_IDENTITY()


and on the application side have a SqlParameter called companyIDParameter as an output parameter in order to get the CompanyID, and just do

id = (int)companyIDParameter.Value;

after I execute the stored proc?

JoeFallon1 replied on Monday, October 30, 2006

Yes - I am sure it works.

The tr parameter is the transaction.

Joe

 

guyroch replied on Monday, October 30, 2006

JoeFallon1:

Yes - I am sure it works.

The tr parameter is the transaction.

Joe

I'm not so sure about that Joe.  I had a similar problem a few weeks ago and both command were using the same db transaction and it did not work.

JoeFallon1 replied on Monday, October 30, 2006

Guy,

Thanks for the response - and the heart attack. <g>

I was sure until I read your response. Now I have some doubt.

How do you know the tr is not carried forward and returns the correct answer?

After all, it has not been committed yet.

I could easily change it to the code sample you showed IF that "other" database (Oracle) was not involved in my platform. I would also need to know how to do it for Oracle. And to my knowledge Oracle does not support 2 statements in one "batch".

Looks like some research is required.

Thanks.

Joe

 

guyroch replied on Monday, October 30, 2006

JoeFallon1:

IF that "other" database (Oracle) was not involved in my platform.

You mean Obstacle 10g, right.  LOL 

http://www.cafepress.com/oraclehaters.15080672

Enjoy

JoeFallon1 replied on Monday, October 30, 2006

Yes - "I hate Oracle." Trying very hard to drop support for it. Need to get a couple of clients to wise up first though.

I have been reading about Scope_Identity - most people think it is easiest to use the 2 statements in a batch. But there was one question like this:

===============================================

If I do the following:
- Connect to a SQL Server 2000 database (using SqlClient objects)
- Start a transaction
- INSERT a new row
- Run “SELECT Scope_Identity”

will I get a value specific to my transaction?

===============================================

Answer:

Specific to your transaction, otherwise it wouldn't be much use.

===============================================

I think I will post some sample code over in the SQL Server group and see what they have to say.

My code has worked correctly for a long time now - but you have me nervous that it won't always work correctly. Say under load or something.

Joe

===============================================

Some sample code showing 2 separate queries to get the Identity value: (the Insert ran in a different method but the connection was passed to this method - like I pass my tr.)

    /// <summary>
    /// This is called when a row is updated, and it retrieves the latest identity
    /// value.  Note that it has to use the same connection which I can get from args
    /// Also note that I try both the sqlserver2000 and alternative just as an example.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args) {
      if (args.StatementType == StatementType.Insert) {
        SqlConnection con = args.Command.Connection;
        Object new_id = 0;

        // sqlserver 2000 version
        SqlCommand id_cmd = new SqlCommand("select scope_identity() as id", con);
        new_id = id_cmd.ExecuteScalar();
 

 

 

guyroch replied on Monday, October 30, 2006

Joe, take a look at this thread, which was one of my original thread :)

http://forums.lhotka.net/forums/thread/7101.aspx

You'll see that the tests I did a while back showed different results when using hard text sql command versus parametrized queries.

If you're using hard text sql commands then you are fine.  However, if you're using parametrized queries YOU MUST include the scope_Identity inside the same call.

Keep in mind though that parametrized queries are _faster_ than hard text sql command because they will get compiled and cached - so it will be faster going forward on subsequent calls.

JoeFallon1 replied on Monday, October 30, 2006

I am using hard text SQL queries, not parameterized queries.

I am aware of the risks/differences - except for the whole scope identity thing. <g>

Whew. Thanks.

guyroch replied on Monday, October 30, 2006

Sorry about the clogged arteries I gave you; glad to see you’re out of the intensive care unit though :)

 

Best regards,

JoeFallon1 replied on Wednesday, November 01, 2006

I found the reason for the issue with parameterized queries.

Answer posted here:

http://forums.lhotka.net/forums/thread/7101.aspx

 

Joe

RobKraft replied on Friday, July 20, 2007

This topic has deviated from the original issue, but I have a different solution to the temporary ID problem.  I let the collection class assign temporary IDs.

I have a private int in the collection:

      private int _nextTempID = 0;

I have one method in the collection used to add items to the collection and it has:

      componentRec.SubmitListID = _nextTempID++;

  this.Add(componentRec);

And in the collection's dataportal_fetch I set the _nextTempID so that when I load existing data, I won't be assigning a tempID value that matches a real id:

      while (xxx.Read())

      {

            ComponentList info = new ComponentList(xxx);

            this.Add(info);

            if (info.SubmitListID > maxID) 

            {

                  _nextTempID = info.SubmitListID + 1;

            }

Does anyone perusing this thread see a problem with this design?  I did have to scope the "setter"  of my unique SubmitListID of my component to be internal instead of private, but that seems a minor trade for the simplicity.

 

guyroch replied on Monday, October 30, 2006

For it work and to be executed in the same transaction scope, both commands (the insert and the scope identity) must be sent all at once.

mKey = CInt(DAL.ExecuteScalar(tr, CommandType.Text, "<<Insert command here>> ; SELECT Scope_Identity()"))

Notice the semi-colon ';' between both commands.  Doing it this way will work, this is how I have it setup.  If they are sent in 2 distinctive calls to the database, they will NOT run in the scope.

 

Copyright (c) Marimer LLC