2
\$\begingroup\$

SecureADODB is a very instructive VBA OOP demo. I have played with SecureADODB for a while and plan to integrate it into the prototype of my database manager application. In my previous post, I showed ContactEditor's class diagram with the ADOlib class encapsulated into the DataTableADODB backend. The ADOlib class contains a few very basic wrappers around the ADODB library, and I created it as a stub to focus on other components of the db manager app. Now, I would like to replace ADOlib with SecureADODB. In preparation, I forked SecureADODB, slightly refactored the original codebase, and incorporated additional functionality from ADOlib. The focus of this question is on several modifications, particularly the DbManager class and two helper classes.


Class diagram of this SecureADODB fork (inteface classes not shown)

Class Diagram

DbManager (renamed UnitOfWork)

'@Folder "SecureADODB.DbManager"
'@ModuleDescription("An object that encapsulates a database transaction.")
'@PredeclaredId
'@Exposed
Option Explicit
Implements IDbManager
Private Type TDbManager
 DbMeta As DbMetaData
 DbConnStr As DbConnectionString
 Connection As IDbConnection
 CommandFactory As IDbCommandFactory
 UseTransactions As Boolean
 LogController As ILogger
End Type
Private this As TDbManager
'@Ignore ProcedureNotUsed
'@Description("Returns class reference")
Public Property Get Class() As DbManager
 Set Class = DbManager
End Property
'@Description("Default factory")
Public Function Create(ByVal db As IDbConnection, _
 ByVal factory As IDbCommandFactory, _
 Optional ByVal UseTransactions As Boolean = True, _
 Optional ByVal LogController As ILogger = Nothing) As IDbManager
 Dim Instance As DbManager
 Set Instance = New DbManager
 Instance.Init db, factory, UseTransactions, LogController
 Set Create = Instance
End Function
'@Description("Default constructor")
Friend Sub Init(ByVal db As IDbConnection, _
 ByVal factory As IDbCommandFactory, _
 Optional ByVal UseTransactions As Boolean = True, _
 Optional ByVal LogController As ILogger = Nothing)
 
 Guard.NullReference factory
 Guard.NullReference db
 Guard.Expression db.State = adStateOpen, Source:="DbManager", Message:="Connection should be open."
 Set this.LogController = LogController
 Set this.Connection = db
 Set this.CommandFactory = factory
 this.UseTransactions = UseTransactions
