11日の件があまりにひどく、日に日に辛くなってきたのでちゃんとまとめておくことにします。
背景
この前参加させてもらった、「Silverlightを囲む会in大阪#18」でこんな話題がありました。
- LightSwitchの排他制御ってどうなってるのだろう?
- 自動生成されるテーブルには排他に使いそうなタイムスタンプとかなさそう
- まさか、後から更新した方が勝つとか?
そんなわけで、ちょっと動かしてみたら確認できるかな?と思って試してみました。
自動作成されたテーブルを確認
まず、自動生成されたテーブルがどうなっているのか見ておきます。
アプリの発行とインストール
アプリを多重起動するためにアプリケーションの発行を行います。確認用なのでローカルで簡単に動かせる構成にしてます。
アプリケーションの発行
プロジェクトを右クリックして「発行」を選択します。
クライアント構成はデスクトップでアプリケーションサーバはローカルにします。
このとき、SQLServerExpressを使用していれば、以下のフォルダにデータベースファイルが格納されていると思います。
C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA
インストール
発行時に指定したPublishフォルダ配下のSetup.exeを実行し、インストールしておきます。
更新時の排他確認
更新時の排他の動作を確認します。クライアントを2つ起動して、これらのクライアントをそれぞれ、クライアント1、クライアント2と呼ぶことにします。
レコードの追加
まずはテスト用にデータを1件登録しておきます。クライアント1、クライアント2どちらから実行してもかまいません。
ちなみに、このときに発行される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で下図のように更新して保存します。
このときに発行される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でも更新してみます。
すると、以下の様に更新でエラーになります。
このときの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を行ってデータを取得しているのは、以下の様にサーバの最新の値を取得するためだと思われます。
ここで、更新の受け入れを行ってみます。このときの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が発行されているか確認します。
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の排他制御について以下の様にまとめます。
- LightSwitchは楽観的なロックを行って排他制御を行っている
- 勝手に上書きで更新する様なことはなさそう
- エラーになったとき「更新の受け入れ」が行えてしまうため、利用には注意が必要
- サーバ上のデータと比較して、受け入れてもいいかどうか判断できる人(情シス担当者等)が使うなら問題ない
- よくわかっていない人(エンドユーザ等)が使用するには少し危険
- 「更新の受け入れ」を行えない様にする方法があるかどうかについては不明