先前談過防禦式 SQL 更新 - 用更嚴謹的態度執行正式資料庫更新作業,滿足保留資料更新前後對照、過程與預期不同自動煞車、方便檢閱複核... 等要求。純 SQL 語法簡單明瞭,但施行有些前題:操作環境必須有安裝 GUI 或 Command Line 的 SQL Client 工具、操作人員必須具備 SQL 工具基本技巧。此外,若系統有自己的連線字串保存機制,帳號密碼僅限程式存取不該透露給操作人員,但缺少帳號密碼又無法操作 SQL Client 工具,就會形成矛盾。

我想到的解法是為該次資料更新作業撰寫一段專屬程式,如此可從自訂機制取連線字串不經過人手,程式能跑各式複雜 SQL 更新邏輯,察覺有異自動煞車,甚至留下是否要 Commit 的人工發射鈕(延伸閱讀:小心駛得萬年船 - SQL指令保險栓),彈性遠勝純 SQL。對我來說,寫程式用 C# 寫是首選,但先前說過,PowerShell 免編譯免部署又以明碼示人便於人工審核,是能取代 C# 在艱苦環境野外求生的利器,所以就用 PowerShell 來寫吧!

以下範例使用 PowerShell 搭 Managed ODP.NET 更新一筆資料,如要更新 MSSQL 原理相似,亦可照方煎藥。

Add-Type -Path .\CnStrManager.dll #某種自訂連線字串保管機制
Add-Type -Path .\Oracle.ManagedDataAccess.dll
try {
    $cs = [TheNamespace.CnStrManager.Helper]::GetConnString("MYDB")
    $cn = New-Object Oracle.ManagedDataAccess.Client.OracleConnection($cs)
    $cn.Open()
    $trn = $cn.BeginTransaction()
    $cmd = $cn.CreateCommand()
    $cmd.Transaction = $trn
    $cmd.CommandText = "select n from t where i = 1"
    $dr = $cmd.ExecuteReader()
    if (!$dr.Read()) { throw "更新前查無資料" }
    $origChk = $dr.Item('n')
    Write-Host "更新前檢查 = $($origChk)" -ForegroundColor Yellow
    if ($origChk -ne "Jeffrey") {
        throw "執行前檢查不符: $($origChk) vs Jeffrey" 
    }

    #執行更新
    $cmd.CommandText = "update t set n = 'Updated' where i = 1";
    $rowCountAffected = $cmd.ExecuteNonQuery()

    #檢查筆數
    if ($rowCountAffected -ne 1) {
        throw "執行筆數不為 1 : $($rowCountAffected)"
    }

    #檢查更新後結果
    $cmd.CommandText = "select n from t where i = 1"
    $dr = $cmd.ExecuteReader()
    if (!$dr.Read()) { throw "更新後查無資料" }
    $updChk = $dr.Item('n')
    Write-Host "更新後檢查 = $($updChk)" -ForegroundColor Yellow
    if ($updChk -ne "Updated") {
        throw "執行後檢查不符: $($updChk) vs Updated"
    }
    Write-Host "檢查結果相符: $($updChk) vs Updated" -ForegroundColor Green
    $confirm = Read-Host "是否確認 Commit? Y/N"
    if ($confirm.ToUpper() -eq 'Y') {
        $trn.Commit()
        Write-Host "資料庫更新動作完成" -ForegroundColor Cyan
    }
    else {
        throw "操作人員放棄更新"
    }
}
catch {
    Write-Host '執行失敗' -ForegroundColor Red
    Write-Host $_ -ForegroundColor Red
    if ($trn) { 
        $trn.Rollback() 
    }  
}
finally {
    if ($cn) { $cn.Close() }
}

在程式裡我加了四道保險:

  • 更新前檢查資料內容是否符合預期,並留下證據
  • 更新時比對異動筆數是否符合預期
  • 更新後檢查資料是否如預期改成新值,並留下證據
  • 是否 Commit Transaction 由人工做最後判別

以上四者任一關卡未通過就 Transaction.Rollback 放棄更新。若執行成功,擷取畫面可當成呈堂證供。

實測範例如下,示範 1) 修改前內容不符 2) 找不到要更新資料 3) 操作人員放棄 4) 成功更新 四種情境。

只需把握「時時檢核、保留證據、出錯還原」原則,程式可依需求自行發揮調整,滿足大部分需求。

同場加映:執行多個 SQL 指令的寫法。

方法一,以 begin ... end; 包含多組更新指令,透過 Output Parameter 取回影響筆數:

    #執行多行更新
    $cmd.CommandText = @"
begin    
    update t set n = 'Updated' where i = 1;
    :rowNum := sql%rowcount;
    update t set n = 'Updated' where i = 2;
    :rowNum := :rowNum + sql%rowcount;
end;
"@
    $pRowNum = $cmd.Parameters.Add("rowNum", [Oracle.ManagedDataAccess.Client.OracleDbType]::Int32, [System.Data.ParameterDirection]::Output)
    $cmd.ExecuteNonQuery() | Out-Null
    $rowCountAffected = $pRowNum.Value
    $cmd.Parameters.Clear()

缺點是每執行一個指令要取一次 sql%rowcount 累加統計,有點麻煩。

方法二,跑迴圈執行:

    #執行多行更新
    $cmdLines = @"
update t set n = 'Updated' where i = 1
update t set n = 'Updated' where i = 2
"@.Split("`r")
    foreach ($cmdLine in $cmdLines) {
        $cmd.CommandText = $cmdLine
        $rowCountAffected = $cmd.ExecuteNonQuery()
        #檢查筆數
        if ($rowCountAffected -ne 1) {
            throw "執行筆數不為 1 : $($rowCountAffected) @ $($cmdLine)"
        }
    }

邏輯單純好寫易讀,指令亦可考慮另存成獨立檔案,但迴圈數多時要留意效能。

Tutorial of how to update SQL in defensive way to decrease risk of mistakes, the PowerShell edition.


Comments

Be the first to post a comment

Post a comment