SQL Server 在链接的 MS Access 表上插入后返回不同的记录

Posted

技术标签:

【中文标题】SQL Server 在链接的 MS Access 表上插入后返回不同的记录【英文标题】:SQL Server returns different record after insert on linked MS Access table 【发布时间】:2011-08-22 20:05:22 【问题描述】:

我们最近将后端数据库从 SQL Server 2000 升级到 SQL Server 2008。自从切换以来,我们遇到了间歇性(阅读:不可能持续重现)和奇怪的问题,但它们似乎都以某种方式相关。

在一种情况下,我们的用户通过绑定表单将新记录添加到表中。 保存记录后,会在其位置显示一条不同的(更旧的)记录。Shift+F9 强制重新查询表单的 带回新添加的记录(表单被过滤以仅显示一条记录)。

我们已经设法根据发生在不同表单上的日志记录来隔离问题的特定实例。在表单的更新前事件中,时间戳被正确填写在插入的记录上。在同一表格的更新后事件中,将在另一个表中创建历史记录,其中包括第一个表的自动编号 ID。 这些历史记录中约有十分之一是使用错误的自动编号 ID 创建的。

有没有人目睹过这种行为或对此有任何解释?

编辑:其他想法:

后端数据库是合并复制的一部分 Access 前端版本为 2000 和 2002(其他版本未经测试) 我读过一篇文章,建议 Access 在幕后使用 @@IDENTITY 从 SQL Server 取回新添加的记录 使用SQL Server ODBC 驱动程序和SQL Server Native Client 10.0 ODBC 驱动程序连接到后端表时出现问题 兼容性级别设置为 80(SQL Server 2000 级别兼容性)

编辑: SQL Profiler Trace 结果:

我运行 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。不过太糟糕了,因为这似乎是最简单的解决方法。

【问题讨论】:

“错误的自动编号 ID”是什么意思? 例如,一条新添加的记录的自动生成 ID 为 210272。在AfterUpdate 事件中,我们将该值插入到历史记录表中。所以历史表记录的值应该是 210272。但是,插入的值是 1077,这是一个更旧记录的自动 ID。 AutoNumber 对于 MS Access 就像 IDENTITY 对于 SQL Server 一样 根据给出的信息很难排除故障。但是,我可以想到两个地方: 1. 确保有问题的表有一个主键(并且 Access 知道它们)。 2. 确保您没有对排序顺序做出任何假设。 SQL Server 对行顺序提供零保证,除非您指定 Order By。 向合唱团讲道。使用我所拥有的信息进行故障排除非常困难。 1. 表确实有主键,Access 可以正确识别它们。 2. 没有关于排序顺序的假设。问题似乎与 MS Access 中的底层实现有关。我在另一个论坛上阅读了一篇文章,其中建议 Access 可能在幕后使用 @@IDENTITY 从 SQL Server 获取新插入的记录。我应该补充一点,Access 版本 2000 和 2002 中出现了问题。无法测试其他版本。 【参考方案1】:

使用 SCOPE_IDENTITY 代替 @@IDENTITY。

因为@@IDENTITY 返回最后一个 当前生成的标识值 会话,如果有一些触发器 当前操作的任何表 会话,我们将获得意想不到的价值。 为了得到所需的值, 请使用 SCOPE_IDENTITY。这 函数将返回插入的值 仅在当前范围内。

more

【讨论】:

+1 链接。它让我(和罗兰)指出了正确的方向。不幸的是,切换到使用 SCOPE_IDENTITY 不是一种选择,因为对 @@IDENTITY 的调用在 Access 中是硬编码的。 严格来说,它不是“在 Access 中硬编码”,而是在 Jet/ACE(或 ODBC 驱动程序——目前尚不清楚应该归咎于哪个)中。【参考方案2】:

环顾四周(主要是在 garik 包含为“更多”的链接之外),表明您对这种行为感到困惑——这是一个 Access/SQL Server 通信错误。 但是,this link 中描述了一种解决方法。

