MS Access (Jet) 事务和工作空间

4
我在提交事务时遇到了问题(使用Access 2003 DAO)。它好像从未调用过BeginTrans一样——在CommitTrans上我会得到3034错误,“您尝试在没有开始事务的情况下提交或回滚事务”;而更改被写入数据库(可能是因为它们从未包装在事务中)。然而,如果您逐步执行,则会运行BeginTrans
  • 我正在使用DBEngine(0)工作区在Access环境中运行它。
  • 我要添加记录的表都是通过Jet数据库连接打开的(连接到同一个数据库),并使用DAO.Recordset.AddNew / Update。
  • 连接在开始BeforeTrans之前打开。
  • 在事务中间我没有做任何奇怪的事情,如关闭/打开连接或多个工作区等。
  • 有两个嵌套的事务级别。基本上,它将多个插入语句包装在外部事务中,因此如果有任何失败,它们都会失败。内部事务运行没有错误,是外部事务不起作用。

以下是我研究并排除的一些事项:

  • 事务跨越了几种方法,并且BeginTrans和CommitTrans(以及Rollback)都在不同的位置。但当我尝试以这种方式运行事务的简单测试时,似乎这不应该有影响。

  • 我想也许当它超出本地范围时,数据库连接会关闭,尽管我有另一个“全局”引用它(老实说,我从来不知道DAO如何处理数据库连接)。但是这似乎不是这种情况——在提交之前,连接及其记录集是活动的(我可以检查它们的属性,EOF = False等)

  • 我的CommitTrans和Rollback是在事件回调中完成的。(非常基本地:解析器程序在解析结束时抛出“onLoad”事件,我通过根据是否发生任何错误来处理,在处理过程中插入或回滚。)然而,再次尝试简单测试,似乎这不应该有影响。

为什么我不能工作?

谢谢。

编辑5月25日

以下是(简化后的)代码。与事务有关的关键点是:

  • 工作区是DBEngine(0),在公共(全局)变量APPSESSION中引用。
  • 数据库连接在下面的LoadProcess.cache中打开,请参见Set db = APPSESSION.connectionTo(dbname_)行。
  • BeginTrans在LoadProcess.cache中调用。
  • CommitTrans在process__onLoad回调中调用。
  • Rollback在process__onInvalid回调中调用。
  • 记录集更新在process__onLoadRow、logLoadInit和logLoad中完成

Eric

'------------------- 
'Application globals
'-------------------

Public APPSESSION As DAOSession

'------------------
' Class LoadProcess
'------------------

Private WithEvents process_ As EventedParser
Private errs_ As New Collection

Private dbname_ As String
Private rawtable_ As String
Private logtable_ As String
Private isInTrans_ As Integer

Private raw_ As DAO.Recordset
Private log_ As DAO.Recordset
Private logid_ As Variant

Public Sub run
    '--- pre-load
    cache
    resetOnRun    ' resets load state variables per run, omitted here
    logLoadInit
    Set process_ = New EventedParser

    '--- load
    process_.Load
End Sub

' raised once per load() if any row invalid
Public Sub process__onInvalid(filename As String)
    If isInTrans_ Then APPSESSION.Workspace.Rollback
End Sub

' raised once per load() if all rows valid, after load
Public Sub process__onLoad(filename As String)
    If errs_.Count > 0 Then
        logLoadFail filename, errs_
    Else
        logLoadOK filename
    End If

    If isInTrans_ Then APPSESSION.Workspace.CommitTrans
End Sub

' raised once per valid row
' append data to raw_ recordset
Public Sub process__onLoadRow(row As Dictionary)
On Error GoTo Err_

    If raw_ Is Nothing Then GoTo Exit_   
    DAOext.appendFromHash raw_, row, , APPSESSION.Workspace

Exit_:
    Exit Sub

Err_:
    ' runtime error handling done here, code omitted
    Resume Exit_

End Sub


Private Sub cache()
Dim db As DAO.Database

    ' TODO raise error
    If Len(dbname_) = 0 Then GoTo Exit_       
    Set db = APPSESSION.connectionTo(dbname_)
    ' TODO raise error
    If db Is Nothing Then GoTo Exit_ 

    Set raw_ = db.OpenRecordset(rawtable_), dbOpenDynaset)
    Set log_ = db.OpenRecordset(logtable_), dbOpenDynaset)    

    APPSESSION.Workspace.BeginTrans
    isInTrans_ = True

Exit_:
    Set db = Nothing

End Sub

