SQL

データベースを更新するコード

普通にSQLクエリアナライザを使ってデータベースを更新するときは以下のようにすると思います。

    1: use DatabaseName
    2: go
    3: create table mytable (
    4:     cd varchar(6) not null,
    5:     nm varchar(50) not null,
    6:     mkday datetime not null default(getdate()),
    7:     constraint pk_mytable primary key (
    8:         cd
    9:     )
    10: )
    11: insert into mytable (cd, nm) values ('000001', 'あいうえお')
    12: insert into mytable (cd, nm) values ('000002', 'かきくけこ')
    13: go
    14: create view mytableview
    15: as
    16: select
    17:     cd,
    18:     nm,
    19:     convert(varchar(23), mkday, 121) as mkday
    20: from
    21:     mytable
    22: go

もちろんエラーがなければ全く問題ないのですが、何かエラーがあったときは部分的に更新されてしまうため、どこが更新されたのか、どこが更新されなかったのかわからなくなり、結局アップデートに失敗して復旧に大変な手間をかけなければならなくなることもしばしばあります。

SQLクエリアナライザでは何も指示しなければ自動コミットモードで動作します。この場合各バッチ(「go」で区切られた各部分)はランタイムエラーがなければコミットされ、エラーがあればロールバックします。バッチが複数あるとそれぞれにエラーの有無でロールバックしたりコミットしたりと成否がわかれてしまうわけです。

そこで、アップデートに失敗したときにアップデートコード全体をロールバックできないかと考えたのが次のコードです。スクリプト全体をひとつのトランザクションで管理するのではなく、必要に応じてトランザクションを開始したりロールバックしたりして制御します。

    1: set implicit_transactions on
    2: set xact_abort on
    3: set nocount on
    4:
    5: begin tran
    6: begin tran
    7: go
    8:-- ここにアップデーコードを書く
    9:
    10:
    11: go
    12: if @@trancount > 1
    13: begin
    14:     while @@trancount > 0
    15:         commit
    16:     print 'アップデートは正常に終了しました。'
    17: end
    18:
    19: else
    20: begin
    21:     if @@trancount > 0
    22:         rollback
    23:     print 'このアップデートは適用されません。'
    24: end
    25:
    26: set nocount off
    27: set xact_abort off
    28: set implicit_transactions off

「set implicit_transactions on」で暗黙のトランザクションモードを使うことを指示します。トランザクションの必要な処理を行うと自動的にトランザクションを開始します。併せて「set xact_abort on」でエラー時に自動的にトランザクションをロールバックし、バッチを終了させることも指示しておきます。この2つによって、どこでもトランザクションが自動的に開始され、エラーでロールバックします。処理中のトランザクションは明示的にコミットされない限り完了しません。それらは放っておけば接続が切れたときにロールバックされます。

最終的にエラーがあったのかどうかを調べるために、はじめに複数の(=ネストされた)トランザクションを開始しておきます。アップデートコードのどこかでエラーがあった場合、トランザクション全体がロールバックされ、トランザクションの数(深さ)は0になります。そのあと自動的に開始されるかもしれないトランザクションの数は1より大きくならないので、最終的にトランザクションの数が1よりも大きければエラーがなかったことをそうでなければエラーがあったことを検出できます。

最初の明示的なトランザクションの開始の前に暗黙のトランザクションが開始されるようなので「begin tran」は1つでも良いのですが、コードが複数のトランザクションの中で動作していることを理解できるように2つ書いておきます。

トランザクション全体をコミットするには全てのトランザクションをコミットする必要があります。ネストされたトランザクションのコミットは@@trancountをデクリメントする以外に何も引き起こさないため、確実に最初のトランザクションをコミットしなければなりません。

気をつけなければいけないことのひとつは「alter view」と「alter procedure」がトランザクションを開始させないことです。「drop」してから「create」するようにします。それと「raiserror」は例外を発生させますが、ランタイムエラーではないのでトランザクションをロールバックさせません。エラーとしたいところで「rollback」と「return」を記述しておきます。

※ヘルプには暗黙のトランザクションを開始させるとは書いてないのですが、やってみると「alter view」と「alter procedure」は暗黙のトランザクションを開始させます。

コンパイルエラーでは、そのバッチは構築に失敗して全体を実行しませんが、他のバッチは実行します。そして@@errorをセットしますが、トランザクションをロールバックしません。したがって各バッチのあとでエラーがないか調べる必要があるようです。

出力に日時を追加し、実際に使用しているのは、次のようなコードです。多分時間は正確ではありませんが、作業のログとしても利用しています。

    set implicit_transactions on
    set xact_abort on
    set nocount on

    print char(13) + char(10)
    print 'ScriptName, Copyright (C) yyyy, CompanyName'
    print char(13) + char(10)

    print convert(varchar(23), getdate(), 121) + ' アップデートを開始します。'

    begin tran
    begin tran
    go
    create table mytable (
        cd varchar(6) not null,
        nm varchar(50) not null,
        mkday datetime not null default(getdate()),
        constraint pk_mytable primary key (
            cd
        )
    )

    print convert(varchar(23), getdate(), 121) + ' mytableを作成しました。'

    insert into mytable (cd, nm) values ('000001', 'あいうえお')
    insert into mytable (cd, nm) values ('000002', 'かきくけこ')

    print convert(varchar(23), getdate(), 121) + ' mytableに'
	     + convert(varchar(16), @@rowcount) + '件を追加しました。'
    go
    if @@error <> 0 begin if @@trancount > 1 rollback end
    go
    create view mytableview
    as
    select
        cd,
        nm,
        convert(varchar(23), mkday, 121) as mkday
    from
        mytable
    go
    if @@error <> 0 begin if @@trancount > 1 rollback end
    else print convert(varchar(23), getdate(), 121) + ' mytableviewを作成しました。'
    go
    if @@trancount > 1
    begin
        while @@trancount > 0
            commit

        print convert(varchar(23), getdate(), 121) + ' アップデートは正常に終了しました。'
    end

    else
    begin
        if @@trancount > 0
            rollback

        print convert(varchar(23), getdate(), 121) + ' このアップデートは適用されません。'
    end

    set nocount off
    set xact_abort off
    set implicit_transactions off

    ScriptName, Copyright (C) yyyy, CompanyName

    yyyy/MM/dd HH:mm:ss.mmm アップデートを開始します。
    yyyy/MM/dd HH:mm:ss.mmm mytableを作成しました。
    yyyy/MM/dd HH:mm:ss.mmm mytableに1件を追加しました。
    yyyy/MM/dd HH:mm:ss.mmm mytableviewを作成しました。
    yyyy/MM/dd HH:mm:ss.mmm アップデートは正常に終了しました。

各バッチの間にチェックコードを挿入するのはかなり面倒な作業です。もう少し工夫が必要かもしれません。

ランタイムエラーにはいくつか種類があるかもしれません。例えば以下のコードを実行すると、#tがなければコンパイルエラーではなくランタイムエラーが発生します。「set xact_abort on」としてあればロールバックするはずだと思うのですが、この場合はトランザクションをロールバックしません。

    select * from #t