详细再现对我来说太复杂了,非常在那里解释得很好,但基本上你在启动触发器时将 @@IDENTITY 保存到变量,然后做一个虚假的 #temp 插入到将值欺骗回您希望最后返回的值。

【讨论】:

在我看来,用 sproc 进行插入并不是什么大问题,你不觉得吗? @David:问题是您不能使用绑定表单。 Access 依赖 @@IDENTITY 从 SQL Server 取回新插入的记录,但由于 @@IDENTITY 有据可查的缺点,这会导致问题。 在VB6下,它实际上比Access更适合我,我使用sprocs进行了all DB访问,并且没有使用绑定表单at全部。更清洁,恕我直言。是的,还有更多的代码要写/读,但你知道发生了什么。 @mwolfe02:我首先不会在绑定表单中添加大多数记录——我倾向于使用未绑定表单来收集必填字段,插入记录,然后加载结果新记录到主数据编辑/显示表单中。 SPROC 在这种情况下效果很好。 @David:您是否也对子表单使用相同的范例?如果是这样,你采取什么方法?将未绑定的字段放在父表单上以向子表单添加记录?【参考方案3】:

哇!那是多么痛苦的经历。首先,给 MS Access 团队做个简短的说明:

如果您还没有为 A2010 执行此操作,请花五分钟时间在您的代码库上进行查找和替换,将 SELECT @@IDENTITY 的每个实例替换为与 SQL Server 接口的SCOPE_IDENTITY()。这是很久以前修复开源项目的一个很好的例子......但我离题了。

对于这将是一篇非常冗长的帖子,我深表歉意。我将尝试记录我在过去一周中获得的所有知识,试图解决这个令人烦恼的问题。我原来的帖子总结了我看到的行为。请先阅读完整的上下文。

让我最适合这个问题的是它看似随机的性质。我们会做一个插入,它会失败。然后我们再做五十个,他们都会成功。然后第二天我们会做一个插入,它会再次失败。然后我们再做十个,他们都会成功。然后几个小时后,一个又会失败。等等。

问题是由合并复制引起的。当表作为文章添加到合并复制发布时,会自动生成多个触发器来管理复制。当我们在 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

这是上面代码 sn-p 中发生的事情。 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 表中插入新行。] 然后我们会再做 50 次,它们都会成功。 [genhistory 表中的行增加了。] 然后第二天 [在每晚(和每小时)合并代理运行并关闭 genhistory 表中的行] 我们会做一个插入,它会再次失败。 [在 genhistory 表中插入新行。] 然后我们再做十个,它们都会成功。 [genhistory 表中的行增加了。] 然后几个小时后 [在每小时合并代理运行并关闭了 genhistory 表中的行] 之后,又会失败。 [在 genhistory 表中插入新行。]

现在我们终于知道发生了什么,我们需要一些方法来解决它。 “正确”的方法是使用SCOPE_IDENTITY() 而不是SELECT @@IDENTITY。但是,这种行为是硬编码到 MS Access 中的,因此我们不得不在 SQL Server 中提供一种解决方法。 @Roland 提供的 This link 建议有三种解决方法(有关详细信息,请参阅链接)。

第三种解决方法似乎是最好的选择,但它需要多次关闭和重新启动整个数据库服务器以及作者自己的额外警告:

最后一句话(免责声明): 正如我已经说过的,微软完全不支持此修补程序。对于因使用或无法使用本文档或其中包含的任何材料,或因任何行动或决定而造成的任何损害(包括但不限于业务损失或利润损失),本人概不负责由于使用本文档或任何此类材料而采取的。我在我的环境中测试了这个过程,解决了问题并按预期工作。我鼓励您首先在虚拟化/测试环境中进行自己的测试。在应用将来可能出现的任何新的 SQL Server Service Pack 之前,我建议停止服务器,恢复我们在步骤 1 中复制的原始文件,然后按照 SP 的说明进行操作。应用 SP 后(也在订阅者上),您可以重新启动发布并查看 MS Access 错误是否再次出现。如果是这样,我想您可以重新修补 SP 可能已设置的新资源数据库。

