Генератор NHibernate HiLo генерирует дубликаты идентификаторов
У меня есть приложение, работающее на nHibernate v4.0.4.4000 - оно работает на трех независимых веб-серверах. Для генерации ID я использую стандартную реализацию HiLo по умолчанию (уникальный идентификатор через таблицы).
Иногда он создает дубликат идентификатора при сохранении новых объектов со следующей трассировкой стека:
at NHibernate.AdoNet.SqlClientBatchingBatcher.DoExecuteBatch(IDbCommand ps)
at NHibernate.AdoNet.AbstractBatcher.ExecuteBatchWithTiming(IDbCommand ps)
at NHibernate.AdoNet.AbstractBatcher.ExecuteBatch()
at NHibernate.AdoNet.AbstractBatcher.PrepareCommand(CommandType type, SqlString sql, SqlType[] parameterTypes)
at NHibernate.AdoNet.AbstractBatcher.PrepareBatchCommand(CommandType type, SqlString sql, SqlType[] parameterTypes)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object id, Object[] fields, Boolean[] notNull, Int32 j, SqlCommandInfo sql, Object obj, ISessionImplementor session)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object id, Object[] fields, Object obj, ISessionImplementor session)
at NHibernate.Action.EntityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at Xena.Database.Main.Listeners.Strategies.CreateEntityAuditTrailStrategy.Execute(Object criteria) in K:\Projects\Xena\WorkDir\src\Xena.Database.Main\Listeners\Strategies\CreateEntityAuditTrailStrategy.cs:line 41
at Xena.Domain.Rules.Strategies.StrategyExtensions.Execute[TCriteria](IEnumerable`1 strategies, TCriteria criteria) in K:\Projects\Xena\WorkDir\src\Xena.Domain\Rules\Strategies\RelayStrategy.cs:line 55
at NHibernate.Action.EntityInsertAction.PostInsert()
at NHibernate.Action.EntityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultAutoFlushEventListener.OnAutoFlush(AutoFlushEvent event)
at NHibernate.Impl.SessionImpl.AutoFlushIfRequired(ISet`1 querySpaces)
at NHibernate.Impl.SessionImpl.List(CriteriaImpl criteria, IList results)
at NHibernate.Impl.CriteriaImpl.List(IList results)
at NHibernate.Impl.CriteriaImpl.UniqueResult[T]()
at Xena.Web.EntityUpdaters.LedgerPostPreviewUpdater.TryCreateNewFiscalEntity(ISession session, FiscalSetup fiscalSetup, LedgerPostPreview& entity, IEnumerable`1& errors) in K:\Projects\Xena\WorkDir\src\Xena.Web\EntityUpdaters\LedgerPostPreviewUpdater.cs:line 52
at Xena.Web.SecurityContext.<>c__DisplayClass8_0`1.<TrySaveUpdate>b__0(ISession session, TEntity& entity, IEnumerable`1& errors) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 235
at Xena.Web.SecurityContext.<>c__DisplayClass41_0`1.<TrySave>b__0(ITransaction tx) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 815
at Xena.Web.SecurityContext.TryWrapInTransaction[T](Func`2 action, T& result, IEnumerable`1& errors, Boolean alwaysCommit) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 804
at Xena.Web.SecurityContext.TrySave[TEntity](IEntityUpdater`1 entityUpdater, EntityCreate`1 create) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 812
at Xena.Web.SecurityContext.TrySaveUpdate[TEntity](IFiscalEntityUpdater`1 entityUpdater) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 236
at Xena.Web.Api.XenaFiscalApiController.WrapSave[TEntity,TDto](IFiscalEntityUpdater`1 updater, Func`2 get, Action`2 postGet) in K:\Projects\Xena\WorkDir\src\Xena.Web\Api\Abstract\XenaFiscalApiController.cs:line 35
at Xena.Web.Api.ApiLedgerPostPreviewController.Post(LedgerPostPreviewDto ledgerPostPreview) in K:\Projects\Xena\WorkDir\src\Xena.Web\Api\ApiLedgerPostPreviewController.cs:line 79
at lambda_method(Closure , Object , Object[] )
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters)
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()
И следующее сообщение:
Message=Violation of PRIMARY KEY constraint 'PK_LedgerPostPreview'. Cannot insert duplicate key in object 'dbo.LedgerPostPreview'. The duplicate key value is (94873244).
The statement has been terminated.
SessionFactory настроен на использование SnapshotIsolation, DB устанавливается на уровне совместимости 2008 (100)
Насколько я могу судить, обновление значения hilo выполняется в транзакции отдельно от "нормальных" транзакций (я пробовал вызывать исключение - значение hilo не откатывается (что имеет смысл)).
Согласно профилировщику NHibernate, SQL-прогон против сервера для значений hilo:
Reading high value:
select next_hi
from hibernate_unique_key with (updlock, rowlock)
Updating high value:
update hibernate_unique_key
set next_hi = 5978 /* @p0 */
where next_hi = 5977 /* @p1 - next_hi */
Что мне не хватает? Не следует ли защищать HiLo от дубликатов?
EDIT: повторяющиеся идентификаторы выполняются не только на одной таблице, но и в таблицах с очень частыми вставками и удалениями. Вышеприведенный код был самым простым среди подозреваемых и очень прост - он только .Get()
родительский, чтобы проверить, что он там, а затем создает и вызывает .Save()
в новом сущности вместе со строкой контрольного журнала (в которой используется сценарий события PostInsert в nHibernate).
EDIT2: Id-Mapping для вышеуказанного типа (используется для всех объектов):
public static void MapId<TMapping, TType>(this TMapping mapping)
where TMapping : ClassMapping<TType>
where TType : class,IHasId
{
mapping.Id(m => m.Id, m => m.Generator(Generators.HighLow, g => g.Params(new { max_lo = 100 })));
}
Странная часть состоит в том, что (из-за комментария @Dexions), когда я проверяю как контрольный журнал, так и таблицу - ничего не было сохранено. Код, используемый для сохранения, выглядит следующим образом:
using (var tx = Session.BeginTransaction())
{
try
{
var voucherPreview = Session.Get<VoucherPreview>(voucherPreviewId); //Parent
var postPreview = //Factory create with the voucherPreview;
var index = Session.QueryOver<LedgerPostPreview>()
.Where(lpp => lpp.VoucherPreview == voucherPreview)
.SelectList(l => l.SelectMax(lpp => lpp.Index))
.SingleOrDefault<int>() + 1
postPreview.Index = index;
// Set a few other properties and check validity
Session.SaveOrUpdate(postPreview);
}
catch(Exception ex)
{
//Errorhandling leading to the above stacktrace
}
}
Ответы
Ответ 1
Я понял проблему. Оказывается, это не имело никакого отношения к Id.
Как часть инструкции insert, мы обновляем вторичную таблицу, которая управляет числовой серией. Проблема возникает, если эта вторичная таблица испытывает ошибку изоляции моментального снимка - поскольку все обрабатывается внутри SQLCommandSets внутри nHibernate - ошибка создает пузырьки цепи с ошибочным описанием.
Ответ 2
Учитывая цепочку комментариев на вопрос, IMHO есть два возможных случая, о которых я могу думать в данный момент.
Вы либо пропускаете сеансы nhibernate, и вы получаете состояние скрытой гонки, когда происходит генерация фактического идентификатора на данном экземпляре (поскольку идентификация в базе данных изолирована транзакцией). Предполагается, что экземпляр приложения тот же успешно вставлен {ID = 123}, а затем попытался вставить другой объект с {ID = 123}. Вы можете трассировать вставки обратно в экземпляры приложений, чтобы убедиться, что дублирование вложений происходит в одном экземпляре. Я не очень уверен, действительно ли этот сценарий правдоподобен во всей цепочке конвейера NHibernate, но ISession не является потокобезопасным (и это известный факт). Вы говорите, что это работает уже 4 года (хотя вы не упоминаете, что ошибка была там так долго), поэтому, возможно, недавняя фиксация ввела это поведение (коллекции .AsParallel() было бы достаточно, чтобы вызвать ее Я верю)?
Другой подход к проблеме предполагает, что уже вставленный объект был загружен, а затем отсоединен от ISession, но получил повторное присоединение (по дизайну или случайно) к (тому же/другому) ISession, которое затем быстро попыталось вставить объект. Это может произойти, и гипотетический сценарий может быть
- var entity123 = Get (123)
- var entity123 = entity123.Clone() или ISession.Evict(entity123).
- где-то вдоль строки, которую вы вызываете SaveOrUpdate (entity123) (или, что еще хуже, для отслеживания вы добавляете ее в ссылочную коллекцию с правилами каскадного сохранения)
- NHibernate видит объект с управляемым объектом с идентификатором на месте, пытается
вставьте его.
В некоторой более ранней версии NHibernate я видел это поведение с неидентичными вставками.
Вышеупомянутое может также произойти с методом bad/dumb factory, который также копирует идентификатор.
Проследить эту проверку, если вставляются параметры SQL (для log4net, которая была бы записью NHibernate.SQL с отладкой, хотя я думаю, что профилировщик NHibernate также обнаружит это) сопоставить существующие значения столбца строки. Если они точно совпадают, то может произойти нечто подобное выше. Если они частично совпадают, возможно, вы делаете частичные копии сущностей, и он также случайно копирует идентификатор.
Ответ 3
что, если вы только что изменили на:
postPreview.Index = index+1;