MS Access (Jet) транзакции, рабочие пространства - PullRequest
4 голосов
/ 25 мая 2010

У меня проблемы с совершением транзакции (с использованием Access 2003 DAO). Он действует так, как будто я никогда не вызывал BeginTrans - я получаю ошибку 3034 в CommitTrans, "Вы пытались зафиксировать или откатить транзакцию без предварительного начала транзакции" ; и изменения записываются в базу данных (предположительно, потому что они никогда не были включены в транзакцию). Однако BeginTrans запускается , если вы пройдете через него.

  • Я запускаю его в среде Access, используя рабочее пространство DBEngine (0).
  • Все таблицы, в которые я добавляю записи, открываются через соединение с базой данных Jet (к той же базе данных) и с использованием DAO.Recordset.AddNew / Update.
  • Соединение открывается перед запуском BeforeTrans.
  • Я не делаю ничего странного в середине транзакции, например закрытие / открытие соединений или нескольких рабочих областей и т. Д.
  • Существует два вложенных уровня транзакции. В основном это обертывание нескольких вставок во внешней транзакции, поэтому, если произойдет сбой, все они потерпят неудачу. Внутренние транзакции выполняются без ошибок, это внешняя транзакция, которая не работает.

Вот несколько вещей, которые я изучил и исключил:

  • Транзакция распределяется по нескольким методам, и BeginTrans и CommitTrans (и Rollback) находятся в разных местах. Но когда я попробовал выполнить простой тест выполнения транзакции таким образом, это не имело значения.

  • Я подумал, что, может быть, соединение с базой данных закрывается, когда оно выходит за пределы локальной области, хотя у меня есть другая «глобальная» ссылка на него (я никогда не уверен, что DAO делает с соединениями dbase, если честно). Но, похоже, это не так - прямо перед фиксацией соединение и его наборы записей живы (я могу проверить их свойства, EOF = False и т. Д.)

  • Мои CommitTrans и Rollback выполняются в рамках обратных вызовов событий. (Очень просто: программа синтаксического анализатора генерирует событие «onLoad» в конце синтаксического анализа, который я обрабатываю либо фиксацией, либо откатом вставок, которые я сделал во время обработки, в зависимости от того, возникли ли какие-либо ошибки.) Однако снова попытка простой тест, похоже, это не имеет значения.

Есть идеи, почему это не работает для меня?

Спасибо.

РЕДАКТИРОВАТЬ 25 мая

Вот (упрощенный) код. Ключевые моменты, связанные с транзакцией:

  • Рабочее пространство - DBEngine (0), на которое ссылается публичная (глобальная) переменная APPSESSION.
  • Соединение с базой данных открывается в LoadProcess.cache ниже, см. Строку Set db = APPSESSION.connectionTo(dbname_).
  • BeginTrans вызывается в LoadProcess.cache.
  • CommitTrans вызывается в обратном вызове process__onLoad.
  • Откат вызывается в обратном вызове процесса__onInvalid.
  • Обновления набора записей выполняются в процессах __onLoadRow, logLoadInit и logLoad

Эрик

'------------------- 
'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

Ответы [ 3 ]

3 голосов
/ 27 мая 2010

Транзакции используются путем определения рабочего пространства (оно не обязательно должно быть новым), а затем запуска транзакции в этом рабочем пространстве, выполнения того, что вам нужно с ним делать, и последующего подтверждения транзакции, если все в порядке. Скелетный код:

  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

Теперь внутри блока, в котором вы [делаете все, что угодно], вы можете передавать рабочую область, базы данных и наборы записей подпрограммам, но структура управления верхнего уровня должна оставаться в одном месте.

Ваш код этого не делает - вы зависите от глобальных переменных. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ЗЛЫ. Не используйте их. Вместо этого передайте приватные переменные в качестве параметров подпрограммам, с которыми вы хотите работать. Я также сказал бы, никогда не пропускайте рабочее пространство - пропускайте только объекты, которые вы создали с рабочим пространством.

Как только вы это освоите, возможно, это поможет вам объяснить, что должен делать ваш код (что я не имею ни малейшего представления о том, чтобы прочитать его), а затем мы можем посоветовать вам, что вы ' делаешь неправильно.

2 голосов
/ 27 мая 2010

ОК, после долгой отладки я обнаружил ошибку в транзакциях 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
0 голосов
/ 01 апреля 2011

Для чего-то это кажется немного более распространенным, чем просто транзакции доступа. Я только что столкнулся с подобной ситуацией, используя Access 2007 и DAO в качестве внешнего интерфейса для MySQL. С MySQL Autocommit=0 транзакции SQL, тем не менее, таинственным образом фиксируются на полпути транзакции.

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

и транзакции просто не работали. Я перепробовал все мыслимые за 2 недели. Как только я объявил и закрыл rs в транзакции, все заработало отлично!

...