End Sub
'''' Factory for file-based databases.
''''
'''' Args:
'''' DbType (string):
'''' Type of the database: "sqlite", "csv", "xls"
''''
'''' DbFileName (string, optional, ""):
'''' Database file name. If not provided, ThisWorkbook.VBProject.Name
'''' will be used. Extension is added based on the database type:
'''' "sqlite" - "db" or "sqlite"
'''' "csv" - "xsv" or "csv"
'''' "xls" - "xls
''''
'''' ConnectionOptions (variant, optional, Empty):
'''' Connection options. If Empty, default values is selected based on the
'''' database type (see DbConnectionString constructor code for details).
''''
'''' N.B.: "xls" backend support is not currently implemented
''''
'@Description "Factory for file-based databases"
Public Function CreateFileDb( _
 ByVal DbType As String, _
 Optional ByVal DbFileName As String = vbNullString, _
 Optional ByVal ConnectionOptions As String = vbNullString, _
 Optional ByVal UseTransactions As Boolean = True, _
 Optional ByVal LoggerType As LoggerTypeEnum = LoggerTypeEnum.logGlobal _
 ) As IDbManager
 Dim LogController As ILogger
 Select Case LoggerType
 Case LoggerTypeEnum.logDisabled
 Set LogController = Nothing
 Case LoggerTypeEnum.logGlobal
 Set LogController = Logger
 Case LoggerTypeEnum.logPrivate
 Set LogController = Logger.Create
 End Select
 
 Dim provider As IParameterProvider
 Set provider = AdoParameterProvider.Create(AdoTypeMappings.Default)
 
 Dim baseCommand As IDbCommandBase
 Set baseCommand = DbCommandBase.Create(provider)
 
 Dim factory As IDbCommandFactory
 Set factory = DbCommandFactory.Create(baseCommand)
 
 Dim DbConnStr As DbConnectionString
 Set DbConnStr = DbConnectionString.CreateFileDb(DbType, DbFileName, , ConnectionOptions)
 Dim db As IDbConnection
 Set db = DbConnection.Create(DbConnStr.ConnectionString, LogController)
 
 Dim Instance As DbManager
 Set Instance = DbManager.Create(db, factory, UseTransactions, LogController)
 Set Instance.DbConnStr = DbConnStr
 Set Instance.DbMeta = DbMetaData.Create(DbConnStr)
 Set CreateFileDb = Instance
End Function
'@Ignore ProcedureNotUsed
Public Property Get DbConnStr() As DbConnectionString
 Set DbConnStr = this.DbConnStr
End Property
Public Property Set DbConnStr(ByVal Instance As DbConnectionString)
 Set this.DbConnStr = Instance
End Property
'@Ignore ProcedureNotUsed
Public Property Get DbMeta() As DbMetaData
 Set DbMeta = this.DbMeta
End Property
Public Property Set DbMeta(ByVal Instance As DbMetaData)
 Set this.DbMeta = Instance
End Property
'@Description("Returns class reference")
Public Property Get IDbManager_Class() As DbManager
 Set IDbManager_Class = DbManager
End Property
Private Property Get IDbManager_LogController() As ILogger
 Set IDbManager_LogController = this.LogController
End Property
Private Property Get IDbManager_DbConnStr() As DbConnectionString
 Set IDbManager_DbConnStr = this.DbConnStr
End Property
Private Property Get IDbManager_DbMeta() As DbMetaData
 Set IDbManager_DbMeta = this.DbMeta
End Property
Private Property Get IDbManager_Connection() As IDbConnection
 Set IDbManager_Connection = this.Connection
End Property
Private Function IDbManager_Command() As IDbCommand
 Set IDbManager_Command = this.CommandFactory.CreateInstance(this.Connection)
End Function
Private Function IDbManager_Recordset( _
 Optional ByVal Scalar As Boolean = False, _
 Optional ByVal Disconnected As Boolean = True, _
 Optional ByVal CacheSize As Long = 10, _
 Optional ByVal CursorType As ADODB.CursorTypeEnum = -1, _
 Optional ByVal AsyncMode As Boolean = False, _
 Optional ByVal AsyncOption As ADODB.ExecuteOptionEnum = 0) As IDbRecordset
 Dim cmd As IDbCommand
 Set cmd = this.CommandFactory.CreateInstance(this.Connection)
 Set IDbManager_Recordset = DbRecordset.Create( _
 cmd, Scalar, Disconnected, CacheSize, CursorType, AsyncMode, AsyncOption)
End Function
Private Sub IDbManager_Begin()
 Guard.Expression this.UseTransactions, Source:="DbManager", Message:="Transactions are disabled by the caller."
 this.Connection.BeginTransaction
End Sub
Private Sub IDbManager_Commit()
 Guard.Expression this.UseTransactions, Source:="DbManager", Message:="Transactions are disabled by the caller."
 this.Connection.CommitTransaction
End Sub
Private Sub IDbManager_Rollback()
 Guard.Expression this.UseTransactions, Source:="DbManager", Message:="Transactions are disabled by the caller."
 this.Connection.RollbackTransaction
End Sub

While this DbManager class is functionally similar to the UnitOfWork class in RD SecureADODB, I added two helper classes to simplify specific tasks.

DbConnectionString

During the prototyping stage, I do not want to worry about config files and connection string issues. So using this class for file-based databases (currently SQLite and CSV backends are supported), I can simply indicate the type of the database and get a connection string with sane defaults.

'@Folder "SecureADODB.DbManager.DbConnectionString"
'@ModuleDescription "Helper routines for building connection strings"
'@PredeclaredId
'@Exposed
''''
'''' The module incorporates routines, which facilitate construction of
'''' connection strings for the ADODB library. Presently, a distinction is made
'''' between file-based databases, such as sqlite, csv, xls, etc., and network
'''' based databases. A file-based database is accessed based on its type and
'''' file pathname.
'''' CreateFileDB/InitFileDB pair is used for file based databases.
''''
Option Explicit
Option Compare Text
Private Type TDbConnectionString
 DbType As String
 DbPath As String
 Options As String
 Driver As String
 ConnectionString As String
End Type
Private this As TDbConnectionString
'''' Factory for file-based databases.
''''
'''' Args:
'''' FileType (string, optional, "sqlite"):
'''' Type of the database: "sqlite", "csv", "xls"
''''
'''' FileName (string, optional, ""):
'''' Database file name. If not provided, ThisWorkbook.VBProject.Name
'''' will be used. Extension is added based on the database type:
'''' "sqlite" - "db" or "sqlite"
'''' "csv" - "xsv" or "csv"
'''' "xls" - "xls
''''
'''' Driver (variant, optional, Empty):
'''' Database driver. If Empty, default values is selected based on
'''' the database type (see constructor code for details).
''''
'''' ConnectionOptions (variant, optional, Empty):
'''' Connection options. See above for details.
''''
'''' N.B.: "xls" backend support is not currently implemented
''''
'''' Examples:
'''' >>> ?DbConnectionString.CreateFileDB("sqlite").ConnectionString
'''' "Driver=SQLite3 ODBC Driver;Database=<Thisworkbook.Path>\SecureADODB.db;SyncPragma=NORMAL;FKSupport=True;"
''''
'''' >>> ?DbConnectionString.CreateFileDB("sqlite").QTConnectionString
'''' "OLEDB;Driver=SQLite3 ODBC Driver;Database=<Thisworkbook.Path>\SecureADODB.db;SyncPragma=NORMAL;FKSupport=True;"
''''
'''' >>> ?DbConnectionString.CreateFileDB("csv").ConnectionString
'''' "Driver={Microsoft Text Driver (*.txt; *.csv)};DefaultDir=<Thisworkbook.Path>;"
''''
'''' >>> ?DbConnectionString.CreateFileDB("xls").ConnectionString
'''' NotImplementedErr
''''
'@Description "Factory for file-based databases"
Public Function CreateFileDb(Optional ByVal FileType As String = "sqlite", _
 Optional ByVal FileName As String = vbNullString, _
 Optional ByVal Driver As Variant = Empty, _
 Optional ByVal ConnectionOptions As Variant = Empty _
 ) As DbConnectionString
 Dim Instance As DbConnectionString
 Set Instance = New DbConnectionString
 Instance.InitFileDB FileType, FileName, Driver, ConnectionOptions
 Set CreateFileDb = Instance
