Question

nagoya-nakagawa on Tue, 07 Feb 2017 07:27:08


ASP.netのGridViewチュートリアル(以下URL)を実施しています。

https://msdn.microsoft.com/ja-jp/library/aa992036.aspx?f=255&MSPPError=-2147217396&cs-save-lang=1&cs-lang=csharp#code-snippet-2

実行して、Updateボタンを押下したところ、


更新 コマンドが SqlDataSource 'SqlDataSource3' 上の値すべてを比較するように指定しましたが、oldValues に対して渡されたディレクトリは空です。更新 に対して有効なディレクトリを渡すか、またはモードを OverwriteChanges に変更してください。

というエラーが発生しました。(GridViewにはSqlDataSource3というIDをバインドしています)

以下のスレッドに関するものです。
https://social.msdn.microsoft.com/Forums/ja-JP/11d44c96-c729-4834-8220-96d96eda7a8c/-?forum=aspnetja

何か考えられることがありましたらご教示いただけないでしょうか。
よろしくお願いいたします。



Sponsored



Replies

SurferOnWww on Tue, 07 Feb 2017 08:49:34


楽観的同時実行制御が怪しいと先のスレッドでヒントを書いたのだから、まずはそれを信用してもらいたいのですが。

エラーメッセージ「更新 コマンドが SqlDataSource 'SqlDataSource3' 上の値すべてを比較するように指定しましたが、oldValues に対して渡されたディレクトリは空です。更新 に対して有効なディレクトリを渡すか、またはモードを OverwriteChanges に変更してください。」で、それは 99.9% 間違いないです。

質問者さんは多分「SQL 生成の詳細オプション」のところで「オプティミスティック同時実行制御」にチェックを入れていたのだと想像しています。

想像だけでは何なので、実際に以下のようにして作ってみました。

結果、以下の通り質問者さんと同じ結果が再現できます。

ということで、100% 楽観的同時実行制御が原因です。

なぜ楽観的同時実行制御をかけるとこのようなエラーになるのか分からなければ聞いてください。


nagoya-nakagawa on Tue, 07 Feb 2017 10:22:47


SurferOnWww様
ありがとうございます。

確かに「オプティミスティック同時実行制御」にチェックをいれていたかと思います。
(先のスレッドでの自己解決と称した対応を行う前)

>なぜ楽観的同時実行制御をかけるとこのようなエラーになるのか分からなければ聞いてください。
分かりません。何故なのでしょうか?

理由を聞くと同時に恐縮ですが、
このチュートリアルのような複数レコード一括更新を行うようなGrid形式の場合、
 オプティミスティック同時実行制御(同時実行の競合が避けられます)
を享受するのは難しいのでしょうか?

よろしくお願いいたします。

SurferOnWww on Tue, 07 Feb 2017 12:32:01


> 分かりません。何故なのでしょうか?

SqlDataSource のウィザードの「SQL 生成の詳細オプション」のところで「オプティミスティック同時実行制御」にチェックを入れると、今回のケースでは自動生成される UPDATE クエリは以下のようになるはずです。(分かりやすいように改行、インデントを入れてます)

UPDATE [Employees] SET [LastName] = @LastName, [FirstName] = @FirstName 
    WHERE [EmployeeID] = @original_EmployeeID 
          AND 
          [LastName] = @original_LastName 
          AND 
          [FirstName] = @original_FirstName


@original_LastName, @original_FirstName には初期画面を表示する時点で DB から取得した LastName, FirstName フィールドのデータを代入し、UPDATE クエリをかけた時点で DB から取得したデータと一致する場合のみ UPDATE がかかるようになっています。違っていたら他のユーザーが変更したということで UPDATE はされません。それが楽観的同時実行制御ということです。そこのところの理解はよろしいですか?

で、この @original_LastName, @original_FirstName に代入するデータをどこから取得するかというと、それが OldValues という名前のディクショナリです(エラーメッセージの「ディレクトリ」は誤訳)。

