проблема с транзакциями и тригерами

Alex S
Дата: 23.01.2009 15:55:07
Исходные данные. (Все упрощено в целях понимания) Есть 3 таблицы
DOCTABLE(.....DD datetime,ACCDE int ,ACCKR int, SUMM decimal(18,4) ...) - документы
OVERTABLE(.....DD datetime,ACCDE int ,ACCKR int, SUMM decimal(18,4) ...) - проводки
SALDOTABLE(.....ACC int, DD datetime,DEB decimal(18,4), KRE decimal(18,4),SALDO decimal(18,4)) - остатки

ACCDE - счет дебета
ACCKR - счет кредита
SUMM - сумма проводки

ACC счет
DEB оборот по дебету за DD
KRE оборот по кредиту за DD
SALDO остаток за DD

На DOCTABLE есть триггер:

CREATE TRIGGER dbo.DOCTABLE_CALC ON dbo.DOCTABLE  
FOR INSERT, UPDATE, DELETE 
AS
declare ....
Set NOCOUNT ON
Set XACT_ABORT ON
Set @LastIdent = @@Identity

while курсоры по inserted
begin
    if @ACCDE  is null
    begin
      raiserror ('Не указан счет дебета в документе',16,1)
      goto ex
    end
    if @ACCKR  is null
    begin
      raiserror ('Не указан счет кредита в документе',16,1)
      goto ex
    end
...
    Insert Into OVERTABLE (...) values (...)
end курсоры по inserted

while курсоры по deleted
begin
   delete from OVERTABLE where <Документ> = @ID
end курсоры по deleted


Set @s = 'SELECT Identity(int, ' + CAST(@LastIdent as varchar(10)) + ', 1 ) as I INTO #_T'
Exec(@s)

Set NOCOUNT OFF
return

ex:
Set NOCOUNT OFF
if @@trancount > 0 rollback

На OVERTABLE тоже есть триггер:

CREATE TRIGGER dbo.OVERTABLE_CALC ON dbo.OVERTABLE  
FOR INSERT, UPDATE, DELETE 
AS
Declare ...
Set NOCOUNT ON
Set XACT_ABORT ON
Set @LastIdent = @@Identity

while курсоры по deleted
begin
   exec CALCSALDO (@ACCDE ,2/* удалить движение*/,@DD,@SUMM,...)
   exec CALCSALDO (@ACCKR ,2/* удалить движение*/,@DD,@SUMM,...)
end курсоры по deleted

while курсоры по inserted
begin
   exec CALCSALDO (@ACCDE ,1/* положить движение*/,@DD,@SUMM,...) /*  место 1 */
   exec CALCSALDO (@ACCKR ,1/* положить движение*/,@DD,@SUMM,...) /*  место 2 */
end курсоры по inserted

Set @s = 'SELECT Identity(int, ' + CAST(@LastIdent as varchar(10)) + ', 1 ) as I INTO #_T'
Exec(@s)

Set NOCOUNT OFF
return

ex:
Set NOCOUNT OFF
if @@trancount > 0 rollback

Ну и последнее, процедура:

CREATE PROCEDURE dbo.CALCSALDO ...
Declare ...
SET nocount on
SET XACT_ABORT ON

Select @nCount=count(*) from SALDOTABLE 
with (PAGLOCK,XLOCK,HOLDLOCK) /*эта мешанина подобрана в ходе разборок 
с проблемой, но влияет только на дедлоки, проблему не устраняет*/
 where ACC=@acc AND DD >= @dd 

