ms-access - español - microsoft.ace.oledb.12.0 download
Transacciones de MS Access(Jet), espacios de trabajo (3)
Las transacciones se usan definiendo un espacio de trabajo (no tiene que ser uno nuevo) y luego comienza la transacción en ese espacio de trabajo, haciendo lo que tiene que hacer con él, y luego comprometiendo la transacción si todo está bien. Código esquelético:
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
Ahora, dentro del bloque donde [haga lo que sea], puede pasar el espacio de trabajo y las bases de datos y los conjuntos de registros a las subrutinas, pero la estructura de control de nivel superior debe permanecer en un solo lugar.
Su código no hace eso; en cambio, depende de variables globales. LAS VARIABLES GLOBALES SON MALAS. No los uses En su lugar, pase variables privadas como parámetros a las subrutinas que desea operar en ellas. También diría que nunca pase el espacio de trabajo, solo pase los objetos que ha creado con el espacio de trabajo.
Una vez que haya absorbido eso, tal vez lo ayude a explicar lo que se supone que debe lograr su código (que no tengo la menor idea de leerlo), y luego podemos aconsejarle sobre lo que está haciendo mal .
Tengo problemas para comprometer una transacción (usando Access 2003 DAO). Está actuando como si nunca hubiera llamado a BeginTrans. Recibo el error 3034 en CommitTrans, "Intentó confirmar o deshacer una transacción sin primero comenzar una transacción" ; y los cambios se escriben en la base de datos (presumiblemente porque nunca se envolvieron en una transacción). Sin embargo, BeginTrans se ejecuta, si lo atraviesas.
- Lo estoy ejecutando dentro del entorno de acceso usando el espacio de trabajo DBEngine (0).
- Las tablas a las que agrego registros se abren a través de una conexión de base de datos Jet (a la misma base de datos) y usando DAO.Recordset.AddNew / Update.
- La conexión se abre antes de iniciar BeforeTrans.
- No estoy haciendo nada extraño en el medio de la transacción, como cerrar / abrir conexiones o múltiples espacios de trabajo, etc.
- Hay dos niveles de transacción anidados. Básicamente está envolviendo inserciones múltiples en una transacción externa, por lo que si falla, todas fallarán. Las transacciones internas se ejecutan sin errores, es la transacción externa que no funciona.
Aquí hay algunas cosas que he estudiado y descartado:
La transacción se distribuye en varios métodos y BeginTrans y CommitTrans (y Rollback) están en diferentes lugares. Pero cuando probé una prueba simple de ejecutar una transacción de esta manera, no parece que esto debería importar.
Pensé que tal vez la conexión de la base de datos se cierra cuando se sale del alcance local, aunque tengo otra referencia "global" (nunca estoy seguro de lo que DAO hace con las conexiones dbase para ser honesto). Pero parece que este no es el caso, justo antes del commit, la conexión y sus recordssets están vivos (puedo verificar sus propiedades, EOF = False, etc.)
Mi CommitTrans y Rollback se realizan dentro de devoluciones de llamadas de eventos. (Básicamente, un programa analizador está lanzando un evento ''onLoad'' al final del análisis sintáctico, que estoy procesando ya sea confirmando o anulando las inserciones que hice durante el procesamiento, dependiendo de si ocurrió algún error). Sin embargo, de nuevo, intentando una prueba simple, no parece que esto debería importar.
¿Alguna idea de por qué esto no funciona para mí?
Gracias.
EDITAR 25 de mayo
Aquí está el código (simplificado). Los puntos clave que tienen que ver con la transacción son:
- El espacio de trabajo es DBEngine (0), referenciado dentro de la variable pública (global)
APPSESSION
. - La conexión de la base de datos se abre en LoadProcess.cache a continuación, consulte la línea
Set db = APPSESSION.connectionTo(dbname_)
. - BeginTrans se llama en LoadProcess.cache.
- Se llama a CommitTrans en la devolución de llamada process__onLoad.
- La reversión se llama en el proceso de devolución de llamada inválida inválida.
- Las actualizaciones del conjunto de registros se realizan en process__onLoadRow, logLoadInit y 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
OK, después de mucha depuración frustrante, creo que descubrí un error en las transacciones de Jet. Después de todo eso, no tiene nada que ver con mi código "enormemente intrincado" o "variables globales malvadas" :)
Parece que cuando lo siguiente es verdadero, obtiene el error # 3034:
- Abre un conjunto de registros tipo snapshot
- El conjunto de registros se abre antes de iniciar la transacción
- El conjunto de registros se cierra / desreferencia después de comenzar la transacción, pero antes de la confirmación o reversión.
No he comprobado si esto ya se conoce, aunque no puedo imaginar que no lo sea.
Por supuesto, es un poco raro hacer las cosas en este orden de todos modos y pedir problemas, no sé por qué lo hice. Moví abrir / cerrar el conjunto de registros de instantáneas dentro de la transacción y todo funciona bien.
El siguiente código muestra el error:
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
Y esto soluciona el error:
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
Por lo que vale, esto parece estar un poco más extendido que las transacciones de acceso. Acabo de encontrar una situación similar usando Access 2007 y DAO como interfaz para MySQL. Con MySQL Autocommit=0
, las transacciones de SQL sin embargo se comprometerían misteriosamente a la mitad de una transacción.
Después de 2 semanas de rascarse la cabeza, me encontré con esta publicación y volví a mirar mi código. Efectivamente, las inserciones de MySQL se realizaban mediante un procedimiento almacenado que se llamaba desde un módulo de clase de VBA. Este módulo de clase tenía un dao.recordset
que se abrió en module.initialize()
y se cerró en terminate()
. Además, este conjunto de registros se utilizó para recopilar los resultados del procedimiento almacenado. Así que tuve (en pseudo código ...)
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
y las transacciones simplemente no funcionaban. Intenté todo lo imaginable durante 2 semanas. Una vez que declare y cerré el rs dentro de la transacción, ¡todo funcionó perfectamente!