У меня есть приложение, работающее на 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
}
}