エラーメッセージ「oldValues に対して渡されたディレクトリは空です」は @original_LastName, @original_FirstName に代入するデータを取得できないと言っています。

普通に作れば、データバウンドコントロール(GridView, ListView など)は、DataKeyNames プロパティ、子コントロール、ビューステートなどからパラメータ名と値を取得し、Keys, Values, OldValues, NewValues という IDictionary コレクションを作成して、それをデータソースコントロール(SqlDataSource, ObjectDataSource など)のパラメータに渡すように設定してくれます。

でも、チュートリアルのように普通でないこと(全行一括更新)をすると、そこまでは面倒を見てくれないということです。なので、

> このチュートリアルのような複数レコード一括更新を行うようなGrid形式の場合、
>   オプティミスティック同時実行制御(同時実行の競合が避けられます)
> を享受するのは難しいのでしょうか?

はその通りです。

そもそも、他のユーザーとの同時実行が起こりうる環境で全行一括更新などという無茶なことをするべきではないと思います。



nagoya-nakagawa on Wed, 08 Feb 2017 08:41:01


SurferOnWww様
ありがとうございます。

詳細なご説明ありがとうございました。
IDictionary コレクションのくだりは私にはまだ難しいところでありますが、
徐々に理解したいと思います。

>そもそも、他のユーザーとの同時実行が起こりうる環境で全行一括更新などという無茶なことをするべきではないと思います。
登録済みの伝票明細複数行を排他制御で更新したいというような場面はあるのかと考えますが、
別途工夫するようにします。

ありがとうございました。

trapemiya on Wed, 08 Feb 2017 09:13:39


GridViewにSqlDataSourceを使用した場合、GridViewのEditIndexに0以上の値がセットされており、GridViewがEditモードで開かれているということが前提になります。GridViewは複数行をEditモードで開くことができないため、SqlDataSourceを使用するには限界があり、今回の同時実行制御がうまく動作しないということになります。

どうしても全行一括更新で同時実行制御をしたいのであれば、SqlDataSourceではなく、TableAdapterなどを使用して、更新ロジックを独自に記述する必要があります。同時実行制御を行うのであれば、TableAdapterを使うのがお勧めです。
手元のデータでサンプルコードを書いてみました。TableAdapterで同時実行制御ありにしています。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="GridViewAllRowEdit2.aspx.cs" Inherits="test2015web.GridViewAllRowEdit2" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
    </div>
        <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="id">
            <Columns>
                <asp:BoundField DataField="id" HeaderText="id" InsertVisible="False" ReadOnly="True" SortExpression="id" />
                <asp:TemplateField HeaderText="sex" SortExpression="sex">
                    <ItemTemplate>
                        <asp:TextBox ID="SexTextBox" runat="server" Text='<%# Bind("sex") %>'></asp:TextBox>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField HeaderText="name" SortExpression="name">
                    <ItemTemplate>
                        <asp:TextBox ID="NameTextBox" runat="server" Text='<%# Bind("name") %>'></asp:TextBox>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
       </asp:GridView>
        <asp:Button ID="bttn_Update" runat="server" OnClick="bttn_Update_Click" Text="Update" />
    </form>
</body>
</html>

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using test2015web.DataSet1TableAdapters;

namespace test2015web
{
    public partial class GridViewAllRowEdit2 : System.Web.UI.Page
    {
        TESTforDelTableAdapter ta;
        DataSet1.TESTforDelDataTable dt;

        protected void Page_Load(object sender, EventArgs e)
        {
            if(!IsPostBack)
            {
                ta = new TESTforDelTableAdapter();
                dt = new DataSet1.TESTforDelDataTable();

                ta.Fill(dt);

                GridView1.DataSource = dt;
                GridView1.DataBind();

                Session["ta"] = ta;
                Session["dt"] = dt;
            }
            else
            {
                ta = (TESTforDelTableAdapter)Session["ta"];
                dt = (DataSet1.TESTforDelDataTable)Session["dt"];
            }
        }

