Уф! Какой это был мучительный опыт. Прежде всего, краткое примечание для команды MS Access:
Если вы еще не сделали этого для A2010, потратьте пять минут и найдите и замените кодовую базу, заменив каждый экземпляр SELECT @@IDENTITY
на SCOPE_IDENTITY()
, где он взаимодействует с SQL Server. Это хороший пример того, где проект с открытым исходным кодом был бы исправлен давно ... Но я отвлекся.
Мои искренние извинения за то, что это будет очень длинный пост. Я попытаюсь записать все знания, которые я получил за последнюю неделю, чтобы решить эту неприятную проблему. В моем оригинальном посте я описал поведение, которое я видел. Пожалуйста, прочтите это сначала для полного контекста.
То, что дало мне наибольшее удовлетворение этой проблемой, было, казалось бы, случайным характером. Мы сделаем вставку, и она потерпит неудачу. Тогда мы сделаем еще пятьдесят, и все они добьются успеха. Затем на следующий день мы вставим вставку, и она снова не удастся. Затем мы сделали бы еще десять, и все они преуспели бы. Затем через несколько часов снова произойдет сбой. И так далее.
Проблема была вызвана репликацией слиянием. Когда таблица добавляется как статья в публикацию репликации слиянием, автоматически генерируются несколько триггеров для управления репликацией. Это не было проблемой для нас, когда мы использовали репликацию слиянием в SQL Server 2000. Однако эти триггеры были изменены, начиная с SQL Server 2005. Конкретное изменение, вызвавшее проблему, заключается в этих строках кода, которые были автоматически сгенерированы при вставке. триггер для затронутых таблиц:
select @newgen = NULL
select top 1 @newgen = generation from [dbo].[MSmerge_genvw_8D1ADB4453634BF39DA4AA582FE18F78] with (rowlock, updlock, readpast)
where art_nick = 14201004 and genstatus = 0
and changecount <= (1000 - isnull(@article_rows_inserted,0))
if @newgen is NULL
begin
insert into [dbo].[MSmerge_genvw_8D1ADB4453634BF39DA4AA582FE18F78] with (rowlock)
(guidsrc, genstatus, art_nick, nicknames, coldate, changecount)
values (newid(), 0, @tablenick, @nickbin, @dt, @article_rows_inserted)
select @error = @@error, @newgen = @@identity
if @error<>0 or @newgen is NULL
goto FAILURE
end
else
begin
-- now update the changecount of the generation we go to reflect the number of rows we put in this generation
update [dbo].[MSmerge_genvw_8D1ADB4453634BF39DA4AA582FE18F78] with (rowlock)
set changecount = changecount + @article_rows_inserted
where generation = @newgen
if @@error<>0 goto FAILURE
end
Вот что происходит в приведенном выше фрагменте кода. SQL Server проверяет, есть ли открытая строка в таблице MSmerge_genhistory (MSmerge_genvw_8D1ADB4453634BF39DA4AA582FE18F78
- это системное представление этой таблицы). Если есть открытая строка (genstatus = 0
) и количество вставок плюс количество изменений не превышает 1000, то счетчик увеличивается. Но если открытой строки нет, вставляется новая. Что сбрасывает переменную @@IDENTITY
. Массовая истерия наступает. Кошки и собаки живут вместе. И т. Д.
Для ясности, ошибка здесь в команде Access для использования @@IDENTITY
, а не в команде SQL Server для изменения внутренних компонентов репликации слиянием. Но, черт возьми, я думал, что вы, ребята, играете за одну команду ...
Стоит отметить, что для каждой таблицы, участвующей в репликации слиянием, есть разные строки. Я нашел следующий запрос наиболее полезным для устранения неполадок и понимания того, что происходит в таблице genhistory:
SELECT A.name
,H.generation
,H.art_nick
,H.coldate
,H.genstatus
,H.changecount
FROM [MSmerge_genhistory] AS H
INNER JOIN [sysmergearticles] AS A
ON H.art_nick = A.nickname
ORDER BY H.generation DESC
Так что же закрывает строку в таблице родословных? Ну, между прочим, каждая строка в таблице genhistory закрывается всякий раз, когда агент слияния работает с базой данных. Любой агент слияния. Для любой публикации. В нашем случае у нас есть две отдельные публикации репликации слиянием, которые заканчиваются в одной базе данных. Один агент слияния работает ежечасно; другой бежит ночью.
Это возвращает нас к, казалось бы, случайному поведению. Я объясню свой предыдущий абзац, чтобы объяснить поведение:
Мы сделаем вставку, и она потерпит неудачу. [ Новая строка вставлена в таблицу родословных. ] Тогда мы сделаем еще пятьдесят, и все они добьются успеха. [ Строка в таблице genhistory увеличена. ] Затем на следующий день [ после ночного (и ежечасного) агента слияния запустится и закроет строку в таблице genhistory ], мы сделаем вставку и это снова не получится. [ Новая строка вставлена в общую таблицу. ] Затем мы выполнили бы еще десять, и все они преуспели бы. [ Строка в таблице genhistory увеличена. ] Затем, спустя несколько часов [ после того, как почасовой агент слияния запустил и закрыл строку в таблице genhistory ], снова произойдет сбой. [ Новая строка вставлена в общую таблицу. ]
Теперь, когда мы наконец знаем, что происходит, нам нужен какой-то способ это исправить.«Правильный» способ - использовать SCOPE_IDENTITY()
вместо SELECT @@IDENTITY
.Однако это поведение жестко запрограммировано в MS Access, поэтому мы вынуждены обойти SQL Server. Эта ссылка , предоставленная @Roland, предлагает три обходных пути (подробности см. По ссылке).
Третий обходной путь показался лучшим вариантом, но он требовал многократных остановок и перезапусков всего сервера базы данных, а также дополнительных замечаний автора:
Несколько заключительных слов (Отказ от ответственности): Как я уже говорил, Microsoft не поддерживает эту процедуру исправления.Я не несу ответственности за какой-либо ущерб (включая, помимо прочего, ущерб от потери бизнеса или упущенной выгоды), возникший в результате использования или невозможности использования этого документа или любых содержащихся в нем материалов, а также любых действий или решенийприняты в результате использования этого документа или любого такого материала.Я протестировал эту процедуру в своей среде, решил проблему и работал как ожидалось.Я рекомендую вам сначала провести свой собственный тест в виртуализированной среде.Перед применением любого нового пакета обновления SQL Server, который может возникнуть в будущем, я предлагаю остановить сервер, восстановить исходные файлы, которые мы скопировали на шаге 1 процедуры, а затем следовать инструкциям SP.После применения SP (также для подписчиков) вы можете переустановить публикации и посмотреть, не появляется ли снова ошибка MS Access.Если это так, то я полагаю, что вы могли бы заново установить новую базу данных ресурсов, которую мог установить SP.
.... Так что после запуска и остановки всего сервера базы данных мне нужно сделать нескольконеподдерживаемые изменения в поведении ядра SQL Server, а также не забудьте отменить эти изменения и повторно применить их перед применением будущих пакетов обновления.Хммм, нет, спасибо.
Я перечитал статью несколько раз и понял, что ключевой информацией является этот трюк , чтобы сохранить и восстановить значение @@IDENTITY
.Я понял, что все, что мне нужно было сделать, это применить эти четыре строки к каждому триггеру вставки слияния:
declare @identity int, @strsql varchar(128)
set @identity=@@identity
set @strsql='select identity (int, ' + cast(@identity as varchar(10)) + ',1) as id into #tmp'
execute (@strsql)
Авторский подход состоял в том, чтобы модифицировать базовый триггер вставки слияния таким образом, чтобы автоматически сгенерированные триггеры создавались с помощью вышелинии уже на месте.Это включает в себя редактирование только одного места, но имеет уже упомянутые недостатки.Мой подход состоял в том, чтобы изменить триггеры после того, как они уже были созданы. Единственный недостаток - это то, что вы должны делать это в гораздо большем количестве мест (т. Е. Один раз для каждой таблицы).Но если бы вы могли как-то написать сценарий .....
Последней частью головоломки, чтобы заставить все это работать, было выяснение того, какими были все текущие триггеры.Это потребовало использования двух системных представлений: triggers
и syscomments
.Я использовал представление triggers
, чтобы идентифицировать нарушающие триггеры (все их имена начинаются с «MSmerge_ins»).Затем я использовал представление syscomments
, чтобы получить T-SQL для создания каждого триггера.Поле syscomments.text
имеет размер 4000. Если T-SQL превышает 4000 символов, оно разбивается на несколько строк, упорядоченных по syscomments.colid
.
Мой последний алгоритм работает следующим образом:
- Перебирать каждый триггер 'MSMerge_ins'
- Перестраивать CREATE TRIGGER T-SQL из представления syscomments
- Запустить CREATE TRIGGER T-SQL с помощью регулярного выражения
- Если триггер необходимо изменить, регулярное выражение возвращает оператор
ALTER TRIGGER
- Если триггер уже был изменен, регулярное выражение возвращает T-SQL без изменений
- Если
ALTER TRIGGER
оператор возвращается, он выполняется как сквозной запрос, который изменяет триггер
Мне все еще нужно помнить, чтобы запускать этот код всякий раз, когда я создаю (или заново создаю) публикацию репликации слиянием. Другим недостатком моего подхода является то, что моё регулярное выражение, возможно, потребуется изменить, чтобы обрабатывать будущие изменения автоматически сгенерированных триггеров. Но я могу сделать это на живом сервере, мне не нужно ничего отключать, и я не мучаюсь тем, что я могу нарушить в основной функциональности. Решите для себя, что вы можете и не можете жить.
Я написал это для запуска в MS Access. Чтобы упростить код, я создал ссылки на представления triggers
, syscomments
и sys_tables
. Представление sys_tables
не является строго необходимым, но я оставил его для отладки.
Вот код:
Sub FixInsertMergeTriggers()
Dim SQL As String, TriggerSQL As String, AlterSQL As String
Dim PrevTrigger As String, ProcessTrigger As Boolean
SQL = "SELECT Ta.Name AS TblName, Tr.Name AS TriggerName, " & _
" C.Text, C.Number, C.colid " & _
"FROM (syscomments AS C " & _
"INNER JOIN triggers AS Tr ON C.id=Tr.object_id) " & _
"INNER JOIN sys_tables AS Ta ON Tr.parent_id=Ta.object_id " & _
"WHERE Tr.name like 'MSmerge_ins*' " & _
"ORDER BY Tr.name, C.colid"
With CurrentDB.OpenRecordset(SQL)
Do
If .EOF Then
If Len(PrevTrigger) > 0 Then
ProcessTrigger = True
Else
Exit Do
End If
Else
ProcessTrigger = (!TriggerName <> PrevTrigger)
End If
If ProcessTrigger Then
If Len(TriggerSQL) > 0 Then
AlterSQL = ModifyTrigger(TriggerSQL)
If AlterSQL <> TriggerSQL Then
ExecPT AlterSQL
Debug.Print !TblName; " insert trigger altered"
End If
End If
TriggerSQL = ""
If .EOF Then Exit Do
End If
TriggerSQL = TriggerSQL & !Text
PrevTrigger = !TriggerName
.MoveNext
Loop
End With
Debug.Print "Done."
End Sub
Private Function ModifyTrigger(TriggerSQL As String) As String
Const DeclarationSection As String = " declare @identity int, @strsql varchar(128)" & vbCrLf & _
" set @identity=@@identity"
Const ExecuteSection As String = " set @strsql='select identity (int, ' + cast(@identity as varchar(10)) + ',1) as id into #tmp'" & vbCrLf & _
" execute (@strsql)"
Dim P As String 'variable that holds our regular expression pattern'
'Use regular expression to modify the trigger'
P = P & "(.*)" '1. The beginning'
P = P & "(create trigger)" '2. Need to change 'CREATE' to 'ALTER''
P = P & "(.*$)" '3. Rest of the first line'
P = P & "(^\s*declare\s*@is_mergeagent)" '4. First declaration line'
P = P & "([\s\S]*)" '5. The middle part'
P = P & "(if\s*@@error[\s\S]*)" '6. The lines after ...'
P = P & "(FAILURE:[\s\S]*)" '7. ... where we add our workaround'
ModifyTrigger = RegExReplace(P, TriggerSQL, _
"$1ALTER trigger$3" & vbCrLf & _
DeclarationSection & vbCrLf & _
"$4$5" & vbCrLf & _
ExecuteSection & vbCrLf & vbCrLf & _
"$6$7", , True, True)
End Function
Private Function RegExReplace(SearchPattern As String, TextToSearch As String, ReplacePattern As String, _
Optional GlobalReplace As Boolean = True, _
Optional IgnoreCase As Boolean = False, _
Optional MultiLine As Boolean = False) As String
Dim RE As Object
Set RE = CreateObject("vbscript.regexp")
With RE
.MultiLine = MultiLine
.Global = GlobalReplace
.IgnoreCase = IgnoreCase
.Pattern = SearchPattern
End With
RegExReplace = RE.Replace(TextToSearch, ReplacePattern)
End Function
'Execute pass-through SQL'
Private Sub ExecPT(SQL As String, Optional DbName As String = "MyDB")
Const QName As String = "TemporaryPassThroughQuery"
Dim qdef As DAO.QueryDef
On Error Resume Next
CurrentDB.QueryDefs.Delete QName
On Error GoTo 0
Set qdef = CurrentDB.CreateQueryDef(QName)
qdef.Connect = "ODBC;Driver={SQL Server};Server=myserver;database=" & DbName & ";Trusted_Connection=Yes;"
qdef.SQL = SQL
qdef.ReturnsRecords = False
CurrentDB.QueryDefs(QName).Execute
End Sub