スポンサーリンク
IT全般プログラミング

【DB運用】ROLLBACK前提でINSERT/UPDATEを検証できるExcelツールを作った

IT全般
この記事は約21分で読めます。

はじめに

DB運用で INSERT や UPDATE を行うとき、
いちばん怖いのは「本当にこのSQLを流して大丈夫か?」という不安だと思います。

  • WHERE 条件を一つ書き間違えただけで全件更新
  • INSERT が混ざって、結果が見づらくなる
  • Excelから直接DBを更新する設計にはしたくない

こうした経験から、今回私は次の方針でツールを作りました。

Excelは「更新用SQLを作るだけ」
実テーブルは決して更新しない

この記事では、そのツールの仕組みと便利さを簡単に紹介し、
最後にソースコード全文を掲載します。


このツールのコンセプト

このExcelツールは「DB更新ツール」ではありません。

❌ やらないこと

  • ExcelからDBを直接更新しない
  • UPDATE / INSERT を自動実行しない
  • COMMIT を流さない

✅ やること

  • ExcelをUIとして使う
  • INSERT / UPDATE 用のSQLを自動生成する
  • ROLLBACK前提の検証用SQLを生成する

流れとしては、非常にシンプルです。

Excelで編集
 ↓
INSERT / UPDATE 用SQLを生成
 ↓
SSMSなどでSQLを実行して確認
 ↓
問題なければ COMMIT(人が判断)

「便利さ」と「安全性」を両立するための設計です。

このツールでは、A列を「更新区分」として使っています。

意味
IINSERT
UUPDATE
空白変更なし

ただし、ここが重要なポイントです。

I / U は手入力しません

Worksheet_Change による自動判定

セルを編集すると Worksheet_Change が発火し、

  • ORIGINAL(取得時点のデータ)に存在しない行
    INSERT(I)
  • ORIGINAL と値が変わった行
    UPDATE(U)
  • 元に戻した行
    → 自動的に解除

という判定を 完全に自動で行います。

そのため、

  • I / U の付け忘れ
  • U を消し忘れる
  • 意図しないSQL生成

といった事故を防げます。

見た目にも分かりやすく

Excel上では次のように視覚的に分かるようにしています。

  • INSERT 行:薄いグリーン
  • UPDATE 行:薄いオレンジ
  • 変更されたセル:赤文字+太字

Excelを開いた瞬間に、

「どの行がINSERT対象で、どの行がUPDATE対象か」

が一目で分かります。

ROLLBACK前提で「更新前・更新後・差分」を確認できる

このツール最大のポイントがここです。

生成されるSQLは、単なる UPDATE / INSERT 文ではなく、
必ず ROLLBACK 前提の検証用SQLになります。

構成としては、次のような流れです。

BEGIN TRANSACTION;

-- 更新前の状態
SELECT * INTO #Before FROM テーブル名;

-- INSERT / UPDATE(Excelが生成)
INSERT INTO ...
UPDATE ...

-- 更新後の状態
SELECT * INTO #After FROM テーブル名;

-- 差分
SELECT * FROM #After
EXCEPT
SELECT * FROM #Before;

ROLLBACK TRANSACTION;

この方式のメリット

  • 実DBを使って「本番と同じ条件」で確認できる
  • SQLを実行しても 実データは一切変わらない
  • 更新前/更新後/差分をすべて目視できる

つまり、

DB更新の“リハーサル”が安全にできる

という状態になります。

INSERTがある場合の工夫(行ズレ防止)

INSERT が含まれると、
単純な SELECT * FROM テーブル では行順がズレてしまい、
更新前後の比較が非常にやりづらくなります。

そこでこのツールでは、

  • 元から存在していたデータ
  • INSERTされたデータ

SQL上で分けて表示することで、

  • 既存データは同じ順序
  • INSERT行は最後にまとまって表示

されるようにしています。

これにより、
✅ 実行前後の比較
✅ 差分確認
がかなり楽になります。


本番実行はどうするのか?

本番で更新する場合も、Excel側で特別なことはしません。

  1. ExcelでSQLを生成
  2. SSMSでコピペして実行
  3. 問題がなければ
ROLLBACK TRANSACTION;

COMMIT TRANSACTION;

人が書き換えるだけです。

