SQL Server возвращает другую запись после вставки в связанную таблицу MS Access - PullRequest
2 голосов
/ 11 мая 2011

Недавно мы обновили нашу внутреннюю базу данных с SQL Server 2000 до SQL Server 2008. С момента переключения у нас были периодические (читай: невозможно воспроизвести последовательно) и странные проблемы, но все они как-то связаны.

В одном случае наши пользователи добавляют новую запись в таблицу через связанную форму. Как только запись сохранена, на ее месте отображается другая (намного более старая) запись. Нажатие Shift + F9 для принудительного запроса формывозвращает вновь добавленную запись (форма фильтруется так, чтобы показывать только одну запись).

Нам удалось выделить конкретный случай проблемы на основе ведения журнала в другой форме.В событии BeforeUpdate формы метка времени корректно заполняется при вставке записи.В событии AfterUpdate той же формы в другой таблице создается запись истории, которая содержит идентификатор Autonumber для первой таблицы. Приблизительно 1 из 10 этих записей истории создается с неправильным идентификатором Autonumber.

Кто-нибудь был свидетелем такого поведения или у него есть какое-либо объяснение?

РЕДАКТИРОВАТЬ: Дополнительные мысли:

  • Бэкэнд-база данных является частью репликации слиянием
  • Внешние версии Access - 2000 и 2002 (другие версии не тестировались)
  • один пост, который я прочитал, предложил Access использует @@IDENTITY за кулисами, чтобы получить вновь добавленную запись из SQL Server
  • проблема возникает с использованием драйвера {SQL Server} ODBC и {SQL Server Native Client 10.0}Драйвер ODBC для подключения к бэкэнд-таблице * Уровень совместимости 1031 *
  • установлен равным 80 (уровень совместимости с SQL Server 2000)

РЕДАКТИРОВАТЬ: Результаты трассировки SQL Profiler:

Я запустил SQL Profiler и подтвердил, что Access действительно использует SELECT @@IDENTITY за кулисами для возврата вновь вставленной записи.Я подтвердил, что это происходит с интерфейсами MS Access 2000, 2002 (XP) и 2007.Также происходит, связаны ли таблицы с помощью драйвера {SQL Server} ODBC или драйвера {SQL Server Native Client 10.0} ODBC.

Я должен подчеркнуть, что Access использует SELECT @@IDENTITY за сценой .Насколько я знаю, нет способа заставить Access использовать SCOPE_IDENTITY.Слишком плохо, хотя, потому что кажется, что это будет самое простое решение.

Ответы [ 3 ]

3 голосов
/ 11 мая 2011

Используйте SCOPE_IDENTITY вместо @@ IDENTITY.

Поскольку @@ IDENTITY возвращает последние значения идентификаторов, сгенерированные в текущем сеансе, если в любых таблицах, которыми манипулируют в текущем сеансе, есть триггеры, мы получимнеожиданное значение.Чтобы получить требуемое значение, используйте SCOPE_IDENTITY.Эта функция будет возвращать значение, вставленное только в пределах текущей области.

more

2 голосов
/ 16 мая 2011

Уф! Какой это был мучительный опыт. Прежде всего, краткое примечание для команды 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.

Мой последний алгоритм работает следующим образом:

  1. Перебирать каждый триггер 'MSMerge_ins'
  2. Перестраивать CREATE TRIGGER T-SQL из представления syscomments
  3. Запустить CREATE TRIGGER T-SQL с помощью регулярного выражения
  4. Если триггер необходимо изменить, регулярное выражение возвращает оператор ALTER TRIGGER
  5. Если триггер уже был изменен, регулярное выражение возвращает T-SQL без изменений
  6. Если 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
2 голосов
/ 11 мая 2011

Немного оглядываясь по сторонам (в основном за счет ссылки, включенной как «more» от garik), показывает, что вы застряли в поведении - это ошибка связи Access / SQL Server. Однако , есть обходной путь, описанный на по этой ссылке .

Это слишком сложно для меня, чтобы воспроизвести в деталях, и очень хорошо объяснено там, но в основном вы сохраняете @@ IDENTITY в переменную при запуске триггера, а затем делаете фальшивую #temp вставку, чтобы подделать значение обратно к тому, что вы хотите вернуть в конце.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...