' Append initial record to log table
Private Sub logLoadInit()
Dim info As New Dictionary
On Error GoTo Err_

    ' TODO raise error?
    If log_ Is Nothing Then GoTo Exit_   

    With info
        .add "loadTime", Now
        .add "loadBy", CurrentUser
    End With

    logid_ = DAOext.appendFromHash(log_, info, , APPSESSION.Workspace)

Exit_:
    Exit Sub

Err_:
    ' runtime error handling done here, code omitted
    Resume Exit_

End Sub

Private Sub logLoadOK(filename As String)
    logLoad logid_, True, filename, New Collection
End Sub

Private Sub logLoadFail(filename As String, _
                      errs As Collection)
    logLoad logid_, False, filename, errs
End Sub

' Update log table record added in logLoadInit
Private Sub logLoad(logID As Variant, _
                    isloaded As Boolean, _
                    filename As String, _
                    errs As Collection)

Dim info As New Dictionary
Dim er As Variant, strErrs As String
Dim ks As Variant, k As Variant
On Error GoTo Err_

    ' TODO raise error?
    If log_ Is Nothing Then GoTo Exit_   
    If IsNull(logID) Then GoTo Exit_

    For Each er In errs
        strErrs = strErrs & IIf(Len(strErrs) = 0, "", vbCrLf) & CStr(er)
    Next Er

    With info
        .add "loadTime", Now
        .add "loadBy", CurrentUser
        .add "loadRecs", nrecs
        .add "loadSuccess", isloaded
        .add "loadErrs", strErrs
        .add "origPath", filename
    End With

    log_.Requery
    log_.FindFirst "[logID]=" & Nz(logID)
    If log_.NoMatch Then
        'TODO raise error
    Else
        log_.Edit
        ks = info.Keys
        For Each k In ks
            log_.Fields(k).Value = info(k)
        Next k
        log_.Update
    End If

Exit_:
    Exit Sub

Err_:
    ' runtime error handling done here, code omitted
    Resume Exit_

End Sub


'-------------
' Class DAOExt
'-------------
' append to recordset from Dictionary, return autonumber id of new record
Public Function appendFromHash(rst As DAO.Recordset, _
                          rec As Dictionary, _
                          Optional map As Dictionary, _
                          Optional wrk As DAO.workspace) As Long
Dim flds() As Variant, vals() As Variant, ifld As Long, k As Variant
Dim f As DAO.Field, rst_id As DAO.Recordset
Dim isInTrans As Boolean, isPersistWrk As Boolean
On Error GoTo Err_

    ' set up map (code omitted here)

    For Each k In rec.Keys
        If Not map.Exists(CStr(k)) Then _
            Err.Raise 3265, "appendFromHash", "No field mapping found for [" & CStr(k) & "]"
        flds(ifld) = map(CStr(k))
        vals(ifld) = rec(CStr(k))
        ifld = ifld + 1
    Next k

    If wrk Is Nothing Then
        isPersistWrk = False
        Set wrk = DBEngine(0)
    End If

    wrk.BeginTrans
        isInTrans = True
        rst.AddNew
        With rst
            For ifld = 0 To UBound(flds)
                .Fields(flds(ifld)).Value = vals(ifld)
            Next ifld
        End With
        rst.Update

        Set rst_id = wrk(0).OpenRecordset("SELECT @@Identity", DAO.dbOpenForwardOnly, DAO.dbReadOnly)
        appendFromHash = rst_id.Fields(0).Value

    wrk.CommitTrans
    isInTrans = False

Exit_:
    On Error GoTo 0
    If isInTrans And Not wrk Is Nothing Then wrk.Rollback
    If Not isPersistWrk Then Set wrk = Nothing
    Exit Function

Err_:
    ' runtime error handling, code omitted here
    Resume Exit_

End Function


'-----------------
' Class DAOSession (the part that deals with the workspace and dbase connections)
'-----------------
Private wrk_ As DAO.workspace
Private connects_ As New Dictionary
Private dbs_ As New Dictionary

Public Property Get workspace() As DAO.workspace
    If wrk_ Is Nothing Then
        If DBEngine.Workspaces.Count > 0 Then
            Set wrk_ = DBEngine(0)
        End If
    End If
    Set workspace = wrk_
End Property

Public Property Get connectionTo(dbname As String) As DAO.database
    connectTo dbname
    Set connectionTo = connects_(dbname)
End Property