Select @AccDOpen = DOPEN,@ACCid = ID from ACCONTS where ID = @ACC

 /* проверки параметров счета */
 if @ACCid is null 
 begin
   RaisError('Счет не найден.',16,1)
   goto ex 
 end

 if @AccDOpen > @dd
 begin 
   RaisError('Счет не открыт на дату операции.',16,1)
   goto ex
 end

 if /* еще ряд проверок */
 begin 
   RaisError(' Сообщение о ошибке.',16,1)
   goto ex
 end

  /* НАЧАЛО ПЕРЕСЧЕТА ОСТАТКА */
  if /* записи в  SALDOTABLE за эту дату еще нет - вставка */
  begin
     insert into SALDOTABLE( ....
  end
  if /* запись в  SALDOTABLE за эту дату уже есть*/
  begin
     update SALDOTABLE( ....
  end
  if /* отменены все движения по счету за этут дату*/
  begin
     delete from SALDOTABLE where ....
  end
  /* КОНЕЦ ПЕРЕСЧЕТА */

 if /* проверка на красное сальдо по счету */
 begin 
   RaisError('Счет выходит на красное сальдо в дату ....',16,1) /*место 3 */
   goto ex
 end

SET nocount off
return

ex:
SET nocount off
if @@trancount > 0 rollback
Все "служебные" операторы (nocount,xact_abort и т.д) и структура текстов сохранены.

Этот код работает на нескольких базах. Кроме одной. На одной базе периодически происходят рассогласования SALDOTABLE и OVERTABLE. Всегда это выглядит так: по счету кредита сумма кредитового оборота (поле KR) в SALDOTABLE завышена и не соответствует OVERTABLE. В OVERTABLE нет записи, на ту сумму, которая есть в KR в SALDOTABLE .

Мне сейчас представляется такой ход событий:

Вставляется запись в OVERTABLE, это вызывает срабатывание триггера OVERTABLE_CALC он в свою очередь вызывает расчет по счету дебета процедурой CALCSALDO (место 1), затем по счету кредита (место 2).

Но при вызове расчета по счету дебета иногда возникает логическая ошибка в месте 3 (практически подтвержденно по профайлеру - сложно мониторить рабочую базу с большим потоком операций). И на основании этой ошибки вся транзакция откатывается. НО после отката остается запись по счету кредита в SALDOTABLE.

Такое ощущение, что несмотря на SET XACT_ABORT ON в триггере все-таки вызывается CALCSALDO (место2) но уже БЕЗ транзакции. И после отката всего остального запись в SALDOTABLE по такому движению остается.

В профайлере такжде иногда пробегают ошибки "Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing" но точно определить к чему они относятся мне не удалось. Возможно и к пересчету.

Это пока только мои догадки. Насколько они могут соответствовать реальности? Нужно ли и как правильно обработать выход из первого вызова CALCSALDO. Проверять на @@trancount = 0 ? Или @@error ?

Повторить ситуацию на тестовом сервере не могу. Есть только рабочая - там тестовые проводки делать не имею права - приходится пока разбираться теоритически.



Сервер 2005 SP2 версия 9,0,3073
Q
Дата: 23.01.2009 16:19:07
А внешние операции, собственно, вставляющие записи в таблицы, завернуты в транзакции? Если есть хоть одна операция, которая НЕ (ну, кто-то посчитал, что однооператорная транзакция итак откатится :) ), то, по крайней мере в 2000
BOL
The trigger continues executing any remaining statements after the ROLLBACK statement. If any of these statements modify data, the modifications are not rolled back. No nested triggers are fired by the execution of these remaining statements
.
В общем, "во избежание", я бы if @@error ... в подозрительные места вставил. Если это не слишком большой оверхед, конечно.
Alex S
Дата: 23.01.2009 16:25:14
Q, да внешние операции ВСЕГДА обернуты в транзакцию, правда в неявную SET IMPLICIT_TRANSACTIONS ON и в конце всегда будет commit если не было ексепшенов и if @@trancount > 0 rollback если они были
Alex S
Дата: 23.01.2009 16:27:32
Q
В общем, "во избежание", я бы if @@error ... в подозрительные места вставил. Если это не слишком большой оверхед, конечно.

Да, попробую, спасибо.
Ken@t
Дата: 23.01.2009 16:32:10
Вот ведь жесть вы себе устроили , сделайте материализованные представления ... и курсоры, что-то подсказывает они там не нужны.
Alex S
Дата: 23.01.2009 16:49:22
Ken@t
Вот ведь жесть вы себе устроили , сделайте материализованные представления ... и курсоры, что-то подсказывает они там не нужны.

В смысле Indexed View с запросом, рассчитывающим величины, которые сейчас в SALDOTABLE по OVERTABLE ?
daw
Дата: 23.01.2009 16:56:43

CREATE PROCEDURE dbo.CALCSALDO ...

ex:
SET nocount off
if @@trancount > 0 rollback

вот здесь у вас откатится транзакция, но при этом выполнение батча продолжится (да - несмотря на set xact_abort on - она
никак не затрагивает реакцию сервера на пользовательские ошибки). и проверка @@error после выполнения процедуры вам
никак не поможет - @@error на этот момент уже обнулится.
смотрите:
create table t1 (c int)
go
create table t2 (c int)
go
create proc p1 @c int
as
insert into t2 values (@c)
if @c < 100
begin
   raiserror('!', 16, 1)
   rollback tran
end
go
create trigger it_t1 on t1
for insert
as
set xact_abort on
exec p1 10
print @@error
exec p1 1000
print @@error
go
insert into t1 values (0)
go
select * from t1
select * from t2
go
drop proc p1
go
drop table t1
go
drop table t2

раз у вас 2005-ый, то спасти должно оборачивание в try ... catch ...
create table t1 (c int)
go
create table t2 (c int)
go
create proc p1 @c int
as
insert into t2 values (@c)
if @c < 100
begin
   raiserror('!', 16, 1)
   rollback tran
end
go
create trigger it_t1 on t1
for insert
as
set xact_abort on
begin try
   exec p1 10
   exec p1 1000
end try
begin catch
   print 'error!'
end catch
go
insert into t1 values (0)
go
select * from t1
select * from t2
go
drop proc p1
go
drop table t1
go
drop table t2
или возвращайте из процедуры с помощью return статус, с которым она выполнилась и проверяйте
его после вызова.

Posted via ActualForum NNTP Server 1.4

Q
Дата: 23.01.2009 17:36:33
просто окончание текста процедуры (и, вроде бы, RETURN тоже) сохраняют глобальное состояние @@error, можно этим пользоваться. Хотя, если есть возможность все переписать все на TRY, стОит это сделать.
Alex S
Дата: 23.01.2009 17:50:01
daw, спасибо!
Буду переписывать на return статус (тоже думал об этом, еще и по другим причинам) try catch не подойдет - нужно чтобы код работал на 2000 (есть инсталяции пока). Пример шикарный ) .
Crimean
Дата: 23.01.2009 17:50:47
ОФФ/2 - формирование IDENTITY имхо правильнее через табличные переменные сделать