Comentarios sobre el código VBA-Sync

Aug 18 2020

Esperaba obtener información sobre un módulo de clase que estoy diseñando para abstraer el texto estándar de la utilización de consultas asincrónicas en VBA. cQueryable admite consultas sincrónicas y asincrónicas. Entonces podría hacer algo como llamar a un paquete para completar tablas temporales. Esto se haría de forma sincrónica porque querría que esto se complete antes de ejecutar sus consultas seleccionadas. Después, ejecutaría consultas seleccionadas en cada una de las tablas temporales de forma asincrónica.

Este código realmente simplemente abstrae gran parte de la funcionalidad en la biblioteca ADODB. Intenté nombrar mis propiedades y métodos de manera similar a lo que usan los objetos en esa biblioteca cuando sea posible. Mi propiedad connectionString tiene un nombre similar al mismo en el objeto ADODB.Connection. Y mi método CreateParam tiene un nombre similar al método createParameter del objeto ADODB.Command.

Algunos de los nuevos procedimientos que he introducido son la propiedad sql. Esto mantiene la consulta SQL para ser ejecutada (esto se asigna al texto de comando en el objeto de comando). Otro es ProcedureAfterQuery. Esto es para mantener el procedimiento de nombre que será llamado por el objeto de conexión después de que genera un evento cuando se completa la consulta. Otros son SyncExecute y AsyncExecute, que deberían describir lo que hacen en sus nombres.

Una cosa a tener en cuenta sobre estos dos es que SyncExecute es una función mientras que AsyncExecute es una subrutina. Quería que SyncExecute devolviera un conjunto de registros cuando se complete. Pero para AsyncExecute, quería que fuera un sub, no quería dar a entender que devolvía nada. Utilizo un código similar (pero diferente) para hacer esto. Entonces creo que violé el principio DRY. Podría consolidar estos dos para llamar a un procedimiento de subrutina compartido. Ese procedimiento compartido sería entonces más complicado, pero el código al menos se compartiría. No tengo ninguna preferencia de una forma u otra.

Aunque CreateParam es similar al método CreateParameter del objeto de comando, existen dos diferencias. Una es que el orden de los argumentos es diferente. Esto se debe principalmente a que los parámetros de tamaño y dirección se enumeran como parámetros opcionales con valores predeterminados. Sus valores predeterminados solo se pueden usar cuando el valor es numérico, pero el tamaño debe especificarse si el valor es una cadena. Entonces, en ciertas situaciones, el tamaño es opcional, mientras que en otras es obligatorio. Y la consulta fallará si no se proporciona.

Otras cosas que no consideré (o probé) es que he leído que ADODB se puede usar esencialmente en cualquier lugar donde se pueda proporcionar un controlador. Por lo tanto, esto podría usarse en libros de Excel, tal vez archivos de texto y otras fuentes en lugar de solo bases de datos. Entonces, quizás las consultas sincrónicas y asincrónicas también funcionen allí. Pero eso no es lo que me propuse diseñar o probar.

Agradecería las críticas constructivas.

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "cQueryable"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

'Requires a refernce to the Microsoft ActiveX Data Objects 6.1 Library (or equivalent)

Private WithEvents mASyncConn As ADODB.Connection
Attribute mASyncConn.VB_VarHelpID = -1
Private mSyncConn As ADODB.Connection
Private mConn As ADODB.Connection
Private mComm As ADODB.Command
Private mSql As String
Private mProcedureAfterQuery As String
Private mAsync As Boolean
Private mConnectionString As String

Private Const mSyncExecute As Long = -1

Private Sub Class_Initialize()
    Set mComm = New ADODB.Command
    Set mConn = New ADODB.Connection
End Sub

Public Property Let Sql(value As String)
    mSql = value
End Property

Public Property Get Sql() As String
    Sql = mSql
End Property

Public Property Let ConnectionString(value As String)
    mConnectionString = value
End Property

Public Property Get ConnectionString() As String
    ConnectionString = mConnectionString
End Property

Public Property Let procedureAfterQuery(value As String)
    mProcedureAfterQuery = value
End Property

Public Property Get procedureAfterQuery() As String
    procedureAfterQuery = mProcedureAfterQuery
End Property

Public Sub createParam(pName As String, pType As DataTypeEnum, pValue As Variant, Optional pDirection As ParameterDirectionEnum = adParamInput, Optional pSize As Long = 0)
    Dim pm As ADODB.Parameter
    With mComm
       Set pm = .CreateParameter(name:=pName, Type:=pType, direction:=pDirection, value:=pValue, size:=pSize)
       .Parameters.Append pm
    End With
End Sub

Public Function SyncExecute()
    Set mSyncConn = mConn
    If connectionSuccessful Then
        With mComm
            .CommandText = mSql
            Set .ActiveConnection = mSyncConn
            Set SyncExecute = .execute(Options:=mSyncExecute)
        End With
    End If
End Function

Public Sub AsyncExecute()
    Set mASyncConn = mConn
    If connectionSuccessful Then
        With mComm
            .CommandText = mSql
            Set .ActiveConnection = mASyncConn
            .execute Options:=adAsyncExecute
        End With
    End If
End Sub

Private Function connectionSuccessful() As Boolean
    If mConn.State = adStateClosed Then
        mConn.ConnectionString = mConnectionString
    End If
    
    On Error GoTo errHandler
        If mConn.State = adStateClosed Then
            mConn.Open
        End If
    
        connectionSuccessful = (mConn.State = adStateOpen)
    On Error GoTo 0
    
    Exit Function
errHandler:
    Debug.Print "Error: Connection unsuccessful"
    connectionSuccessful = False
End Function

Private Sub mASyncConn_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
    If mProcedureAfterQuery <> "" Then
        Call Application.Run(mProcedureAfterQuery, pRecordset)
    End If
End Sub

Respuestas

2 TinMan Aug 18 2020 at 06:27

Conexión de función privada Exitoso () como booleano

El nombre sugiere que está probando para ver si la conexión ya se ha abierto cuando en realidad se usa para abrir la conexión y probar si tuvo éxito.

Private Function OpenConnection() As Boolean   

Este nombre le indica que está abriendo una conexión. Dado que el tipo de retorno es booleano, es natural suponer que la función devolverá True solo si la conexión fue exitosa.

Hacer que el controlador de errores escape los errores e imprima un mensaje en la ventana Inmediato es contraproducente. Como desarrollador, no busco instintivamente mensajes de error en la ventana Inmediato. Como usuario, notificaré al desarrollador sobre el mensaje de error que apareció en la línea y no en el punto de impacto. Teniendo en cuenta que su código utiliza procedimientos de devolución de llamada, no hay garantía de que alguna vez se genere un error. Lo único que es seguro es que habrá problemas en algún momento.

Definitivamente debería generar un error personalizado si mConnectionStringno está configurado. No es necesario un mensaje de error personalizado para la conexión fallida (si elimina el controlador de errores) porque se lanzará un error ADODB en el punto donde se llamó a este procedimiento.

Public Sub AsyncExecute ()

Considere generar un error si el procedimiento de devolución de llamada no está configurado.

Subclase privada_Terminate ()

Este método debe usarse para cerrar la conexión.

mConn, mASyncConn y mSyncConn

No es necesario utilizar tres variables de conexión diferentes. Está haciendo más trabajo y ofuscando el código. El uso de una variable como AsyncMode As Booleanle dará la misma información y simplificará el código para que sea más fácil de leer.

Convenciones de nombres

Tener valuey executeminúsculas cambia el caso de todas las demás variables y propiedades con los mismos nombres. Por esta razón, utilizo Pascal Case para todas mis variables que no tienen algún tipo de prefijo.

Fábricas de Mathieu Guindon : inicialización de objetos parametrizados

Otras posibles mejoras

Un evento público le permitiría usarlo cQueryableen otras clases personalizadas.

Public Event AsyncExecuteComplete(pRecordset As Recordset)

La capacidad de encadenar consultas juntas parece un ajuste natural.

Public Function NextQuery(Queryable AS cQueryable) AS cQueryable
   Set NextQuery = Queryable 
   Set mQueryable = Queryable 
End Function

Esto le permitirá ejecutar varias consultas en orden sin la necesidad de varias devoluciones de llamada.

CreateTempQuery.NextQuery(FillTempTableQuery).NextQuery(UpdateEmployeesTableQuery)