End Function
'@Description "Constructor for file-based databases"
Friend Sub InitFileDB(Optional ByVal FileType As String = "sqlite", _
 Optional ByVal FileName As String = vbNullString, _
 Optional ByVal Driver As String = vbNullString, _
 Optional ByVal ConnectionOptions As Variant = Empty)
 With this
 .DbType = LCase$(FileType)
 .Driver = Driver
 .Options = ConnectionOptions
 Select Case .DbType
 Case "sqlite"
 If Len(Driver) = 0 Then
 .Driver = "SQLite3 ODBC Driver"
 End If
 If IsEmpty(ConnectionOptions) Then
 .Options = "SyncPragma=NORMAL;FKSupport=True;"
 End If
 .DbPath = VerifyOrGetDefaultPath(FileName, Array("db", "sqlite"))
 .ConnectionString = "Driver=" + .Driver + ";" + _
 "Database=" + .DbPath + ";" + _
 .Options
 Case "worksheet", "wsheet"
 .DbPath = VerifyOrGetDefaultPath(FileName, Array("xls"))
 .ConnectionString = .DbPath
 Case "csv"
 .DbPath = VerifyOrGetDefaultPath(FileName, Array("xsv", "csv"))
 Dim DbFileName As String
 DbFileName = Dir$(.DbPath, vbArchive + vbNormal + vbHidden + vbReadOnly + vbSystem)
 .DbPath = Left$(.DbPath, Len(.DbPath) - Len(DbFileName) - 1)
 #If Win64 Then
 .Driver = "Microsoft Access Text Driver (*.txt, *.csv)"
 #Else
 .Driver = "{Microsoft Text Driver (*.txt; *.csv)}"
 #End If
 .ConnectionString = "Driver=" + .Driver + ";" + _
 "DefaultDir=" + .DbPath + ";"
 Case Else
 .ConnectionString = vbNullString
 End Select
 End With
 If this.ConnectionString = vbNullString Then
 Dim errorDetails As TError
 With errorDetails
 .Number = ErrNo.NotImplementedErr
 .Name = "NotImplementedErr"
 .Source = "DbConnectionString"
 .Description = "Unsupported backend: " & FileType
 .Message = .Description
 End With
 RaiseError errorDetails
 End If
