Настройка
Некоторые из «старых, старых» таблиц нашей базы данных используют экзотическую схему генерации первичного ключа [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);
}
}
Проблема
Я могу отобразить это в файлах сопоставления 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]
Это не работает, потому что, поскольку генератор каждый раз запрашивает базу данных, NHibernate просто заканчивает тем, что получает один и тот же идентификатор, сгенерированный несколько раз, , поскольку он еще ничего не сохранил.
Другие генераторы NHibernate, такие как IncrementGenerator
, похоже, обходят это, запрашивая у базы данных начальное значение один раз, а затем увеличивая значение в памяти во время последующих вызовов в том же сеансе. Я бы предпочел не делать этого в моей реализации, если бы пришлось, поскольку весь код, который мне нужен, уже находится в базе данных, просто ожидая, чтобы я его правильно вызвал.
- Есть ли способ заставить NHibernate фактически выдавать INSERT после каждого вызова для генерации идентификатора для сущностей определенного типа? Вращение с настройками размера пакета, похоже, не помогает.
- Есть ли у вас какие-либо предложения / другие обходные пути, кроме повторной реализации кода генерации в памяти или использования некоторых триггеров для устаревшей базы данных? Я думаю, я всегда мог бы рассматривать их как «назначенные» генераторы и пытаться как-то скрыть этот факт в рамках модели домена ....
Спасибо за любой совет.
Обновление: через 2 месяца
В ответах ниже было предложено использовать IPreInsertEventListener
для реализации этой функции. Хотя это звучит разумно, с этим было несколько проблем.
Первая проблема заключалась в том, что установка id
сущности на AssignedGenerator
и затем фактически не присваивание чего-либо в коде (поскольку я ожидал, что моя новая реализация IPreInsertEventListener
выполнит эту работу) привела к исключению выдается AssignedGenerator
, так как его метод Generate()
по сути не делает ничего, кроме проверки, чтобы убедиться, что id
не равен NULL, в противном случае выдается исключение. Это достаточно легко обойти, создав мой собственный IIdentifierGenerator
, который похож на AssignedGenerator
без исключения.
Вторая проблема заключалась в том, что возвращение нулевого значения из моего нового IIdentifierGenerator
(тот, который я написал для преодоления проблем с AssignedGenerator
, привел к тому, что внутренности NHibernate выдавали исключение, жалуясь на то, что был сгенерирован нулевой идентификатор. хорошо, я изменил свой IIdentifierGenerator
, чтобы он возвращал строковое значение, скажем, "NOT-REALLY-THE-REAL-ID", зная, что мой IPreInsertEventListener
заменит его на правильное значение.
Третья проблема и окончательное прерывание сделки заключалось в том, что IPreInsertEventListener
выполняется настолько поздно, что вам необходимо обновить как реальный объект сущности, так и массив значений состояний, которые использует NHibernate. Обычно это не проблема, и вы можете просто следовать примеру Айенде. Но есть три проблемы с полем id
, относящиеся к IPreInsertEventListeners
:
- Свойство находится не в массиве
@event.State
, а в собственном свойстве Id
.
- Свойство
Id
не имеет общедоступного set
метода доступа.
- Обновление только сущности, но не свойства
Id
, приводит к тому, что дозорное значение NOT-REALLY-THE-REAL-ID передается в базу данных, поскольку IPreInsertEventListener
не удалось вставить в нужных местах.
Таким образом, в этот момент я решил использовать рефлексию, чтобы получить доступ к этому свойству NHibernate, или по-настоящему сесть и сказать: «Посмотрите, инструмент просто не должен был использоваться таким образом».
Итак, я вернулся к своему исходному IIdentifierGenreator
и заставил его работать для отложенных сбросов: он получил высокое значение из базы данных при первом вызове, а затем я повторно реализовал эту функцию генерации идентификатора в C # для последующих вызовов, моделируя это после генератора 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;
}
Некоторое время это работает нормально, но, как и генератор increment
, мы могли бы также назвать его TimeBombGenerator. Если есть несколько рабочих процессов, выполняющих этот код в несериализуемых транзакциях, или если есть несколько сущностей, сопоставленных с одной и той же таблицей базы данных (это старая база данных, это произошло), то мы получим несколько экземпляров этого генератора с одинаковым lastGenerated
начальное значение, что приводит к дублированию идентификаторов.
@ # $ @ # $ @. * * 1092
На этом этапе я решил сделать кэш генератора словарём от WeakReference
s до ISessions
и их lastGenerated
значений. Таким образом, lastGenerated
эффективно зависит от времени жизни конкретного ISession
, а не от времени жизни IIdentifierGenerator
, и потому что я держу WeakReferences
и отбираю их в начале каждого Generate()
вызов, это не взорвется в потреблении памяти. И так как каждый ISession
будет попадать в таблицу базы данных при первом вызове, мы получим необходимые блокировки строк (при условии, что мы находимся в транзакции), нам нужно предотвратить возникновение дублирующихся идентификаторов (и если они это сделают, такие как из фантомного ряда, нужно выбросить только ISession
, а не весь процесс).
Это некрасиво, но более осуществимо, чем изменение схемы первичного ключа 10-летней базы данных. FWIW.
<ч />
[1] Если вы хотите узнать о генерации идентификатора, вы берете подстроку (len - 2) из всех значений, находящихся в данный момент в столбце PK, приводите их к целым числам и находите максимум, добавляете одно к этому числу добавьте все цифры этого числа и добавьте сумму этих цифр в качестве контрольной суммы. (Если в базе данных есть одна строка, содержащая «1000001», то мы получим максимум 10000, +1 равно 10001, контрольная сумма - 02, в результате новый PK будет «1000102». Не спрашивайте меня, почему.