当子表单绑定到特定存储过程时,为什么我无法加载它?
Why am I unable to load a subform when it is bound to a specific stored procedure?
总结
在 Microsoft Access 2010 数据库 (accdb) 中,我有一个表单可以将其他表单动态加载到主表单上的子表单对象中。子窗体对象中使用的窗体绑定到 ODBC 传递查询,该查询对 return 记录集执行存储过程。我不明白为什么我可以使用一个存储过程并且它工作得很好,但是如果我将表单绑定到另一个存储过程,它就无法加载子表单。
技术演练
我有两个直通 ODBC 查询。 qryGood
和 qryBad
。它们使用相同的 ODBC 连接字符串 (ODBC;DRIVER=SQL Server;SERVER=MyServer;UID=MyUser;Trusted_Connection=Yes;DATABASE=MyDatabase
),它们后面的 SQL 是相同的,但指向 SQL 2012 数据库服务器上的两个不同的 SQL 存储过程。
qryGood 来源:exec spGoodProc 123456
qryBad 来源:exec spBadProc 123456
存储过程后面的SQL很简单。 Return 条来自单个 table 的记录,按作为参数传递的 ID 进行过滤。 (有些人会做更复杂的事情,但我在这里只关注一个演示问题的简化示例。)
frmMySubform
的RecordSource
属性设置为qryBad
。
子表单 SourceObject
通过 VBA 代码设置:sfrmMain.SourceObject = "frmMySubform"
此时不会抛出任何错误。虽然 SourceObject
属性 现在 return 是 frmMySubform
,但 .Form
对象似乎没有设置。
然后我尝试在子表单上引用 属性:Debug.Print sfrmMain.Form.Name
失败并出现错误 2467:您输入的表达式引用了一个已关闭或未关闭的对象'不存在。
然后我可以在设计视图中打开 frmMySubform
,将 RecordSource
属性 更改为 qryGood
,它工作正常。这似乎指向 spBadProc
的问题,只有在子表单上用作 RecordSource
时才会出现。
我试过的
为了解决这个问题,我使用排除法尽可能地缩小了范围,但我仍然不明白为什么一个存储过程有效而另一个存储过程无效。 return 记录在 SQL 和 运行 直接传递查询时都很好。直接打开表单时两者都可以正常工作。只有在子表单控件中将表单设置为 SourceObject
时才会出现问题。
我用sp_procedure_params_rowset
比较了存储过程中的参数,它们是相同的。我比较了 SQL 中列的数据类型,tblBad
中没有任何新内容或不同之处,tblGood
中没有。我还尝试在设置表单时分析 SQL 服务器,它似乎可以很好地调用存储过程。比较坏调用和好调用之间的轨迹时,我没有看到任何线索。
将 RecordSet
直接设置为 ODBC link 到 tblBad
工作正常(并且我认为视图也可以)但是以某种方式具有简单的存储过程包装器触发错误。
我还比较了 spGoodProc
和 spBadProc
的安全性、属性和扩展属性,它们是相同的。
我的问题
在故障排除方面我可以做些什么来进一步减少这种情况? 有没有人在子表单上遇到与绑定存储过程类似的问题?我正在处理一个非常复杂的数据库,其中包含数百个表单、tables 和查询,所以我真的很想在我走得太远之前了解为什么会发生这种情况。
提前感谢您就这个令人困惑的问题分享任何见解。 :-)
找到了!
在追踪到具有特定 table 的内容后,我删除了所有约束、键,然后从 table 的副本中删除了列,系统地测试我是否可以查明问题。果然是存储过程中特定的列名!
只需将此列别名为其他名称即可解决问题。 (详见下文)
进一步测试后更新
在进一步测试以进一步查明问题之后,我想我现在明白了为什么会发生这种情况。 当您 link ODBC table 并指定唯一(键)列时,Access 将 automatically 尝试设置 LinkMasterFields
和 LinkChildFields
在加载子表单时添加到键列名称,并且子表单具有同名的列。 虽然这适用于 linked tables or views, 当子窗体的RecordSource设置为时不起作用存储过程.
如果您尝试通过手动添加子表单来执行此操作,您将看到以下通知:
但是,如果您通过 VBA 代码设置子表单目标,则不会收到任何警告或错误消息。它根本不(完全)加载子表单。 @Albert D. Kallal,你说得对,这与 master/child 字段问题有关!
我能够在 Access 2010 和 Access 2016 的测试数据库文件中一致地重现该问题。如果您想亲眼看看,可以使用以下步骤重现它:
- 使用
PrimaryID
列创建 SQL table。
CREATE TABLE [dbo].[tblBugTest](
[PrimaryID] [int] NOT NULL,
[TestColumn] [nchar](10) NULL
) ON [PRIMARY]
- 将几个测试记录添加到您刚刚创建的 table。
- 为 return 记录创建一个存储过程。
CREATE PROCEDURE [dbo].[spBugExample]
AS SELECT * FROM tblBugTest
- 创建一个空白的 Microsoft Access 数据库 (accdb)。
- 使用 ODBC,创建 linked table 到
tblBugTest
。
- 重要提示:Select
PrimaryID
作为唯一列。
- 使用 ODBC 创建一个名为
qryPassThrough
的传递查询到同一个数据库,并将 SQL 设置为 exec spBugExample
。
- 打开查询以验证它 returns 记录。
- 在数据库中创建三个空白表单。
frmMain
、frmSubForm
和 frmBlank
。
- 将
frmBlank
添加为 frmMain
的子表单。将子窗体命名为 sfrmSubform
.
- 将
frmMain
的 RecordSource
设置为 linked table.
- 向
frmMain
添加一个按钮以将子表单从 frmBlank
切换到 frmSubForm
。
Private Sub cmdShowBug_Click()
With Me.sfrmSubform
.SourceObject = "frmSubForm"
Debug.Print .LinkMasterFields
Debug.Print .LinkChildFields
Debug.Print .Form.Name
.SourceObject = "frmBlank"
End With
End Sub
- 将
frmSubForm
的 RecordSource 设置为 qryPassThrough
。
- 将几个绑定控件拖放到
frmSubForm
上。
- 自己测试
frmSubForm
。它应该从 tblBugTest
. 加载一条记录
- 打开
frmMain
并单击按钮。它应该会引发错误。
如果您单步执行代码,您会看到在设置 SourceObject
之前,LinkMasterFields
属性 是空白的。设置好SourceObject
后,可以将鼠标悬停在LinkMasterFields
上,可以看到现在已经设置到PrimaryID
列了。
解决方法
更改以下任何一项都可以通过避免 master/child 字段的自动 linking 问题来解决错误。
- 删除并重新link linked table,这次不指定唯一列。
- 将存储过程中的列别名为与唯一列不同的名称。
- 清除父窗体的 RecordSource 属性。
- 清除子表单 RecordSource 属性 并在 加载子表单后设置 RecordSet 。
- 在子表单中使用视图或 linked table 而不是存储过程。
请记住,在加载主窗体之前将尝试加载子窗体数据源。这表明在主窗体的加载事件中,您将
首先设置 PT 查询。
然后设置子窗体的OBJECT源。
也就是说子窗体控件的源对象应该是空白的
您设置 PT 查询的代码将是:
With currentdb.queryDefs("qryGood")
.SQL = "EXEC spGoodProc " & 123456
end with
当然,您可以将 123456 替换为变量,甚至是文本框中的值(来自主窗体)。
现在 PT 查询已设置,您接下来要设置要加载子表单的表单。
所以,在上面的代码之后,我们有:
me.mySubForm.SourceObject = "name of subform goes here"
所以,总共应该是4行代码左右。如上所示,您的 VBA 代码中甚至不需要任何连接字符串。
所以,请记住:
按照上述设置 PT 查询。然后您可以启动一个报表,甚至是一个表单,或者在这种情况下设置子表单控件要加载的表单。这也suggests/hints您需要删除子表单控件的源对象(留空)。
您可以设置来自源对象的子对象,但这会建议您在使用基于 PT 查询的子表单启动主表单之前按照上述设置 PT 查询源。如前所述,这组步骤是必需的,因为子表单实际上在主表单显示和呈现之前加载并解析它的数据源。因此,通过将子表单的源对象留空,开发人员可以重新获得对加载顺序的完全控制。
总结
在 Microsoft Access 2010 数据库 (accdb) 中,我有一个表单可以将其他表单动态加载到主表单上的子表单对象中。子窗体对象中使用的窗体绑定到 ODBC 传递查询,该查询对 return 记录集执行存储过程。我不明白为什么我可以使用一个存储过程并且它工作得很好,但是如果我将表单绑定到另一个存储过程,它就无法加载子表单。
技术演练
我有两个直通 ODBC 查询。 qryGood
和 qryBad
。它们使用相同的 ODBC 连接字符串 (ODBC;DRIVER=SQL Server;SERVER=MyServer;UID=MyUser;Trusted_Connection=Yes;DATABASE=MyDatabase
),它们后面的 SQL 是相同的,但指向 SQL 2012 数据库服务器上的两个不同的 SQL 存储过程。
qryGood 来源:exec spGoodProc 123456
qryBad 来源:exec spBadProc 123456
存储过程后面的SQL很简单。 Return 条来自单个 table 的记录,按作为参数传递的 ID 进行过滤。 (有些人会做更复杂的事情,但我在这里只关注一个演示问题的简化示例。)
frmMySubform
的RecordSource
属性设置为qryBad
。
子表单 SourceObject
通过 VBA 代码设置:sfrmMain.SourceObject = "frmMySubform"
此时不会抛出任何错误。虽然 SourceObject
属性 现在 return 是 frmMySubform
,但 .Form
对象似乎没有设置。
然后我尝试在子表单上引用 属性:Debug.Print sfrmMain.Form.Name
失败并出现错误 2467:您输入的表达式引用了一个已关闭或未关闭的对象'不存在。
然后我可以在设计视图中打开 frmMySubform
,将 RecordSource
属性 更改为 qryGood
,它工作正常。这似乎指向 spBadProc
的问题,只有在子表单上用作 RecordSource
时才会出现。
我试过的
为了解决这个问题,我使用排除法尽可能地缩小了范围,但我仍然不明白为什么一个存储过程有效而另一个存储过程无效。 return 记录在 SQL 和 运行 直接传递查询时都很好。直接打开表单时两者都可以正常工作。只有在子表单控件中将表单设置为 SourceObject
时才会出现问题。
我用sp_procedure_params_rowset
比较了存储过程中的参数,它们是相同的。我比较了 SQL 中列的数据类型,tblBad
中没有任何新内容或不同之处,tblGood
中没有。我还尝试在设置表单时分析 SQL 服务器,它似乎可以很好地调用存储过程。比较坏调用和好调用之间的轨迹时,我没有看到任何线索。
将 RecordSet
直接设置为 ODBC link 到 tblBad
工作正常(并且我认为视图也可以)但是以某种方式具有简单的存储过程包装器触发错误。
我还比较了 spGoodProc
和 spBadProc
的安全性、属性和扩展属性,它们是相同的。
我的问题
在故障排除方面我可以做些什么来进一步减少这种情况? 有没有人在子表单上遇到与绑定存储过程类似的问题?我正在处理一个非常复杂的数据库,其中包含数百个表单、tables 和查询,所以我真的很想在我走得太远之前了解为什么会发生这种情况。
提前感谢您就这个令人困惑的问题分享任何见解。 :-)
找到了!
在追踪到具有特定 table 的内容后,我删除了所有约束、键,然后从 table 的副本中删除了列,系统地测试我是否可以查明问题。果然是存储过程中特定的列名!
只需将此列别名为其他名称即可解决问题。 (详见下文)
进一步测试后更新
在进一步测试以进一步查明问题之后,我想我现在明白了为什么会发生这种情况。 当您 link ODBC table 并指定唯一(键)列时,Access 将 automatically 尝试设置 LinkMasterFields
和 LinkChildFields
在加载子表单时添加到键列名称,并且子表单具有同名的列。 虽然这适用于 linked tables or views, 当子窗体的RecordSource设置为时不起作用存储过程.
如果您尝试通过手动添加子表单来执行此操作,您将看到以下通知:
但是,如果您通过 VBA 代码设置子表单目标,则不会收到任何警告或错误消息。它根本不(完全)加载子表单。 @Albert D. Kallal,你说得对,这与 master/child 字段问题有关!
我能够在 Access 2010 和 Access 2016 的测试数据库文件中一致地重现该问题。如果您想亲眼看看,可以使用以下步骤重现它:
- 使用
PrimaryID
列创建 SQL table。
CREATE TABLE [dbo].[tblBugTest](
[PrimaryID] [int] NOT NULL,
[TestColumn] [nchar](10) NULL
) ON [PRIMARY]
- 将几个测试记录添加到您刚刚创建的 table。
- 为 return 记录创建一个存储过程。
CREATE PROCEDURE [dbo].[spBugExample]
AS SELECT * FROM tblBugTest
- 创建一个空白的 Microsoft Access 数据库 (accdb)。
- 使用 ODBC,创建 linked table 到
tblBugTest
。- 重要提示:Select
PrimaryID
作为唯一列。
- 重要提示:Select
- 使用 ODBC 创建一个名为
qryPassThrough
的传递查询到同一个数据库,并将 SQL 设置为exec spBugExample
。- 打开查询以验证它 returns 记录。
- 在数据库中创建三个空白表单。
frmMain
、frmSubForm
和frmBlank
。 - 将
frmBlank
添加为frmMain
的子表单。将子窗体命名为sfrmSubform
. - 将
frmMain
的RecordSource
设置为 linked table. - 向
frmMain
添加一个按钮以将子表单从frmBlank
切换到frmSubForm
。
Private Sub cmdShowBug_Click()
With Me.sfrmSubform
.SourceObject = "frmSubForm"
Debug.Print .LinkMasterFields
Debug.Print .LinkChildFields
Debug.Print .Form.Name
.SourceObject = "frmBlank"
End With
End Sub
- 将
frmSubForm
的 RecordSource 设置为qryPassThrough
。 - 将几个绑定控件拖放到
frmSubForm
上。- 自己测试
frmSubForm
。它应该从tblBugTest
. 加载一条记录
- 自己测试
- 打开
frmMain
并单击按钮。它应该会引发错误。
如果您单步执行代码,您会看到在设置 SourceObject
之前,LinkMasterFields
属性 是空白的。设置好SourceObject
后,可以将鼠标悬停在LinkMasterFields
上,可以看到现在已经设置到PrimaryID
列了。
解决方法
更改以下任何一项都可以通过避免 master/child 字段的自动 linking 问题来解决错误。
- 删除并重新link linked table,这次不指定唯一列。
- 将存储过程中的列别名为与唯一列不同的名称。
- 清除父窗体的 RecordSource 属性。
- 清除子表单 RecordSource 属性 并在 加载子表单后设置 RecordSet 。
- 在子表单中使用视图或 linked table 而不是存储过程。
请记住,在加载主窗体之前将尝试加载子窗体数据源。这表明在主窗体的加载事件中,您将
首先设置 PT 查询。 然后设置子窗体的OBJECT源。
也就是说子窗体控件的源对象应该是空白的
您设置 PT 查询的代码将是:
With currentdb.queryDefs("qryGood")
.SQL = "EXEC spGoodProc " & 123456
end with
当然,您可以将 123456 替换为变量,甚至是文本框中的值(来自主窗体)。
现在 PT 查询已设置,您接下来要设置要加载子表单的表单。
所以,在上面的代码之后,我们有:
me.mySubForm.SourceObject = "name of subform goes here"
所以,总共应该是4行代码左右。如上所示,您的 VBA 代码中甚至不需要任何连接字符串。
所以,请记住: 按照上述设置 PT 查询。然后您可以启动一个报表,甚至是一个表单,或者在这种情况下设置子表单控件要加载的表单。这也suggests/hints您需要删除子表单控件的源对象(留空)。
您可以设置来自源对象的子对象,但这会建议您在使用基于 PT 查询的子表单启动主表单之前按照上述设置 PT 查询源。如前所述,这组步骤是必需的,因为子表单实际上在主表单显示和呈现之前加载并解析它的数据源。因此,通过将子表单的源对象留空,开发人员可以重新获得对加载顺序的完全控制。