....因此,在启动和停止整个数据库服务器之后,我需要对核心 SQL Server 行为进行一些不受支持的更改,并记得在应用未来的服务包之前撤销这些更改并重新应用它们。嗯,不用了,谢谢。

我重读了几遍文章,意识到关键信息是this trick,以保存和恢复@@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)

作者的方法是修改基础合并插入触发器,以便自动生成的触发器是在上述行已经到位的情况下创建的。这仅涉及编辑一个地方,但有已经提到的缺点。我的方法是在创建触发器后对其进行修改。唯一的缺点是您必须在更多的地方执行此操作(即,每个表一次)。但如果你能以某种方式编写脚本......

让这一切发挥作用的最后一个难题是弄清楚所有当前的触发器是什么。这需要使用两个系统视图:triggerssyscomments。我使用triggers 视图来识别有问题的触发器(它们的名称都以“MSmerge_ins”开头)。然后,我使用syscomments 视图获取用于创建每个触发器的 T-SQL。 syscomments.text 字段的大小为 4000。如果 T-SQL 超过 4000 个字符,它将被拆分为按 syscomments.colid 排序的几行。

我的最终算法如下:

    循环遍历每个“MSMerge_ins”触发器 从 syscmets 视图重建 CREATE TRIGGER T-SQL 通过正则表达式运行 CREATE TRIGGER T-SQL 如果需要修改触发器,正则表达式会返回 ALTER TRIGGER 语句 如果触发器已被修改,则正则表达式返回未更改的 T-SQL 如果返回 ALTER TRIGGER 语句,它将作为修改触发器的传递查询执行

每当我创建(或重新创建)合并复制发布时,我仍然必须记住运行此代码。我的方法的另一个缺点是我的正则表达式可能需要修改以处理对自动生成的触发器的未来更改。但是我可以在实时服务器上做到这一点,我不必关闭任何东西,而且我不会为我可能会破坏核心功能而苦恼。自己决定什么可以忍受,什么不能忍受。

我写这个是为了在 MS Access 中运行。为了简化代码,我创建了指向triggerssyscommentssys_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

【讨论】:

这是一个很好的答案。全面且解释清楚。 我仍然认为所有这些都可以通过不使用绑定表单来添加记录来解决,而是使用返回 SCOPE_IDENTITY() 而不是 @@IDENTITY 的 SPROC 添加。 一个小问题:使用@@IDENTITY 而不是 SCOPE_IDENTITY() 的不是 Access,而是 Jet/ACE。确实,应该归咎于 SQL Server ODBC 驱动程序。我认为一个编写良好的 SQL Server ODBC 驱动程序要么默认为 SCOPE_IDENTITY() 要么允许这样做。 我想知道是否真的应该归咎于 SQL Server ODBC 驱动程序。如果真是这样,那就更不可原谅了。特别是因为当通过SQL Server Native Client 10.0 ODBC 驱动程序(即 SQL Server 2008 的本机客户端驱动程序)连接表时使用了@@IDENTITY

以上是关于SQL Server 在链接的 MS Access 表上插入后返回不同的记录的主要内容,如果未能解决你的问题,请参考以下文章

重新定义 MS Access 文件和 SQL Server 之间的数据链接

MS Access:来自 SQL Server 的只读链接表?

SQL Server 在链接的 MS Access 表上插入后返回不同的记录

迁移到新的 SQL Server 后,在 MS Access 中更新链接表的最佳方法是啥?

MS Access 2003 + 到 SQL Server 2005 的链接表 + Windows 身份验证 = 慢

通过链接到SQL Server数据库的MS Access以多对多关系插入数据(中间表)