Following-up on Creating ADODB Parameters on the fly and pushing the "wrapping" of ADODB a step further, I have written two more classes that allows me to expose methods that don't require a Connection
object, without returning an ADODB.Recordset
.
Taking this method as a reference:
Public Function Execute(connection As ADODB.connection, ByVal sql As String, ParamArray parametervalues()) As ADODB.Recordset Dim Values() As Variant Values = parametervalues Dim cmd As ADODB.Command Set cmd = CreateCommand(connection, adCmdText, sql, Values) Set Execute = cmd.Execute End Function
A bit of context
I'm not comfortable with the idea of exposing a method that would return an ADODB.Recordset
without taking in an ADODB.Connection
, because this would mean opening a connection in a function that doesn't control when the connection needs to be closed.
To address this issue, I added two private fields to my SqlCommand
:
Private connString As String Private resultFactory As New SqlResult
I'm using a pre-determined connection string in Class_Initialize
for the connString
value:
Private Sub Class_Initialize() connString = Application.ConnectionString End Sub
I adopted the "Quick" prefix to refer to an "overload" method that owns its own connection, hence the connection-less "overload" for the Execute
method above will be called QuickExecute
:
Public Function QuickExecute(ByVal sql As String, ParamArray parametervalues()) As SqlResult Dim parameters() As Variant parameters = parametervalues Dim connection As New ADODB.connection connection.ConnectionString = connString connection.Open Dim rs As ADODB.Recordset Set rs = Execute(connection, sql, parameters) Set QuickExecute = resultFactory.Create(rs) rs.Close Set rs = Nothing connection.Close Set connection = Nothing End Function
The method consumes the recordset and returns an object that encapsulates its contents, a SqlResult
object.
SqlResult
This type encapsulates a List<string>
and a List<SqlResultRow>
(see List class here), respectively holding field names and field values for each row.
Property Item
has a procedure attribute that makes it the type's default property, and a procedure attribute of -4 on property NewEnum
allows iterating the SqlResultRow
items with a For Each
loop, like this:
Dim sql As String sql = "SELECT TOP 10 * FROM SomeTable" Dim cmd As New SqlCommand Dim result As SqlResult Set result = cmd.QuickExecute(sql) Dim row As SqlResultRow For Each row In result Debug.Print row("SomeFieldName"), TypeName(row("SomeFieldName")) Next
Here's the code:
Private Type tSqlResult
FieldNames As List
Values As List
ToStringValueSeparator As String
End Type
Private this As tSqlResult
Option Explicit
Private Sub Class_Initialize()
Set this.FieldNames = New List
Set this.Values = New List
this.ToStringValueSeparator = ","
End Sub
Public Property Get ValueSeparator() As String
ValueSeparator = this.ToStringValueSeparator
End Property
Public Property Let ValueSeparator(ByVal value As String)
this.ToStringValueSeparator = value
End Property
Public Sub AddFieldName(name As String)
this.FieldNames.Add name
End Sub
Public Function FieldNameIndex(ByVal name As String) As Long
FieldNameIndex = this.FieldNames.IndexOf(LCase$(name)) - 1
End Function
Public Sub AddValue(value As SqlResultRow)
this.Values.Add value
End Sub
Public Property Get Count() As Long
Count = this.Values.Count
End Property
Public Property Get Item(ByVal index As Long) As SqlResultRow
Set Item = this.Values(index + 1)
End Property
Public Property Get NewEnum() As IUnknown
'Gets an enumerator that iterates through the List.
Set NewEnum = this.Values.NewEnum
End Property
Public Function Create(adoRecordset As ADODB.Recordset) As SqlResult
Dim result As New SqlResult
Dim names As New List
Dim fieldValues As New List
Dim row As ADODB.fields
Dim field As ADODB.field
Dim rowFactory As New SqlResultRow
Dim grabFieldName As Boolean
grabFieldName = True
While Not adoRecordset.BOF And Not adoRecordset.EOF
For Each field In adoRecordset.fields
If grabFieldName Then result.AddFieldName LCase$(Coalesce(field.name, vbNullString))
Next
result.AddValue rowFactory.Create(result, adoRecordset.fields)
grabFieldName = False
adoRecordset.MoveNext
Wend
Set Create = result
End Function
SqlResultRow
Each row encapsulates an array of Variant
values, and has an Item
property (which also has a procedure attribute that makes it the type's default property) that can take either a String
representing a field's name, or any number representing a field's index. A ToString
method conveniently outputs all field values separated by commas (the actual separator is configurable in the SqlResult
class).
Private Type tRow
ParentResult As SqlResult
Values() As Variant
IsEmpty As Boolean
End Type
Private this As tRow
Option Explicit
Private Sub Class_Initialize()
ReDim this.Values(0 To 0)
this.IsEmpty = True
End Sub
Public Property Set ParentResult(value As SqlResult)
Set this.ParentResult = value
End Property
Friend Sub AddValue(ByVal value As Variant)
If Not this.IsEmpty Then ReDim Preserve this.Values(0 To UBound(this.Values) + 1)
this.Values(UBound(this.Values)) = value
this.IsEmpty = False
End Sub
Public Property Get Item(nameOrIndex As Variant) As Variant
If TypeName(nameOrIndex) = "String" Then
Item = GetFieldValueByName(nameOrIndex)
ElseIf IsNumeric(nameOrIndex) Then
Item = GetFieldValueByIndex(nameOrIndex)
Else
'return empty variant
End If
End Property
Private Function GetFieldValueByName(ByVal name As String) As Variant
If Not this.IsEmpty Then GetFieldValueByName = this.Values(this.ParentResult.FieldNameIndex(name))
End Function
Private Function GetFieldValueByIndex(ByVal index As Integer) As Variant
If Not this.IsEmpty Then GetFieldValueByIndex = this.Values(index)
End Function
Public Function Create(parent As SqlResult, fields As ADODB.fields) As SqlResultRow
Dim result As New SqlResultRow
Set result.ParentResult = parent
Dim field As ADODB.field
Dim value As Variant
For Each field In fields
If TypeName(field.value) = "String" Then
value = LTrim(RTrim(Coalesce(field.value, vbNullString)))
Else
value = Coalesce(field.value, vbEmpty)
End If
result.AddValue value
Next
Set Create = result
End Function
Public Function ToString() As String
If this.IsEmpty Then
ToString = TypeName(Me)
Exit Function
End If
Dim result As String
result = Join(this.Values, this.ParentResult.ValueSeparator)
ToString = result
End Function
The types are retained, so if a query returns a Date
field, the type of that value will be Date
in the SqlResultRow
.
I use a small helper function, Coalesce
, to deal with null
values. For reference, here's the listing:
Public Function Coalesce(ByVal value As Variant, Optional ByVal value_when_null As Variant = 0) As Variant Dim return_value As Variant On Error Resume Next 'supress error handling If IsEmpty(value) Or IsNull(value) Or (TypeName(value) = "String" And value = vbNullString) Then return_value = value_when_null Else return_value = value End If Err.Clear 'clear any errors that might have occurred On Error GoTo 0 'reinstate error handling Coalesce = return_value End Function
WHERE
clause in the query; if I need sorted results I can have anORDER BY
clause... Why not let the database do the hard work for me? \$\endgroup\$