Public Sub connectTo(dbname As String)
Dim Cancel As Integer
Dim cnn As DAO.database
Dim opts As Dictionary
    Cancel = False

    ' if already connected, use cached reference
    If connects_.Exists(dbname) Then GoTo Exit_

    If wrk_ Is Nothing Then _
        Set wrk_ = DBEngine(0)

    ' note opts is a dictionary of connection options, code omitted here
    Set cnn = wrk_.OpenDatabase(dbs_(dbname), _
                                CInt(opts("DAO.OPTIONS")), _
                                CBool(opts("DAO.READONLY")), _
                                CStr(opts("DAO.CONNECT")))

    ' Cache reference to dbase connection
    connects_.Add dbname, cnn

Exit_:
    Set cnn = Nothing
    Exit Sub

End Sub

没有代码,无法回答你的问题。你提到了DBEngine(0)(0),但你没有说任何关于工作空间的内容,它是实现事务的级别。 - David-W-Fenton
你的代码非常复杂,难以进行故障排除。请查看我的答案。 - David-W-Fenton
3个回答

3

事务是通过定义一个工作区(它不一定是一个新的工作区)来使用的,然后在该工作区上开始事务,对其进行必要的操作,如果一切正常,则提交事务。以下是基本代码:

  On Error GoTo errHandler
    Dim wrk As DAO.Workspace

    Set wrk = DBEngine(0) ' use default workspace
    wrk.BeginTrans
    [do whatever]
    If [conditions are met] Then
       wrk.CommitTrans
    Else
       wrk.Rollback
    End If

  errHandler:
    Set wrk = Nothing

  exitRoutine:
    ' do whatever you're going to do with errors
    wrk.Rollback
    Resume errHandler

现在,在你[做任何事情]的块内,你可以将工作区、数据库和记录集传递给子程序,但顶层控制结构应该保持在一个地方。
你的代码没有这样做——相反,你依赖全局变量。全局变量是有害的,请不要使用它们。相反,将私有变量作为参数传递给你想要操作它们的子程序。我还要说,永远不要传递工作区——只传递你用工作区创建的对象。
一旦你理解了这一点,也许它会帮助你解释你的代码应该完成什么任务(我从阅读中完全不知道),然后我们可以指导你你做错了什么。

1
根据您对全局变量的观点...我同意它们是有害的,但我发现在我开发的VBA应用程序中,没有好的方法可以避免使用它们...我发现Access提供的全局对象——Application、DbEngine等——本身并不太有用。例如,你要把用户首选项存哪儿,以便随时都能访问?如果你不想每次需要它们时都从磁盘加载它们的话? - Eric G
1
我会将用户偏好存储在一张表中,然后用类模块进行检索。或者,采用简单的方式,在应用程序启动时从表格中加载到隐藏表单的文本框中。由于我的用户偏好不多,所以我并不经常使用这个功能。 - David-W-Fenton
2
我的项目通常有多个后端数据库 - 实际上有多组后端数据库。我发现编程方式管理它们(交换链接表,远程查询等)非常有用。而且正如一些人(http://www.fmsinc.com/free/NewTips/Access/LinkedDatabase.asp)所建议的那样,保持后端数据库连接开放有助于提高性能。是的,我可以在任何地方使用DBEngine,但对我来说,在一个地方管理内部更容易。 - Eric G
2
一个简单的例子:假设我想要连接到数据库x。我怎么知道它是否已经在DBEngine(0).Databases中打开了?每次打开和关闭都是很耗费资源的。最好有一个类来缓存第一次连接,并在后续每次返回已经打开的连接。 - Eric G
1
David已经离世。Access世界失去了一位宝贵的资源和该平台的热情捍卫者。 - mwolfe02
显示剩余6条评论

2

好的,在经历了很多令人沮丧的调试之后,我认为我发现了Jet事务中的一个错误。 经过这一切,它与我的“非常复杂”的代码或“邪恶的全局变量”没有任何关系 :)

当以下条件成立时,您会收到错误#3034:

  • 您打开了一个快照类型的记录集
  • 在启动事务之前打开了记录集
  • 在提交或回滚之前关闭/取消引用记录集,但在开始事务之后。

我不知道是否已经知道了这一点,尽管我想象不出不知道。

当然,按照这种顺序做事情有点奇怪,并且容易出问题,我不知道为什么要这样做。 我将打开/关闭快照记录集移至事务内部,一切正常。

以下代码显示错误:

