确保调用自定义 NHibernate IIdentifierGenerator 后进行插入
设置
我们数据库的一些“old old old”表使用了一种奇特的主键生成方案 [1],我试图用 NHibernate 覆盖数据库的这一部分。此生成方案主要隐藏在名为“ShootMeInTheFace.GetNextSeedId”的存储过程中。
我写了一个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);
}
}
问题是
我可以在XML映射文件中映射它并且它工作得很好,但是......
当NHibernate尝试时它不起作用批量插入,例如级联插入,或者在每次调用瞬态实体上的 Save()
后会话未进行 Flush()
处理时取决于这个发电机。
这是因为 NHibernate 似乎在做类似的事情,
for (each thing that I need to save)
{
[generate its id]
[add it to the batch]
}
[execute the sql in one big batch]
这不起作用,因为,由于生成器每次都询问数据库,NHibernate 最终只会多次生成相同的 ID,因为它实际上还没有保存任何内容
其他 NHibernate 生成器(例如 IncrementGenerator)似乎通过向数据库询问一次种子值,然后在后续调用期间增加内存中的值来解决此问题。同一个会话。如果必须的话,我宁愿不在我的实现中这样做,因为我需要的所有代码已经位于数据库中,只是等待我正确调用它。
- 有没有办法让 NHibernate 在每次调用为特定类型的实体生成 ID 后实际发出 INSERT ?摆弄批量大小设置似乎没有帮助。
- 除了在内存中重新实现生成代码或将某些触发器连接到旧数据库之外,您还有什么建议/其他解决方法吗?我想我总是可以将它们视为“分配的”生成器,并尝试以某种方式将这一事实隐藏在域模型的内部......
感谢您的任何建议。
更新:2 个月后
下面的答案中建议我使用 IPreInsertEventListener
来实现此功能。虽然这听起来很合理,但存在一些问题。
第一个问题是将实体的 id
设置为 AssignedGenerator
,然后实际上没有在代码中分配任何内容(因为我期待新的 IPreInsertEventListener
> 实现来完成这项工作)导致 AssignedGenerator
抛出异常,因为它的 Generate()
方法除了检查以确保 id
不为空,否则抛出异常。通过创建我自己的 IIdentifierGenerator
就可以很容易地解决这个问题,它就像 AssignedGenerator
一样,没有例外。
第二个问题是,从我的新 IIdentifierGenerator
返回 null(我为克服 AssignedGenerator
的问题而编写的)导致 NHibernate 的内部抛出异常,抱怨好的,好的,我更改了我的 IIdentifierGenerator
以返回一个哨兵字符串值,例如“NOT-REALLY-THE-REAL-ID”,因为我知道我的 IPreInsertEventListener
code> 会将其替换为正确的值。
第三个问题,也是最终的问题,是 IPreInsertEventListener
在该过程中运行得太晚,以至于您需要更新实际的实体对象以及NHibernate 使用的状态值数组 通常是这样的。不是问题,您可以按照 Ayende 的示例进行操作。 但是与 IPreInsertEventListeners
相关的 id
字段存在三个问题:
- 该属性不在
@event.State
数组,而是在其自己的Id
属性中。 Id
属性没有公共set
访问器。- 仅更新实体而不更新
Id
属性会导致“NOT-REALLY-THE-REAL-ID”哨兵值传递到数据库,因为IPreInsertEventListener
无法实现插入正确的位置。
所以我此时的选择是使用反射来获取 NHibernate 属性,或者真正坐下来说“看,这个工具不适合这样使用。”
所以我回到原来的 IIdentifierGenreator 并使其适用于延迟刷新:它在第一次调用时从数据库中获取了高值,然后我在 C# 中重新实现了该 ID 生成函数以供后续使用调用,在增量生成器之后对其进行建模:
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;
}
这似乎在一段时间内工作正常,但与增量生成器一样,我们不妨将其称为 TimeBombGenerator。如果有多个工作进程在不可序列化的事务中执行此代码,或者有多个实体映射到同一个数据库表(这是一个旧数据库,它发生了),那么我们将获得此生成器的多个实例,具有相同的 < code>lastGenerate 种子值,导致重复的身份。
@#$@#$@。
此时我的解决方案是让生成器缓存一个由 ISessions
及其 lastGenerated
值的 WeakReference
组成的字典。这样,lastGenerate
实际上是特定 ISession
的生命周期的本地变量,而不是 IIdentifierGenerator
的生命周期,并且因为我持有WeakReferences
并在每次 Generate()
调用开始时剔除它们,这不会导致内存消耗激增。由于每个 ISession 都会在第一次调用时访问数据库表,因此我们将获得必要的行锁(假设我们处于事务中),我们需要防止重复身份的发生(并且如果确实如此,例如来自幻像行,则只需丢弃 ISession,而不是整个过程)。
它很丑陋,但比更改 10 年历史的数据库的主键方案更可行。 FWIW。
[1] 如果您想了解 ID 生成,请获取 PK 列中当前所有值的子字符串 (len - 2),将它们转换为整数并找到最大值,将该数字加一,然后将所有值相加该数字的数字,并附加这些数字的总和作为校验和。 (如果数据库中有一行包含“1000001”,那么我们将得到最大值 10000,+1 等于 10001,校验和为 02,结果新的 PK 为“1000102”。不要问我为什么。
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 ownId
property. - The
Id
property does not have a publicset
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 theIPreInsertEventListener
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 WeakReference
s 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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
一个潜在的解决方法是在事件侦听器中生成并分配 ID,而不是使用 IIdentifierGenerator 实现。侦听器应实现 IPreInsertEventListener 并在 OnPreInsert 中分配 ID。
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.
你为什么不直接创建私有字符串lastGenerate;静止的?
Why dont you just make private string lastGenerated; static?