在链接的 MS Access 表上插入后,SQL Server 返回不同的记录
我们最近将后端数据库从 SQL Server 2000 升级到 SQL Server 2008。自从切换以来,我们遇到了间歇性(读取:不可能一致重现)和奇怪的问题,但它们似乎都以某种方式相关。
在一种情况下,我们的用户通过绑定表单向表添加一条新记录。 保存记录后,就会在其位置显示不同的(更旧的)记录。按 Shift+F9 强制重新查询表单的 会返回新添加的记录(表单被过滤以仅显示单个记录)。
我们已经成功地根据不同表单上发生的日志记录来隔离问题的特定实例。在表单的 BeforeUpdate 事件中,在插入的记录上正确填写了时间戳。在同一表单的 AfterUpdate 事件中,会在另一个表中创建历史记录,其中包含第一个表的自动编号 ID。 大约十分之一的历史记录是使用错误的自动编号 ID 创建的。
有人目睹过这种行为或对此有任何解释吗?
编辑:其他想法:
- 后端数据库是合并复制的一部分,
- Access前端版本是2000和2002(其他版本未测试)
- 我读过的一篇文章建议Access使用
@@IDENTITY
在后台从 SQL Server 获取新添加的记录时, - 使用
{SQL Server}
ODBC 驱动程序和{SQL Server Native Client 10.0}
会出现问题code> 连接到后端表的 ODBC 驱动程序 - 兼容性级别设置为 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。但太糟糕了,因为这似乎是最简单的解决办法。
We recently upgraded our backend database from SQL Server 2000 to SQL Server 2008. Since the switch we've had intermittent (read: impossible to consistently reproduce) and strange problems, but they all seem to be related somehow.
In one case, our users add a new record to a table via a bound form. As soon as the record is saved, a different (much older) record is displayed in its place. Pressing Shift+F9 to force a requery of the form brings back the newly added record (the form is filtered to show only a single record).
We have managed to isolate a specific instance of the problem based on logging that occurs on a different form. In the BeforeUpdate event of the form a timestamp is correctly filled in on the record being inserted. In the AfterUpdate event of the same form a history record is created in another table that includes the Autonumber ID of the first table. About 1 in 10 of these history records is created with the wrong Autonumber ID.
Has anyone witnessed this sort of behavior or have any explanation for it?
EDIT: Additional thoughts:
- the backend database is part of a merge replication
- the Access front-end versions are 2000 and 2002 (other versions not tested)
- one post I read suggested Access uses
@@IDENTITY
behind the scenes to get the newly added record back from SQL Server - the problem occurs using both the
{SQL Server}
ODBC driver and the{SQL Server Native Client 10.0}
ODBC driver to connect to the backend table - compatibility level is set to 80 (SQL Server 2000 level compatibility)
EDIT: SQL Profiler Trace results:
I ran SQL Profiler and confirmed that Access is indeed using SELECT @@IDENTITY
behind the scenes to return the newly inserted record. I confirmed this is happening with MS Access 2000, 2002 (XP), and 2007 front-ends. It is also happening whether the tables are linked using the {SQL Server}
ODBC Driver or the {SQL Server Native Client 10.0}
ODBC driver.
I should emphasize that Access is using SELECT @@IDENTITY
behind the scenes. As far as I know there is no way to force Access to use SCOPE_IDENTITY
. Too bad, though, because that seems like it would be the simplest fix.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
使用 SCOPE_IDENTITY 而不是 @@IDENTITY。
更多
Use SCOPE_IDENTITY instead of @@IDENTITY.
more
环顾四周(主要是从 garik 包含的“更多”链接中删除),表明您陷入了这种行为——这是一个 Access/SQL Server 通信错误。 但是,此链接中描述了一种解决方法。
这对我来说太复杂了,无法详细重现,而且非常在那里解释得很好,但基本上你在开始触发器时将 @@IDENTITY 保存到变量,然后做一个假的
#temp
插入以将值欺骗回您想要最后返回的值。A bit of looking around (mostly off the link included as "more" by garik), shows that you're stuck with the behavior--it's an Access/SQL Server communication bug. However, there's a workaround described at this link.
It's way too complicated for me to reproduce in detail, and very well explained there, but basicly you save @@IDENTITY to variable at the start trigger, then do a phony
#temp
insert to spoof the value back to what you want returned at the end.哇!那是多么痛苦的经历啊。首先,向 MS Access 团队简要说明一下:
对于这篇非常冗长的文章,我先表示歉意。我将尝试记录过去一周所获得的所有知识,试图解决这个棘手的问题。我原来的帖子总结了我所看到的行为。请先阅读该内容以获取完整的上下文。
最适合我解决这个问题的是它看似随机的性质。我们会进行插入,但它会失败。然后我们再做五十个,他们都会成功。然后第二天我们再做一次插入,结果又失败了。然后我们再做十次,他们都会成功。然后几个小时后又会失败。不断地。
该问题是由合并复制引起的。当表作为项目添加到合并复制发布时,会自动生成多个触发器来管理复制。当我们在 SQL Server 2000 中使用合并复制时,这对我们来说不是问题。但是,从 SQL Server 2005 开始,这些触发器被修改了。导致问题的具体修改在于插入中自动生成的这些代码行受影响表的触发器:
以下是上面代码片段中发生的情况。 SQL Server 正在检查 MSmerge_genhistory 表(
MSmerge_genvw_8D1ADB4453634BF39DA4AA582FE18F78
是该表的系统视图)。如果存在打开行 (genstatus = 0
),并且插入次数加上更改计数不超过 1000,则计数器会递增。 但是,如果没有打开的行,则会插入新行。这会重置@@IDENTITY
变量。集体歇斯底里随之而来。猫和狗,住在一起。等等。需要明确的是,这里的错误在于使用
@@IDENTITY
的Access团队,而不是修改合并复制内部的SQL Server团队。但是,天啊,我以为你们在为同一支球队效力……值得注意的是,合并复制涉及的每个表都有不同的行。我发现以下查询对于故障排除和了解 genhistory 表中发生的情况最有帮助:
那么什么关闭了 genhistory 表中的一行?嗯,除其他外,每当合并代理针对数据库运行时,genhistory 表中的每一行都会关闭。任何合并代理。对于任何出版物。在我们的例子中,我们有两个独立的合并复制发布,它们在同一个数据库中运行。一个合并代理每小时运行一次;另一个每晚运行。
这让我们回到了看似随机的行为。我将注释我之前的段落来解释该行为:
现在我们终于知道发生了什么,我们需要一些方法来修复它。 “正确”的方法是使用
SCOPE_IDENTITY()
而不是SELECT @@IDENTITY
。然而,这种行为被硬编码到 MS Access 中,因此我们不得不在 SQL Server 中采取解决方法。第三种解决方法似乎是最好的选择,但它需要多次关闭和重新启动整个数据库服务器以及作者自己的额外警告:
....因此,在启动和停止整个数据库服务器之后,我需要对核心 SQL Server 行为进行一些不受支持的更改,并记住在应用未来的服务包之前取消这些更改并重新应用它们。嗯,不用了,谢谢。
我重新阅读了这篇文章几次,意识到关键信息是 这个技巧可以保存和恢复
@@IDENTITY
值。我意识到我需要做的就是将这四行应用到每个合并插入触发器:作者的方法是修改基本合并插入触发器,以便使用已经就位的上述行创建自动生成的触发器。这仅涉及编辑一处,但具有已经提到的缺点。我的方法是在创建触发器后对其进行修改。唯一的缺点是您必须在更多地方执行此操作(即每个表一次)。但如果你能以某种方式编写脚本......
让这一切发挥作用的最后一个难题是弄清楚当前所有的触发器是什么。这需要使用两个系统视图:
triggers
和syscomments
。我使用triggers
视图来识别有问题的触发器(它们的名称均以“MSmerge_ins”开头)。然后,我使用 syscomments 视图获取用于创建每个触发器的 T-SQL。syscomments.text
字段的大小为 4000。如果 T-SQL 超过 4000 个字符,它将被分成按syscomments.colid
排序的多行。我的最终算法如下:
ALTER TRIGGER
语句每当我创建(或重新创建)合并复制发布时,我仍然必须记住运行此代码。我的方法的另一个缺点是我的正则表达式可能需要修改以处理自动生成的触发器的未来更改。但我可以在实时服务器上做到这一点,我不必关闭任何东西,而且我也不会担心我可能会破坏核心功能。自己决定什么可以忍受,什么不能忍受。
我写这个是为了在 MS Access 中运行。为了简化代码,我创建了指向
triggers
、syscomments
和sys_tables
视图的链接。sys_tables
视图并不是绝对必要的,但我将其保留用于调试。这是代码:
Whew! What a harrowing experience that was. First off, a quick note to the MS Access team:
My apologies upfront for what will be a very lengthy post. I'm going to try and record all of the knowledge I've gained over the past week trying to fix this vexing problem. My original post summed up the behavior I was seeing. Please read that first for full context.
The thing that gave me the most fits with this problem was its seemingly random nature. We would do an insert and it would fail. Then we would do fifty more and they would all succeed. Then the next day we would do an insert and it would fail again. We'd then do ten more and they would all succeed. Then a few hours later one would fail again. And on and on.
The problem was being caused by merge replication. When a table gets added as an article to a merge replication publication, several triggers are automatically generated to manage the replication. This was not a problem for us when we used merge replication in SQL Server 2000. However, these triggers were modified starting with SQL Server 2005. The specific modification that caused the problem is in these lines of code that were auto-generated in the insert trigger for the affected tables:
Here's what's going on in the above code snippet. SQL Server is checking to see if there is an open row in the MSmerge_genhistory table (
MSmerge_genvw_8D1ADB4453634BF39DA4AA582FE18F78
is a system view into that table). If there is an open row (genstatus = 0
) and the number of inserts plus the change count does not exceed 1000 then the counter gets incremented. But if there is not an open row a new one gets inserted. Which resets the@@IDENTITY
variable. Mass hysteria ensues. Cats and dogs, living together. Etc, etc.To be clear, the fault here is with the Access team for using
@@IDENTITY
, not the SQL Server team for modifying the internals of merge replication. But, geez, I thought you guys were playing for the same team...It's worth noting that there is a different row for every table involved in merge replication. I found the following query most helpful for troubleshooting and understanding what's going on in the genhistory table:
So what closes a row in the genhistory table? Well, among other things, every row in the genhistory table is closed whenever a merge agent runs against the database. Any merge agent. For any publication. In our case, we have two separate merge replication publications that run out of the same database. One merge agent runs hourly; the other runs nightly.
This brings us back to the seemingly random behavior. I'll annotate my earlier paragraph to explain the behavior:
Now that we finally know what's going on we need some way to fix it. The "right" way would be to use
SCOPE_IDENTITY()
instead ofSELECT @@IDENTITY
. However, that behavior is hard-coded into MS Access so we're forced to put a workaround in SQL Server. This link provided by @Roland suggests there are three workarounds (see the link for full details).The third workaround seemed like the best option, but it required multiple shutdowns and restarts of the entire database server plus the author's own additional caveats:
....So after starting and stopping the entire database server I need to make some unsupported changes to core SQL Server behavior plus remember to back out those changes and re-apply them before applying future service packs. Ummm, no thanks.
I re-read the article several times and realized the key piece of information was this trick to save and restore the
@@IDENTITY
value. I realized all I needed to do was apply these four lines to every merge insert trigger:The author's approach was to modify the base merge insert trigger so that the auto-generated triggers were created with the above lines already in place. This involves editing only one place, but has the drawbacks already mentioned. My approach was to modify the triggers after they were already created. The only drawback is that you have to do it in a lot more places (ie, once for every table). But if you could script it somehow.....
The final piece of the puzzle to get it all to work was figuring out what all the current triggers were. This required the use of two system views:
triggers
andsyscomments
. I used thetriggers
view to identify the offending triggers (their names all start with 'MSmerge_ins'). I then used thesyscomments
view to get the T-SQL used to create each trigger. Thesyscomments.text
field has a size of 4000. If the T-SQL exceeds 4000 characters, it gets split into several rows ordered by thesyscomments.colid
.My final algorithm goes as follows:
ALTER TRIGGER
statementALTER TRIGGER
statement is returned it gets executed as a pass-through query that modifies the triggerI still have to remember to run this code whenever I create (or re-create) a merge replication publication. Another drawback to my approach is that my regular expression may need to be modified to handle future changes to the automatically generated triggers. But I can do it on the live server, I don't have to shut anything down, and I'm not agonizing over what I might break in the core functionality. Decide for yourself what you can and can't live with.
I wrote this to run in MS Access. To simplify the code, I created links to the
triggers
,syscomments
, andsys_tables
views. Thesys_tables
view is not strictly necessary but I left it in for debugging.Here's the code: