Feedback do código VBA-Sync

Aug 18 2020

Eu esperava obter alguma entrada em um módulo de classe que estou projetando para abstrair o boilerplate de utilização de consultas assíncronas no VBA. cQueryable suporta consultas síncronas e assíncronas. Portanto, você poderia fazer algo como chamar um pacote para preencher tabelas temporárias. Isso seria feito de forma síncrona porque você deseja que seja concluído antes de executar suas consultas selecionadas. Depois, você executaria consultas selecionadas em cada uma das tabelas temporárias de forma assíncrona.

Este código realmente abstrai muitas das funcionalidades da biblioteca ADODB. Tentei nomear minhas propriedades e métodos de forma semelhante ao que os objetos nessa biblioteca usam, sempre que possível. Minha propriedade connectionString tem um nome semelhante ao mesmo no objeto ADODB.Connection. E meu método CreateParam é denominado de forma semelhante ao método createParameter do objeto ADODB.Command.

Alguns dos novos procedimentos que introduzi são a propriedade sql. Isso mantém a consulta sql a ser executada (mapeia para o texto de comando no objeto de comando). Outra é ProcedureAfterQuery. Isso é para manter o procedimento de nome a ser chamado pelo objeto de conexão depois que ele gera um evento quando a consulta é concluída. Outros são SyncExecute e AsyncExecute, que devem descrever o que eles fazem em seus nomes.

Uma coisa a observar sobre esses dois é que SyncExecute é uma função, enquanto AsyncExecute é uma sub-rotina. Eu queria que SyncExecute retornasse um conjunto de registros quando fosse concluído. Mas para AsyncExecute, eu queria que fosse uma sub-rotina e não queria insinuar que retornava algo. Eu uso um código semelhante (mas diferente) para fazer isso. Então eu acho que violei o princípio DRY. Eu poderia consolidar esses dois para chamar um procedimento de sub-rotina compartilhado. Esse procedimento compartilhado seria mais complicado, mas o código pelo menos seria compartilhado. Não tenho preferência de um jeito ou de outro.

Embora CreateParam seja semelhante ao método CreateParameter do objeto de comando, há duas diferenças. Uma é que a ordem dos argumentos é diferente. Isso ocorre principalmente porque os parâmetros de tamanho e direção são listados como parâmetros opcionais com valores padrão. Seus valores padrão podem ser usados ​​apenas quando o valor for numérico, mas o tamanho deve ser especificado se o valor for uma string. Portanto, em certas situações, o tamanho é opcional, enquanto em outras é obrigatório. E a consulta falhará se não for fornecida.

Outras coisas que não considerei (ou testei) é que li que o ADODB pode ser usado essencialmente em qualquer lugar que um driver possa ser fornecido. Portanto, isso pode ser usado em pastas de trabalho do Excel, talvez arquivos de texto e outras fontes, em vez de apenas bancos de dados. Portanto, talvez as consultas síncronas e assíncronas também funcionem lá. Mas não foi isso que eu planejei projetar ou testar.

Eu aprecio críticas construtivas.

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

Respostas

2 TinMan Aug 18 2020 at 06:27

Função privada connectionSuccessful () As Boolean

O nome sugere que você está testando para ver se a conexão já foi aberta quando na verdade é usado para abrir a conexão e testar se foi bem-sucedido.

Private Function OpenConnection() As Boolean   

Este nome indica que você está abrindo uma conexão. Como o tipo de retorno é booleano, é natural supor que a função retornará True somente se a conexão for bem-sucedida.

Fazer o manipulador de erros escapar dos erros e imprimir uma mensagem na janela imediata é contraproducente. Como desenvolvedor, não procuro mensagens de erro instintivamente na janela Imediata. Como usuário, notificarei o desenvolvedor sobre a mensagem de erro que foi gerada na linha e não no ponto de impacto. Considerando que seu código usa procedimentos de retorno de chamada, não há garantia de que um erro será gerado. A única coisa certa é que haverá problemas em algum momento.

Definitivamente, você deve gerar um erro personalizado mConnectionStringse não estiver definido. Uma mensagem de erro personalizada para a conexão com falha não é necessária (se você remover o manipulador de erros) porque um erro ADODB será lançado no ponto em que este procedimento foi chamado.

Public Sub AsyncExecute ()

Considere gerar um erro se o procedimento de retorno de chamada não estiver definido.

Sub Class_Terminate privada ()

Este método deve ser usado para fechar a conexão.

mConn, mASyncConn e mSyncConn

Não há necessidade de usar três variáveis ​​de conexão diferentes. Você está trabalhando mais e ofuscando o código. O uso de uma variável como AsyncMode As Booleanfornecerá o mesmo feedback e simplificará o código, tornando-o mais fácil de ler.

Convenções de Nomenclatura

Ter valuee executeminúsculas altera o caso para todas as outras variáveis ​​e propriedades com os mesmos nomes. Por esta razão, eu uso Pascal Case para todas as minhas variáveis ​​que não possuem algum tipo de prefixo.

Fábricas de Mathieu Guindon : inicialização de objetos parametrizados

Outras melhorias possíveis

Um evento público permitiria que você use cQueryableem outras classes personalizadas.

Public Event AsyncExecuteComplete(pRecordset As Recordset)

A capacidade de encadear consultas parece um ajuste natural.

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

Isso permitirá que você execute várias consultas em ordem, sem a necessidade de várias chamadas de retorno.

CreateTempQuery.NextQuery(FillTempTableQuery).NextQuery(UpdateEmployeesTableQuery)