Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ensuring inserts after a call to a custom NHibernate IIdentifierGenerator

The setup

Some of the "old old old" tables of our database use an exotic primary key generation scheme [1] and I'm trying to overlay this part of the database with NHibernate. This generation scheme is mostly hidden away in a stored procedure called, say, 'ShootMeInTheFace.GetNextSeededId'.

I have written an IIdentifierGenerator that calls this stored proc:

public class LegacyIdentityGenerator : IIdentifierGenerator, IConfigurable
{
    // ... snip ...
    public object Generate(ISessionImplementor session, object obj)
    {
        var connection = session.Connection;

        using (var command = connection.CreateCommand())
        {
            SqlParameter param;

            session.ConnectionManager.Transaction.Enlist(command);

            command.CommandText = "ShootMeInTheFace.GetNextSeededId";
            command.CommandType = CommandType.StoredProcedure;

            param = command.CreateParameter() as SqlParameter;
            param.Direction = ParameterDirection.Input;
            param.ParameterName = "@sTableName";
            param.SqlDbType = SqlDbType.VarChar;
            param.Value = this.table;
            command.Parameters.Add(param);

            // ... snip ...

            command.ExecuteNonQuery();

            // ... snip ...

            return ((IDataParameter)command
                .Parameters["@sTrimmedNewId"]).Value as string);
     }
}

The problem

I can map this in the XML mapping files and it works great, BUT....

It doesn't work when NHibernate tries to batch inserts, such as in a cascade, or when the session is not Flush()ed after every call to Save() on a transient entity that depends on this generator.

That's because NHibernate seems to be doing something like

for (each thing that I need to save)
{
    [generate its id]
    [add it to the batch]
}

[execute the sql in one big batch]

This doesn't work because, since the generator is asking the database every time, NHibernate just ends up getting the same ID generated multiple times, since it hasn't actually saved anything yet.

The other NHibernate generators like IncrementGenerator seem to get around this by asking the database for the seed value once and then incrementing the value in memory during subsequent calls in the same session. I would rather not do this in my implementation if I have to, since all of the code that I need is sitting in the database already, just waiting for me to call it correctly.

  • Is there a way to make NHibernate actually issue the INSERT after each call to generating an ID for entities of a certain type? Fiddling with the batch size settings don't seem to help.
  • Do you have any suggestions/other workarounds besides re-implementing the generation code in memory or bolting on some triggers to the legacy database? I guess I could always treat these as "assigned" generators and try to hide that fact somehow within the guts of the domain model....

Thanks for any advice.

The update: 2 months later

It was suggested in the answers below that I use an IPreInsertEventListener to implement this functionality. While this sounds reasonable, there were a few problems with this.

The first problem was that setting the id of an entity to the AssignedGenerator and then not actually assigning anything in code (since I was expecting my new IPreInsertEventListener implementation to do the work) resulted in an exception being thrown by the AssignedGenerator, since its Generate() method essentially does nothing but check to make sure that the id is not null, throwing an exception otherwise. This is worked around easily enough by creating my own IIdentifierGenerator that is like AssignedGenerator without the exception.

The second problem was that returning null from my new IIdentifierGenerator (the one I wrote to overcome the problems with the AssignedGenerator resulted in the innards of NHibernate throwing an exception, complaining that a null id was generated. Okay, fine, I changed my IIdentifierGenerator to return a sentinel string value, say, "NOT-REALLY-THE-REAL-ID", knowing that my IPreInsertEventListener would replace it with the correct value.

The third problem, and the ultimate deal-breaker, was that IPreInsertEventListener runs so late in the process that you need to update both the actual entity object as well as an array of state values that NHibernate uses. Typically this is not a problem and you can just follow Ayende's example. But there are three issues with the id field relating to the IPreInsertEventListeners:

  • The property is not in the @event.State array but instead in its own Id property.
  • The Id property does not have a public set accessor.
  • Updating only the entity but not the Id property results in the "NOT-REALLY-THE-REAL-ID" sentinel value being passed through to the database since the IPreInsertEventListener was unable to insert in the right places.

So my choice at this point was to use reflection to get at that NHibernate property, or to really sit down and say "look, the tool just wasn't meant to be used this way."

So I went back to my original IIdentifierGenreator and made it work for lazy flushes: it got the high value from the database on the first call, and then I re-implemented that ID generation function in C# for subsequent calls, modeling this after the Increment generator:

private string lastGenerated;

public object Generate(ISessionImplementor session, object obj)
{
    string identity;

    if (this.lastGenerated == null)
    {
         identity = GetTheValueFromTheDatabase();
    }
    else
    {
         identity = GenerateTheNextValueInCode();
    }

    this.lastGenerated = identity;

    return identity;
}

This seems to work fine for a while, but like the increment generator, we might as well call it the TimeBombGenerator. If there are multiple worker processes executing this code in non-serializable transactions, or if there are multiple entities mapped to the same database table (it's an old database, it happened), then we will get multiple instances of this generator with the same lastGenerated seed value, resulting in duplicate identities.

@#$@#$@.

My solution at this point was to make the generator cache a dictionary of WeakReferences to ISessions and their lastGenerated values. This way, the lastGenerated is effectively local to the lifetime of a particular ISession, not the lifetime of the IIdentifierGenerator, and because I'm holding WeakReferences and culling them out at the beginning of each Generate() call, this won't explode in memory consumption. And since each ISession is going to hit the database table on its first call, we'll get the necessary row locks (assuming we're in a transaction) we need to prevent duplicate identities from happening (and if they do, such as from a phantom row, only the ISession needs to be thrown away, not the entire process).

It is ugly, but more feasible than changing the primary key scheme of a 10-year-old database. FWIW.


[1] If you care to know about the ID generation, you take a substring(len - 2) of all of the values currently in the PK column, cast them to integers and find the max, add one to that number, add all of that number's digits, and append the sum of those digits as a checksum. (If the database has one row containing "1000001", then we would get max 10000, +1 equals 10001, checksum is 02, resulting new PK is "1000102". Don't ask me why.

like image 610
Nicholas Piasecki Avatar asked Nov 26 '22 12:11

Nicholas Piasecki


1 Answers

A potential workaround is to generate and assign the ID in an event listener rather than using an IIdentifierGenerator implementation. The listener should implement IPreInsertEventListener and assign the ID in OnPreInsert.

like image 120
Jamie Ide Avatar answered Nov 29 '22 02:11

Jamie Ide