なか日記

一度きりの人生、楽しく生きよう。

LightSwitchの排他制御について

11日の件があまりにひどく、日に日に辛くなってきたのでちゃんとまとめておくことにします。

背景

この前参加させてもらった、「Silverlightを囲む会in大阪#18」でこんな話題がありました。

  • LightSwitchの排他制御ってどうなってるのだろう?
    • 自動生成されるテーブルには排他に使いそうなタイムスタンプとかなさそう
    • まさか、後から更新した方が勝つとか?

そんなわけで、ちょっと動かしてみたら確認できるかな?と思って試してみました。

自動作成されたテーブルを確認

まず、自動生成されたテーブルがどうなっているのか見ておきます。

テーブル定義

LightSwitchで以下の様なテーブルを作成(定義)します。
f:id:nakaji999:20110613211420p:image

自動生成されたテーブル

データベースは「プロジェクト名\Bin\Data」配下にあるのでサーバエクスプローラでアタッチします。
で、どんなテーブルが作成されるのかというと、以下の様な感じです。
f:id:nakaji999:20110613211422p:image
テーブル名がbooksになっているのは、LightSwitchのテーブル「Book」のプロパティで複数形の名前に「Books」をしているからです。
LightSwitchのテーブルは、実はエンティティの設定なので気をつけてねとお話しになってた件ですね。
とりあえず、排他に使う余分な項目は自動生成されていないのが確認できました。

アプリの発行とインストール

アプリを多重起動するためにアプリケーションの発行を行います。確認用なのでローカルで簡単に動かせる構成にしてます。

アプリケーションの発行

プロジェクトを右クリックして「発行」を選択します。
クライアント構成はデスクトップでアプリケーションサーバはローカルにします。
このとき、SQLServerExpressを使用していれば、以下のフォルダにデータベースファイルが格納されていると思います。
C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA

インストール

発行時に指定したPublishフォルダ配下のSetup.exeを実行し、インストールしておきます。

評価準備

発行されたSQLの確認

SQLServerExpressを使っている場合、SQLプロファイラが付属していないので、下記オープンソースのプロファイラを使用します。

更新時の排他確認

更新時の排他の動作を確認します。クライアントを2つ起動して、これらのクライアントをそれぞれ、クライアント1、クライアント2と呼ぶことにします。

レコードの追加

まずはテスト用にデータを1件登録しておきます。クライアント1、クライアント2どちらから実行してもかまいません。
f:id:nakaji999:20110613211423p:image
ちなみに、このときに発行されるSQLはこんな感じになってます。

BEGIN TRANSACTION 
go

exec sp_executesql N'insert [dbo].[Books]([ISBN], [Title])
values (@0, @1)
select [Id]
from [dbo].[Books]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',N'@0 nvarchar(255),@1 nvarchar(255)',@0=N'000-000-0000',@1=N'追加'
go

COMMIT TRANSACTION 
go
最新データの表示

それぞれのクライアントで「最新の情報に更新」ボタンを押下し、最新のデータを表示しておきます。

クライアント1で更新

クライアント1で下図のように更新して保存します。
f:id:nakaji999:20110613211424p:image
このときに発行されるSQLはこんな感じになってます。

BEGIN TRANSACTION 
go

exec sp_executesql N'update [dbo].[Books]
set [Title] = @0
where ((([Id] = @1) and ([ISBN] = @2)) and ([Title] = @3))
',N'@0 nvarchar(255),@1 int,@2 nvarchar(255),@3 nvarchar(255)',@0=N'クライアント1で更新',@1=2,@2=N'000-000-0000',@3=N'追加'
go

COMMIT TRANSACTION 
go
クライアント2で更新

同様に、クライアント2でも更新してみます。
f:id:nakaji999:20110613211425p:image
すると、以下の様に更新でエラーになります。
f:id:nakaji999:20110613211426p:image
このときのSQLは以下の様になっています。

BEGIN TRANSACTION 
go

exec sp_executesql N'update [dbo].[Books]
set [Title] = @0
where ((([Id] = @1) and ([ISBN] = @2)) and ([Title] = @3))
',N'@0 nvarchar(255),@1 int,@2 nvarchar(255),@3 nvarchar(255)',@0=N'クライアント2で更新',@1=2,@2=N'000-000-0000',@3=N'追加'
go

ROLLBACK TRANSACTION 
go

exec sp_executesql N'SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[ISBN] AS [ISBN], 
[Extent1].[Title] AS [Title]
FROM [dbo].[Books] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=2
go

update文をわかりやすくすると、以下の様になります。

update [dbo].[Books]
set [Title] = 'クライアント2で更新'
where ((([Id] = 2) and ([ISBN] = '000-000-0000')) and ([Title] = '追加'))

ADO.NetのCommandBuilderで自動生成したときと同じように、すべての項目が最初に読み込んだ値と同じじゃないと更新が0件(つまり失敗)になる様になっています。
また、失敗してrollbackした後、再度selectを行ってデータを取得しているのは、以下の様にサーバの最新の値を取得するためだと思われます。
f:id:nakaji999:20110613211427p:image

ここで、更新の受け入れを行ってみます。このときのSQLは以下の様になっています。

BEGIN TRANSACTION 
go

exec sp_executesql N'update [dbo].[Books]
set [Title] = @0
where ((([Id] = @1) and ([ISBN] = @2)) and ([Title] = @3))
',N'@0 nvarchar(255),@1 int,@2 nvarchar(255),@3 nvarchar(255)',@0=N'クライアント2で更新',@1=2,@2=N'000-000-0000',@3=N'クライアント1で更新'
go

COMMIT TRANSACTION 
go

先ほどと同じように、update文をわかりやすくすると、以下の様になります。

update [dbo].[Books]
set [Title] = 'クライアント2で更新'
where ((([Id] = 2) and ([ISBN] = '000-000-0000')) and ([Title] = 'クライアント1で更新'))

先ほどのupdateで失敗したときに取得した最新の値を使用してupdateを行っています。つまり、updateが失敗して、更新の受け入れを行う間にさらに更新が行われた場合にも誤って上書きしない仕組みになっています。

削除の排他

削除時はどのようなSQLが発行されているか確認します。
f:id:nakaji999:20110613211428p:image

BEGIN TRANSACTION 
go

exec sp_executesql N'delete [dbo].[Books]
where ((([Id] = @0) and ([ISBN] = @1)) and ([Title] = @2))',N'@0 int,@1 nvarchar(255),@2 nvarchar(255)',@0=2,@1=N'000-000-0000',@2=N'クライアント2で更新'
go

COMMIT TRANSACTION 
go

先ほどと同じように、update文をわかりやすくすると、以下の様になります。

delete [dbo].[Books]
where ((([Id] = 2) and ([ISBN] = '000-000-0000')) and ([Title] = 'クライアント2で更新'))

updateと同じように、すべての項目が最初に読み込んだ値と同じじゃないと削除が0件になる様になっています。

まとめ

上記を確認した上で、LightSwitchの排他制御について以下の様にまとめます。

  1. LightSwitchは楽観的なロックを行って排他制御を行っている
    • 勝手に上書きで更新する様なことはなさそう
  2. エラーになったとき「更新の受け入れ」が行えてしまうため、利用には注意が必要
    • サーバ上のデータと比較して、受け入れてもいいかどうか判断できる人(情シス担当者等)が使うなら問題ない
    • よくわかっていない人(エンドユーザ等)が使用するには少し危険
  3. 「更新の受け入れ」を行えない様にする方法があるかどうかについては不明