Public Sub run()
Dim db As DAO.Database, qdf As DAO.QueryDef, rst As DAO.Recordset
Dim wrk As DAO.Workspace, isInTrans As Boolean
On Error GoTo Err_

    Set wrk = DBEngine(0)
    Set db = wrk(0)
    Set rst = db.OpenRecordset("Table2", DAO.dbOpenSnapshot)

    wrk.BeginTrans
    isInTrans = True

    Set qdf = db.CreateQueryDef("", "INSERT INTO [Table1] (Field1, Field2) VALUES (""Blow"", ""Laugh"");")
    qdf.Execute dbFailOnError

Exit_:
    Set rst = Nothing
    Set qdf = Nothing
    Set db = Nothing
    If isInTrans Then wrk.CommitTrans
    isInTrans = False
    Exit Sub

Err_:
    MsgBox Err.Description
    If isInTrans Then wrk.Rollback
    isInTrans = False
    Resume Exit_

End Sub

这样修复了错误:

Public Sub run()
Dim db As DAO.Database, qdf As DAO.QueryDef, rst As DAO.Recordset
Dim wrk As DAO.Workspace, isInTrans As Boolean
On Error GoTo Err_

    Set wrk = DBEngine(0)
    Set db = wrk(0)

    wrk.BeginTrans
    isInTrans = True

    ' NOTE THIS LINE MOVED WITHIN THE TRANSACTION
    Set rst = db.OpenRecordset("Table2", DAO.dbOpenSnapshot)

    Set qdf = db.CreateQueryDef("", "INSERT INTO [Table1] (Field1, Field2) VALUES (""Blow"", ""Laugh"");")
    qdf.Execute dbFailOnError

Exit_:
    Set rst = Nothing
    Set qdf = Nothing
    Set db = Nothing
    If isInTrans Then wrk.CommitTrans
    isInTrans = False
    Exit Sub

Err_:
    MsgBox Err.Description
    If isInTrans Then wrk.Rollback
    isInTrans = False
    Resume Exit_

End Sub

我不理解你的代码中有两件事。第一是为什么要打开记录集;第二是为什么要创建临时QueryDef而不是直接执行SQL。在我看来,做临时QueryDef是数据库膨胀的隐性来源之一,应该避免使用。我无法看出它在任何情况下都有优势。 - David-W-Fenton
我怀疑如果您创建了一个新的工作区并在其中执行事务,那么它不会干扰在事务之外创建的记录集,如果您为其使用默认的工作区。 - David-W-Fenton
就我个人而言,我根本不认为这是一个错误。我认为你只是在工作区方面有些马虎。 - David-W-Fenton
1
这只是一个简单的示例,用于展示错误,而不是我实际使用的代码。你可以很容易地想象一种情况,在实际运行事务之前需要在表中查找某些内容 - 这就是我所做的事情,尽管我将其从上面的示例中删除了。感谢您提供有关直接执行SQL与临时QueryDef的提示。 - Eric G
1
在这个示例代码中,我只有一个工作区和一个数据库 - 默认的工作区和默认的数据库。如果按原样运行上面的第一个子程序,它将重现错误。对我来说,打开或关闭快照记录集并不会改变工作区状态,但它确实会重置事务。这就是为什么我认为这是一个错误。但很容易避免。 - Eric G
"David-W-Fenton"说:“在任何情况下,我都看不出它有任何优势。”Eric创建了一个未命名的QueryDef,因此它是临时的(即仅存在于内存中),因此不会真正增加数据库膨胀。David没有考虑带有参数的SQL。QueryDef参数是一种方便的插入值的方式,无需手动为SQL格式化值,但更重要的是,如果值来自用户或外部来源,则可以避免SQL注入漏洞。 - C Perkins

0

就这个问题而言,它似乎比仅限于Access事务更为普遍。我刚刚遇到了一个类似的情况,使用Access 2007和DAO作为MySQL的前端。在MySQL中,即使Autocommit=0,SQL事务也会神秘地在事务进行到一半时提交。

经过两周的苦思冥想,我看到了这篇文章并重新审视了我的代码。果然,MySQL插入是由一个存储过程完成的,该存储过程从VBA类模块内部调用。这个类模块有一个dao.recordset,在module.initialize()上打开,在terminate()上关闭。此外,这个记录集被用来收集存储过程的结果。所以我有(伪代码...)

module.initialize - rs.open

class properties set by external functions

transaction.begins

Mysql procedure.calls using class properties as parameters - 

commit(or rollback)

rs.populate

class properties.set

properties used by external functions

module terminate - rs.close

而且交易根本就不起作用。我尝试了两个星期的所有可能性。 一旦我在事务内声明并关闭了rs,一切都完美解决了!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接