End Sub
Public Property Get ConnectionString() As String
 ConnectionString = this.ConnectionString
End Property
Public Property Get QTConnectionString() As String
 QTConnectionString = "OLEDB;" & this.ConnectionString
End Property

DbConnectionString relies on the VerifyOrGetDefaultPath routine, which takes a file pathname candidate (possibly blank) and an array of default extensions. Then it attempts to locate the file, checking default directories and file naming conventions.

'''' Resolves file pathname
''''
'''' This helper routines attempts to interpret provided pathname as
'''' a reference to an existing file:
'''' 1) check if provided reference is a valid absolute file pathname, if not,
'''' 2) construct an array of possible file locations:
'''' - ThisWorkbook.Path & Application.PathSeparator
'''' - Environ("APPDATA") & Application.PathSeparator &
'''' & ThisWorkbook.VBProject.Name & Application.PathSeparator
'''' construct an array of possible file names:
'''' - FilePathName
'''' - ThisWorkbook.VBProject.Name & Ext (Ext comes from the second argument
'''' 3) loop through all possible path/filename combinations until a valid
'''' pathname is found or all options are exhausted
''''
'''' Args:
'''' FilePathName (string):
'''' File pathname
''''
'''' DefaultExts (string or string/array):
'''' 1D array of default extensions or a single default extension
''''
'''' Returns:
'''' String:
'''' Resolved valid absolute pathname pointing to an existing file.
''''
'''' Throws:
'''' Err.FileNotFoundErr:
'''' If provided pathname cannot be resolved to a valid file pathname.
''''
'''' Examples:
'''' >>> ?VerifyOrGetDefaultPath(Environ$("ComSpec"), "")
'''' "C:\Windows\system32\cmd.exe"
''''
'@Description "Resolves file pathname"
Public Function VerifyOrGetDefaultPath(ByVal FilePathName As String, ByVal DefaultExts As Variant) As String
 Dim PATHuSEP As String: PATHuSEP = Application.PathSeparator
 Dim PROJuNAME As String: PROJuNAME = ThisWorkbook.VBProject.Name
 
 Dim FileExist As Variant
 Dim PathNameCandidate As String
 
 '''' === (1) === Check if FilePathName is a valid path to an existing file.
 If Len(FilePathName) > 0 Then
 '''' If matched, Dir returns Len(String) > 0;
 '''' otherwise, returns vbNullString or raises an error
 PathNameCandidate = FilePathName
 On Error Resume Next
 FileExist = FileLen(PathNameCandidate)
 On Error GoTo 0
 If FileExist > 0 Then
 VerifyOrGetDefaultPath = PathNameCandidate
 Exit Function
 End If
 End If
 
 '''' === (2a) === Array of prefixes
 Dim Prefixes As Variant
 Prefixes = Array( _
 ThisWorkbook.Path & PATHuSEP, _
 Environ$("APPDATA") & PATHuSEP & PROJuNAME & PATHuSEP _
 )
 
 '''' === (2b) === Array of filenames
 Dim NameCount As Long
 NameCount = 0
 If Len(FilePathName) > 1 And InStr(FilePathName, PATHuSEP) = 0 Then
 NameCount = NameCount + 1
 End If
 If VarType(DefaultExts) = vbString Then
 If Len(DefaultExts) > 0 Then NameCount = NameCount + 1
 ElseIf VarType(DefaultExts) >= vbArray Then
 NameCount = NameCount + UBound(DefaultExts, 1) - LBound(DefaultExts, 1) + 1
 Debug.Assert VarType(DefaultExts(0)) = vbString
 End If
 If NameCount = 0 Then
 VBA.Err.Raise _
 Number:=ErrNo.FileNotFoundErr, _
 Source:="CommonRoutines", _
 Description:="File <" & FilePathName & "> not found!"
 End If
 
 Dim FileNames() As String
 ReDim FileNames(0 To NameCount - 1)
 Dim ExtIndex As Long
 Dim FileNameIndex As Long
 FileNameIndex = 0
 If Len(FilePathName) > 1 And InStr(FilePathName, PATHuSEP) = 0 Then
 FileNames(FileNameIndex) = FilePathName
 FileNameIndex = FileNameIndex + 1
 End If
 If VarType(DefaultExts) = vbString Then
 If Len(DefaultExts) > 0 Then
 FileNames(FileNameIndex) = PROJuNAME & "." & DefaultExts
 End If
 ElseIf VarType(DefaultExts) >= vbArray Then
 For ExtIndex = LBound(DefaultExts, 1) To UBound(DefaultExts, 1)
 FileNames(FileNameIndex) = PROJuNAME & "." & DefaultExts(ExtIndex)
 FileNameIndex = FileNameIndex + 1
 Next ExtIndex
 End If
 
 '''' === (3) === Loop through pathnames
 Dim PrefixIndex As Long
 
 On Error Resume Next
 For PrefixIndex = 0 To UBound(Prefixes)
 For FileNameIndex = 0 To UBound(FileNames)
 PathNameCandidate = Prefixes(PrefixIndex) & FileNames(FileNameIndex)
 FileExist = FileLen(PathNameCandidate)
 Err.Clear
 If FileExist > 0 Then
 VerifyOrGetDefaultPath = Replace$(PathNameCandidate, _
 PATHuSEP & PATHuSEP, PATHuSEP)
 Exit Function
 End If
 Next FileNameIndex
 Next PrefixIndex
 On Error GoTo 0
 
 VBA.Err.Raise _
 Number:=ErrNo.FileNotFoundErr, _
 Source:="CommonRoutines", _
 Description:="File <" & FilePathName & "> not found!"
