在调用自定义NHibernate IIdentifierGenerator之后确保插入

Nic*_*cki 5 legacy nhibernate nhibernate-mapping

设置

我们数据库的一些"旧旧旧"表使用了一种奇特的主键生成方案[1],我试图用NHibernate覆盖这部分数据库.这种生成方案主要隐藏在称为"ShootMeInTheFace.GetNextSeededId"的存储过程中.

我写了一个IIdentifierGenerator调用这个存储过程:

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);
     }
}
Run Code Online (Sandbox Code Playgroud)

问题

我可以在XML映射文件中映射它,它工作得很好,但....

当NHibernate尝试批量插入时,例如在级联中,或者Flush()在每次调用Save()依赖于此生成器的瞬态实体之后未编辑会话时,它不起作用.

那是因为NHibernate似乎正在做类似的事情

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

[execute the sql in one big batch]
Run Code Online (Sandbox Code Playgroud)

这不起作用,因为由于生成器每次询问数据库,NHibernate最终会获得多次生成相同的ID,因为它实际上还没有保存任何内容.

其他NHibernate生成器IncrementGenerator似乎通过向数据库询问种子值一次然后在同一会话中的后续调用期间递增内存中的值来解决这个问题.如果必须的话,我宁愿不在我的实现中执行此操作,因为我需要的所有代码都已经存在于数据库中,只是等待我正确调用它.

  • 有没有办法让NHibernate在每次调用生成特定类型实体的ID后实际发出INSERT?摆弄批量大小设置似乎没有帮助.
  • 除了在内存中重新生成生成代码或者对遗留数据库的某些触发器进行抽薹之外,您是否有任何建议/其他解决方法?我想我总是可以将这些视为"已分配"的生成器,并尝试以某种方式隐藏在域模型的内部......

谢谢你的建议.

更新:2个月后

在下面的答案中建议我使用an IPreInsertEventListener来实现此功能.虽然这听起来很合理但是有一些问题.

第一个问题是将id实体设置为AssignedGenerator然后实际上没有在代码中分配任何东西(因为我期望我的新IPreInsertEventListener实现完成工作)导致异常被抛出AssignedGenerator,因为它的Generate()方法基本上什么都不做但只检查确保id不为null,否则抛出异常.通过创建我自己的IIdentifierGenerator,就像AssignedGenerator没有例外一样,这很容易解决.

第二个问题是从我的新函数中返回null IIdentifierGenerator(我编写的那个用来克服问题AssignedGenerator导致NHibernate内部的问题抛出一个异常,抱怨生成了一个null id.好的,很好,我改变了我IIdentifierGenerator的返回哨兵字符串值,比方说,"NOT-REALLY-THE-REAL-ID",知道我IPreInsertEventListener会用正确的值替换它.

第三个问题,也就是最终的交易破坏者,就是IPreInsertEventListener在这个过程中运行得太晚,你需要更新实际的实体对象以及NHibernate使用的状态值数组.通常这不是问题,你可以按照Ayende的例子.但该id领域涉及的问题有三个IPreInsertEventListeners:

  • 该属性不在@event.State数组中,而是在其自己的Id属性中.
  • 该Id物业没有公共set访问者.
  • 仅更新实体而不更新Id属性会导致"NOT-REALLY-THE-REAL-ID"sentinel值传递到数据库,因为IPreInsertEventListener无法插入到正确的位置.

所以我在这一点上的选择是使用反射来获得NHibernate的属性,或者真的坐下来说"看,这个工具本来就不应该用这种方式."

所以我回到原来的状态IIdentifierGenreator,让它适用于懒惰的刷新:它在第一次调用时从数据库中获得了很高的值,然后我在C#中为后续调用重新实现了ID生成函数,在Increment生成器之后对其进行建模:

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;
}
Run Code Online (Sandbox Code Playgroud)

这似乎工作了一段时间,但像increment生成器一样,我们不妨称之为TimeBombGenerator.如果有多个工作进程在非可序列化事务中执行此代码,或者如果有多个实体映射到同一个数据库表(它是一个旧数据库,它发生了),那么我们将获得具有相同lastGenerated种子的此生成器的多个实例值,导致重复的身份.

@#$ @#$ @.

此时我的解决方案是使生成器缓存WeakReferences to ISessions及其lastGenerated值的字典.这样,lastGenerated它有效地局限于特定ISession的生命周期,而不是生命周期IIdentifierGenerator,并且因为我在WeakReferences每次Generate()调用开始时持有并剔除它们,所以这不会在内存消耗中爆炸.由于每个人ISession都会在第一次调用时访问数据库表,我们将获得必要的行锁(假设我们处于事务中)我们需要防止重复身份发生(如果他们这样做,例如来自幻影行,只ISession需要被抛弃,而不是整个过程).

它很丑陋,但比改变10年历史数据库的主键方案更可行.FWIW.


[1]如果你想知道ID生成,你可以获取当前PK列中所有值的子串(len - 2),将它们转换为整数并找到最大值,将一个加到该数字,添加全部该数字的数字,并将这些数字的总和作为校验和.(如果数据库有一行包含"1000001",那么我们将获得最大10000,+ 1等于10001,校验和为02,结果新PK为"1000102".不要问我为什么.

Jam*_*Ide 1

一个潜在的解决方法是在事件侦听器中生成并分配 ID,而不是使用 IIdentifierGenerator 实现。侦听器应实现 IPreInsertEventListener 并在 OnPreInsert 中分配 ID。