I built a workbook to help me extract data from a MSSQL database into Excel. I realize that this is fairly easy to do with Microsoft SQL Server Management Studio, but I can't easily get that installed on my machine at work, so I first have to remote to the server every time to get any data, which becomes a mission if you have to do it often.
The workbook has a small control sheet as follows:
The button calls Sub ExecSQL()
which will scan this sheet for SQL statements, execute them and then paste the results into the corresponding sheet. In this example, it will execute one SELECT
statement and paste the result into Fct201712
.
I know about SQL injection, but given that I won't be giving this out to users and the SQL auths I use anyway do not have write access to the database, I'm not too concerned about that.
I'm trying to implement some of the suggestions I received on this previous question. Given how I now handle errors, I am not closing the DB connection if I get an error. How can I elegantly structure my code so that I always close the connection even if I get an error without using GoTo
?
Any other advice on how to improve this code will be greatly appreciated. Also, if you think I'm making the code too complicated, please let me know
Here is the full module:
Option Explicit
Sub ExecSQL()
Dim ActSh As Worksheet
Set ActSh = ActiveSheet
'Connect to the database
Dim Conn As ADODB.Connection
Set Conn = NewDBConnection()
'Get the SQL statements and Worksheets from this sheet
Dim Stmt As Scripting.Dictionary
Set Stmt = GetStatements()
If Not CheckStatements(Stmt) Then Exit Sub
'Execute the SQL commands and paste the results
Dim Sh As Variant
For Each Sh In Stmt.Keys()
If Not ExecSQLStmt(Conn, Sh, Stmt(Sh)) Then Exit Sub
Next
'Clean up
Conn.Close
Set Conn = Nothing
ActSh.Activate
MsgBox "SQL statement execution completed", vbInformation + vbOKOnly, "Completed"
End Sub
Private Function NewDBConnection() As ADODB.Connection
Dim ConStr As String
ConStr = "" _
& "Provider=SQLOLEDB.1;" _
& "Password={redacted};" _
& "Persist Security Info=True;" _
& "User ID={redacted};" _
& "Initial Catalog={redacted};" _
& "Data Source={redacted};" _
& "Use Procedure for Prepare=1;" _
& "Auto Translate=True;" _
& "Packet Size=4096;" _
& "Workstation ID=W530;" _
& "Use Encryption for Data=False;" _
& "Tag with column collation when possible=False"
Dim Conn As ADODB.Connection
Set Conn = New ADODB.Connection
Conn.Open ConStr
Set NewDBConnection = Conn
End Function
Private Function GetStatements() As Scripting.Dictionary
Dim Rng As Range
Set Rng = ActiveSheet.UsedRange
Dim Row As Long
Dim RowHdr As Long
Dim RowCount As Long
RowHdr = 0
RowCount = Rng.Rows.Count
Dim Col As Long
Dim ColSh As Long
Dim ColSQL As Long
Dim ColCount As Long
ColSh = 0
ColSQL = 0
ColCount = Rng.Columns.Count
'Get the header row and applicable columns
Dim ValHdr As String
For Row = 1 To RowCount
For Col = 1 To ColCount
ValHdr = UCase(Trim(GetStrValue(Rng.Cells(Row, Col))))
If ValHdr = "!SHEET" Then
RowHdr = Row
ColSh = Col
ElseIf ValHdr = "!SQL" Then
RowHdr = Row
ColSQL = Col
End If
Next
If RowHdr > 0 Then Exit For
Next
'Scan the rows for any applicable entries
Dim Stmt As Scripting.Dictionary
Set Stmt = New Scripting.Dictionary
Dim ValSh As String
Dim ValSQL As String
If ColSh > 0 And ColSQL > 0 Then
For Row = RowHdr + 1 To RowCount
ValSh = Trim(GetStrValue(Rng.Cells(Row, ColSh)))
ValSQL = Trim(GetStrValue(Rng.Cells(Row, ColSQL)))
If ValSh <> "" And ValSQL <> "" Then
Stmt(ValSh) = ValSQL
End If
Next
End If
Set GetStatements = Stmt
End Function
Private Function CheckStatements(Stmt As Scripting.Dictionary) As Boolean
Dim ErrStr As String
ErrStr = ""
If Stmt.Count = 0 Then
ErrStr = "Could not find any SQL statements on the current sheet." _
& vbCrLf _
& "Did you remember to add ""!Sheet"" and ""!SQL"" header tags?"
End If
If ErrStr = "" Then
CheckStatements = True
Else
MsgBox ErrStr, vbCritical + vbOKOnly, "Error"
CheckStatements = False
End If
End Function
Private Function GetStrValue(Rng As Range) As String
'Get the value of a cell, but do not throw and error if the cell
'contains and error. Intead, just return an empty string
Dim Val As String
Val = ""
On Error Resume Next
Val = Rng.Value
On Error GoTo 0
GetStrValue = Val
End Function
Private Function ExecSQLStmt(Conn As ADODB.Connection, ByVal ShName As String, SQLStmt As String) As Boolean
'Execute the SQL statement and paste the result into the corresponding sheet
Dim Sh As Worksheet
'Delete the sheet if it already exists
On Error Resume Next
Set Sh = ActiveWorkbook.Worksheets(ShName)
On Error GoTo 0
If Not Sh Is Nothing Then
Application.DisplayAlerts = False
Sh.Delete
Application.DisplayAlerts = True
End If
'Create the sheet
With ActiveWorkbook
Set Sh = .Sheets.Add(After:=.Sheets(.Sheets.Count))
End With
Sh.Name = ShName
'Execute the SQL statement
Dim Rs As ADODB.Recordset
On Error Resume Next
Set Rs = Conn.Execute(SQLStmt)
If Rs Is Nothing Then
Dim ErrStr As String
ErrStr = "There was an error executing the SQL statement" & vbCrLf _
& SQLStmt & vbCrLf _
& vbCrLf _
& "Error: " & Err.Description
MsgBox ErrStr, vbCritical + vbOKOnly, "Error"
ExecSQLStmt = False
Exit Function
End If
On Error GoTo 0
'Paste the result into the sheet
Dim Col As Long
For Col = 1 To Rs.Fields.Count
Sh.Cells(1, Col).Value = Rs.Fields(Col - 1).Name
Next
Sh.Cells(1, 1).EntireRow.Font.Bold = True
Sh.Range("A2").CopyFromRecordset Rs
ExecSQLStmt = True
End Function