End Function

DbMetaData

DbMetaData currently provides a routine, which takes a table name and returns field names, types, and a mapping from field name to its position.

'@Folder "SecureADODB.DbManager.DbMetaData"
'@ModuleDescription "Database introspection functionality"
'@PredeclaredId
'@Exposed
Option Explicit
Private Type TDbMeta
 DbConnStr As DbConnectionString
End Type
'@Ignore MoveFieldCloserToUsage: Follow the standard pattern
Private this As TDbMeta
'@Description "Default factory"
Public Function Create(ByVal DbConnStr As DbConnectionString) As DbMetaData
 Dim Instance As DbMetaData
 Set Instance = New DbMetaData
 Instance.Init DbConnStr
 Set Create = Instance
End Function
'@Description "Default constructor"
Friend Sub Init(ByVal DbConnStr As DbConnectionString)
 Guard.NullReference DbConnStr
 Set this.DbConnStr = DbConnStr
End Sub
'@Ignore ParameterCanBeByVal: False positive
'@Description "Query table field metadata via the ADOX library"
Public Sub QueryTableADOXMeta(ByVal TableName As String, _
 ByRef FieldNames As Variant, _
 ByRef FieldTypes As Variant, _
 ByVal FieldMap As Scripting.Dictionary)
 Dim Catalog As ADOX.Catalog
 Set Catalog = New ADOX.Catalog
 Catalog.ActiveConnection = this.DbConnStr.ConnectionString
 Dim Table As ADOX.Table
 '@Ignore IndexedDefaultMemberAccess
 Set Table = Catalog.Tables(TableName)
 Dim FieldCount As Long
 FieldCount = Table.Columns.Count
 
 ReDim FieldNames(1 To FieldCount)
 ReDim FieldTypes(1 To FieldCount)
 
 Dim Column As ADOX.Column
 Dim FieldIndex As Long
 For FieldIndex = 1 To FieldCount
 '@Ignore IndexedDefaultMemberAccess
 Set Column = Table.Columns(FieldIndex - 1)
 FieldNames(FieldIndex) = Column.Name
 FieldTypes(FieldIndex) = Column.Type
 '@Ignore IndexedDefaultMemberAccess
 FieldMap(Column.Name) = FieldIndex
 Next FieldIndex
End Sub
asked Aug 30, 2021 at 16:35
\$\endgroup\$

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.