        protected void bttn_Update_Click(object sender, EventArgs e)
        {
            foreach(GridViewRow r in GridView1.Rows)
            {
                string currentName;
                string currentSex;

                int currentID = Convert.ToInt32(GridView1.DataKeys[r.RowIndex].Value);

                currentSex = ((TextBox)r.FindControl("SexTextBox")).Text;
                currentName = ((TextBox)r.FindControl("NameTextBox")).Text;

                DataRow row =
                    dt.Select(String.Format("ID = {0}", currentID))[0];

                if(!currentName.Equals(row["Sex"].ToString())) { row["Sex"] = currentSex; }
                if(!currentName.Equals(row["Name"].ToString())) { row["Name"] = currentName; }
            }

            ta.Update(dt);
        }
    }
}
(追記)
手持ちのテストデータを使用していますので、テーブル名などは気にしないで下さい。


★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

SurferOnWww on Wed, 08 Feb 2017 12:35:54


> 登録済みの伝票明細複数行を排他制御で更新したいというような場面はあるのかと考えますが、

そうでしょうか? そういう場面で排他制御が必要なら、APS.NET 標準で備わっている機能を利用するだけで十分ではないかと自分は思いますけど。

とにかく、ASP.NET にもともと備わっている機能を超えて何かしようとするのは初心者の方にはお勧めできません。そういうことをしようとすると、とたんに敷居が高くなります。

ASP.NET に標準で備わっている機能でどこまでできるのかを十分把握した上で、どうしてもそれを超える機能を実装する必要があるのか、disavdantage はないか(工数が増えるでしょうし、例えば terapemiya さんの案の Session  の利用にはそれなりの disavdantage があります)、その disadvangate と advantage を天秤にかけてどうすべきか、自分のスキルはその目的を果たすのに十分かを評価して進めることをお勧めします。

nagoya-nakagawa on Thu, 09 Feb 2017 01:38:19


trapemiya様
ありがとうございます。

実装しまして、
 同時実行違反
を確認できました。
解決策のひとつとしてとても有益でした。

nagoya-nakagawa on Thu, 09 Feb 2017 01:44:29


SurferOnWww様
ありがとうございます。

 ・そういう場面で排他制御が必要なら、
 ・APS.NET 標準で備わっている機能を利用するだけ
この二つを満たす実装方法はあるのでしょうか?
今回のGrideViewでの実装は、たまたま最初に目に触れたチュートリアルから実現しようとしたものです。
GridViewにこだわりはありませんので、よろしければご教示いただければと思います。

SurferOnWww on Thu, 09 Feb 2017 03:26:08


先の私のレスで、

> そういう場面で排他制御が必要なら、APS.NET 標準で備わっている機能を利用するだけ
> で十分ではないかと自分は思いますけど。

というのは、ステートレスな Web アプリで全行一括更新+楽観的同時実行制御という無茶なことは諦めて、一行ずつ更新していくという ASP.NET 標準のやり方でも十分目的は果たせると思うと言っています。

どうしても全行一括更新にこだわるのであれば、もし SQL Server Management Studio が使えるなら、そちらを使った方がいいと思います。Windows Forms アプリでも同様なことは可能です。


どうしても Web アプリで行う必要があって、全行一括更新+楽観的同時実行制御が must であれば、先にも書きましたように、disavdantage はないか(工数が増えるでしょうし、例えば terapemiya さんの案の Session  の利用にはそれなりの disavdantage があります)、その disadvangate と advantage を天秤にかけてどうすべきか、自分のスキルはその目的を果たすのに十分かを評価して進めることをお勧めします。

どのように実装したらよいかについて話をしたいというご希望があれば、別に新しいスレッドを立てて、そこで質問してください。Session を使わず、かつどの行で衝突が発生したかが分かるような案を提案できると思います。