Excelが勝手にDBを更新することはありません。


このツールを作って良かった点

  • SQL更新に対する心理的ハードルが下がった
  • UPDATE事故の不安が減った
  • 非DBエンジニアにもレビューしてもらいやすい
  • Excelを「危険なDB更新ツール」にしなくて済んだ

「安全にSQLを確認できる」
という安心感は、想像以上に大きいです。


まとめ

  • ExcelはDBを更新しない
  • INSERT / UPDATE は SQL として生成するだけ
  • ROLLBACK前提で更新前後・差分を検証する

この3点を守るだけで、
DB運用はかなり安全になります。

ソースコード全文

以下に、本記事で紹介した
Excel VBA ソースコードの全文を掲載します。

(※ 接続文字列や環境依存部分は適宜置き換えてください)

① Sheet モジュール(Worksheet_Change だけ

✅ I / U 自動判定
✅ 行色・差分セル強調
✅ 多セル変更ガード、EnableEvents 安全復帰済み

Private Sub Worksheet_Change(ByVal Target As Range)

    ' 複数セル変更は無視(貼り付け事故防止)
    If Target.Rows.Count > 1 Or Target.Columns.Count > 1 Then Exit Sub

    Dim wsOrg As Worksheet
    Dim r As Long, c As Long
    Dim lastCol As Long
    Dim orgLastRow As Long
    Dim hasDiff As Boolean, hasData As Boolean
    Dim newVal As String, orgVal As String

    On Error GoTo CLEANUP
    Application.EnableEvents = False

    Set wsOrg = Worksheets("_ORIGINAL")
    If wsOrg Is Nothing Then GoTo CLEANUP

    ' データ領域以外は無視
    If Intersect(Target, _
        Range(Cells(DATA_START_ROW, DATA_START_COL), _
              Cells(Rows.Count, Columns.Count))) Is Nothing Then GoTo CLEANUP

    r = Target.Row
    lastCol = Cells(HEADER_ROW, Columns.Count).End(xlToLeft).Column
    orgLastRow = wsOrg.Cells(wsOrg.Rows.Count, DATA_START_COL).End(xlUp).Row

    ' -------------------------
    ' INSERT 判定(ORIGINALに無い行)
    ' -------------------------
    hasData = False
    For c = DATA_START_COL To lastCol
        If Trim(Cells(r, c).Value) <> "" Then
            hasData = True
            Exit For
        End If
    Next c

    If r > orgLastRow Then
        If hasData Then
            Cells(r, MODE_COL).Value = "I"
            Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.Color = COLOR_INSERT_ROW
        Else
            Cells(r, MODE_COL).ClearContents
            Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.ColorIndex = xlNone
        End If
        UpdateExtractCounts
        GoTo CLEANUP
    End If

    ' -------------------------
    ' UPDATE 判定(差分ベース)
    ' -------------------------
    hasDiff = False
    For c = DATA_START_COL To lastCol
        newVal = Trim(Cells(r, c).Value)
        orgVal = Trim(wsOrg.Cells(r, c).Value)

        If newVal <> orgVal Then
            hasDiff = True
            With Cells(r, c).Font
                .Color = COLOR_CHANGED_FONT
                .Bold = True
            End With
        Else
            With Cells(r, c).Font
                .ColorIndex = xlAutomatic
                .Bold = False
            End With
        End If
    Next c

    If hasDiff Then
        Cells(r, MODE_COL).Value = "U"
        Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.Color = COLOR_UPDATE_ROW
    Else
        Cells(r, MODE_COL).ClearContents
        Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.ColorIndex = xlNone
    End If

    UpdateExtractCounts

CLEANUP:
    Application.EnableEvents = True
End Sub

② 標準モジュール(それ以外全部

✅ 定数・ユーティリティ
✅ データ取得/ORIGINAL退避
✅ 件数集計
実更新しない INSERT/UPDATE SQL生成
ROLLBACK前提 検証用SQL出力
✅ SQLシートは常に最後/全行ハイライト

Option Explicit

'=================================================
' 定数
'=================================================
Public Const HEADER_ROW       As Long = 9
Public Const COMMENT_ROW      As Long = 10
Public Const TYPE_ROW         As Long = 11
Public Const DATA_START_ROW   As Long = 12

Public Const MODE_COL         As Long = 1   ' A列(I/U)
Public Const DATA_START_COL   As Long = 2   ' B列~

Public Const UI_TABLE_ROW     As Long = 3
Public Const UI_TABLE_COL     As Long = 2   ' B3
Public Const UI_TOTAL_ROW     As Long = 5
Public Const UI_UPDATE_ROW    As Long = 6
Public Const UI_INSERT_ROW    As Long = 7
Public Const UI_VALUE_COL     As Long = 2   ' B列

Public Const COLOR_UPDATE_ROW As Long = &HCCF2FF
Public Const COLOR_INSERT_ROW As Long = &HDAEFDA
Public Const COLOR_CHANGED_FONT As Long = &HC0

'=================================================
' ユーティリティ
'=================================================
Private Function Nz(v As Variant) As String
    If IsNull(v) Then Nz = "" Else Nz = CStr(v)
End Function

Public Function IsNumericType(dataType As String) As Boolean
    Select Case LCase(dataType)
        Case "int", "bigint", "smallint", "tinyint", _
             "decimal", "numeric", "float", "real", _
             "money", "smallmoney"
            IsNumericType = True
        Case Else
            IsNumericType = False
    End Select
End Function

'=================================================
' 主キー取得(※接続情報は各自の環境で)
'=================================================
Public Function GetPrimaryKeys(conn As Object, tableName As String) As Collection
    Dim rs As Object, sql As String
    Dim pk As New Collection
    sql = _
        "SELECT c.name FROM sys.key_constraints kc " & _
        "JOIN sys.index_columns ic ON kc.parent_object_id=ic.object_id " & _
        " AND kc.unique_index_id=ic.index_id " & _
        "JOIN sys.columns c ON ic.object_id=c.object_id " & _
        " AND ic.column_id=c.column_id " & _
        "WHERE kc.type='PK' AND kc.parent_object_id=OBJECT_ID('" & tableName & "') " & _
        "ORDER BY ic.key_ordinal"
    Set rs = conn.Execute(sql)
    Do While Not rs.EOF
        pk.Add CStr(rs(0))
        rs.MoveNext
    Loop
    rs.Close
    Set GetPrimaryKeys = pk
End Function

'=================================================
' 件数集計(UI表示)
'=================================================
Public Sub UpdateExtractCounts()
    Dim lastRow As Long, lastCol As Long
    Dim r As Long, c As Long
    Dim totalCnt As Long, updateCnt As Long, insertCnt As Long
    Dim hasData As Boolean

    lastCol = Cells(HEADER_ROW, Columns.Count).End(xlToLeft).Column
    lastRow = Cells(Rows.Count, DATA_START_COL).End(xlUp).Row

    For r = DATA_START_ROW To lastRow
        hasData = False
        For c = DATA_START_COL To lastCol
            If Trim(Cells(r, c).Value) <> "" Then
                hasData = True: Exit For
            End If
        Next c
        If hasData Then
            totalCnt = totalCnt + 1
            If Cells(r, MODE_COL).Value = "U" Then updateCnt = updateCnt + 1
            If Cells(r, MODE_COL).Value = "I" Then insertCnt = insertCnt + 1
        End If
    Next r

    Cells(UI_TOTAL_ROW, UI_VALUE_COL).Value = totalCnt
    Cells(UI_UPDATE_ROW, UI_VALUE_COL).Value = updateCnt
    Cells(UI_INSERT_ROW, UI_VALUE_COL).Value = insertCnt
End Sub

'=================================================
' SQL生成(実更新しない)
'=================================================
Public Sub GenerateInsertUpdateSQL_ToSheet()

    Dim ws As Worksheet, conn As Object
    Dim tableName As String
    Dim lastRow As Long, lastCol As Long
    Dim r As Long, col As Long
    Dim mode As String, value As String, orgVal As String
    Dim colName As String, dataType As String
    Dim colList As String, valList As String
    Dim setClause As String, whereClause As String
    Dim pk As Collection, pkName As Variant
    Dim sqlBody As Collection: Set sqlBody = New Collection

    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual
    On Error GoTo CLEANUP

    Set ws = ActiveSheet
    tableName = Trim(ws.Cells(UI_TABLE_ROW, UI_TABLE_COL).Value)
    If tableName = "" Then GoTo CLEANUP

    lastCol = ws.Cells(HEADER_ROW, ws.Columns.Count).End(xlToLeft).Column
    lastRow = ws.Cells(ws.Rows.Count, DATA_START_COL).End(xlUp).Row

    ' PK取得(※接続情報は各自置換)
    Set conn = CreateObject("ADODB.Connection")
    conn.Open "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;User ID=USER;Password=PASSWORD;"
    Set pk = GetPrimaryKeys(conn, tableName)
    conn.Close

    For r = DATA_START_ROW To lastRow
        mode = UCase(Trim(ws.Cells(r, MODE_COL).Value))
        If mode <> "I" And mode <> "U" Then GoTo NextRow

        colList = "": valList = "": setClause = "": whereClause = ""

        If mode = "I" Then
            For col = DATA_START_COL To lastCol
                colName = ws.Cells(HEADER_ROW, col).Value
                dataType = ws.Cells(TYPE_ROW, col).Value
                value = Trim(ws.Cells(r, col).Value)

                colList = colList & colName & ", "
                If IsNumericType(dataType) Then
                    valList = valList & value & ", "
                Else
                    valList = valList & "'" & Replace(value, "'", "''") & "', "
                End If
            Next col
            sqlBody.Add "INSERT INTO " & tableName & _
                        " (" & Left(colList, Len(colList)-2) & _
                        ") VALUES (" & Left(valList, Len(valList)-2) & ");"
        End If

        If mode = "U" Then
            For col = DATA_START_COL To lastCol
                value = Trim(ws.Cells(r, col).Value)
                orgVal = Trim(Worksheets("_ORIGINAL").Cells(r, col).Value)
                If value <> orgVal Then
                    setClause = setClause & ws.Cells(HEADER_ROW, col).Value & _
                                " = '" & Replace(value, "'", "''") & "', "
                End If
            Next col

            If setClause <> "" Then
                For Each pkName In pk
                    For col = DATA_START_COL To lastCol
                        If ws.Cells(HEADER_ROW, col).Value = pkName Then
                            whereClause = whereClause & pkName & _
                                          " = '" & ws.Cells(r, col).Value & "' AND "
                        End If
                    Next col
                Next pkName

                sqlBody.Add "UPDATE " & tableName & _
                            " SET " & Left(setClause, Len(setClause)-2) & _
                            " WHERE " & Left(whereClause, Len(whereClause)-5) & ";"
            End If
        End If
NextRow:
    Next r

    If sqlBody.Count > 0 Then OutputVerificationSQL sqlBody, tableName

CLEANUP:
    Application.ScreenUpdating = True
    Application.EnableEvents = True
    Application.Calculation = xlCalculationAutomatic
End Sub

'=================================================
' 検証用SQL出力(ROLLBACK前提・1行ずつ)
'=================================================
Public Sub OutputVerificationSQL(sqlBody As Collection, tableName As String)

    Dim wsOut As Worksheet
    Dim r As Long, i As Long

    On Error Resume Next
    Worksheets("SQL").Delete
    On Error GoTo 0

    Set wsOut = Worksheets.Add(After:=Worksheets(Worksheets.Count))
    wsOut.Name = "SQL"
    r = 1

    wsOut.Cells(r, 1).Value = "BEGIN TRANSACTION;": r = r + 2
    wsOut.Cells(r, 1).Value = "SELECT * INTO #Before FROM " & tableName & ";": r = r + 2

    For i = 1 To sqlBody.Count
        wsOut.Cells(r, 1).Value = sqlBody(i)
        r = r + 1
    Next i

    wsOut.Cells(r, 1).Value = "SELECT * INTO #After FROM " & tableName & ";": r = r + 2
    wsOut.Cells(r, 1).Value = "SELECT * FROM #After EXCEPT SELECT * FROM #Before;": r = r + 2
    wsOut.Cells(r, 1).Value = "ROLLBACK TRANSACTION;"

    With wsOut.Range(wsOut.Cells(1, 1), wsOut.Cells(r, 1))
        .Interior.Color = RGB(226, 239, 218)
    End With
    wsOut.Columns(1).AutoFit
End Sub

コメント

タイトルとURLをコピーしました