LINQ を学習しているときに、タイトルにあるデータベースの更新操作という困難に遭遇しそうになりました。今度は私があなたをこの泥沼に一歩ずつ連れて行きますので、レンガと唾液を準備してください、私について来てください。
最も単純なケースから始めましょう。Northwind データベースを例として考えてみましょう。製品の ProductName を変更する必要がある場合は、クライアントで次のコードを直接記述できます。
// リスト 0NorthwindDataContext db = new NorthwindDataContext();
製品製品 = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "チャイが変わりました";
db.SubmitChanges();
テストしてみると、アップデートは成功しました。ただし、再利用はまったく不可能であるため、そのようなコードはプロジェクトには表示されないと思います。さて、それをリファクタリングしてメソッドに抽出しましょう。パラメータは何にすべきでしょうか?は、更新する新しい製品名と製品 ID です。まあ、そういうことのようですね。
public void UpdateProduct(int id, string productName)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
製品製品 = db.Products.Single(p => p.ProductID == id);
製品.製品名 = 製品名;
db.SubmitChanges();
}実際のプロジェクトでは、製品名を変更するだけでは済みません。製品の他のフィールドも変更される可能性があります。 UpdateProduct メソッドのシグネチャは次のようになります。
public void UpdateProduct(int id,
文字列製品名、
int サプライヤー ID、
int カテゴリ ID、
文字列単位あたりの数量、
小数単位価格、
ショートユニット在庫あり、
短い単位でのご注文、
short reorderLevel) もちろん、これは単なる単純なデータベースです。実際のプロジェクトでは、20、30、さらには数百のフィールドがあることも珍しくありません。誰がそのようなやり方を容認できるでしょうか?このように書くと、Product オブジェクトは何をするのでしょうか?
そうです、Product をメソッドのパラメーターとして使用し、面倒な代入操作をクライアント コードにスローします。同時に、Product インスタンスを取得するためのコードを抽出して GetProduct メソッドを形成し、データベース操作に関連するメソッドを特にデータベースの処理を担当する ProductRepository クラスに組み込みました。そうそう、SRP!
// リスト 1
// 製品リポジトリ
public Product GetProduct(int id)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct(Product 製品)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
db.Products.Attach(製品);
db.SubmitChanges();
}
//クライアントコード
ProductRepository リポジトリ = new ProductRepository();
製品製品 = リポジトリ.GetProduct(1);
product.ProductName = "チャイが変わりました";
リポジトリ.UpdateProduct(製品);
ここでは、Attach メソッドを使用して Product のインスタンスを他の DataContext にアタッチします。デフォルトの Northwind データベースの場合、この結果は次の例外になります。
// 例外 1 NotSupportException:
エンティティの添付または追加が試行されました。エンティティは新しいエンティティではなく、別の DataContext からロードされた可能性があります。この操作はサポートされていません。
新しいエンティティをアタッチまたは追加しようとしました。
おそらく、別の DataContext から読み込まれている可能性があります。これはサポートされていません。MSDN を見ると、エンティティをクライアントにシリアル化するときに、これらのエンティティが元の DataContext から切り離されることがわかります。 DataContext は、これらのエンティティに対する変更や他のオブジェクトとの関連付けを追跡しなくなりました。この時点でデータを更新または削除する場合は、SubmitChanges を呼び出す前に、Attach メソッドを使用してエンティティを新しい DataContext にアタッチする必要があります。そうしないと、上記の例外がスローされます。
Northwind データベースでは、Product クラスには、Order_Detail、Category、Supplier という 3 つの関連クラス (つまり、外部キーの関連付け) が含まれています。上の例では、Product をアタッチしますが、それに関連付けられた Attach クラスがないため、NotSupportException がスローされます。
では、Product に関連するクラスを関連付けるにはどうすればよいでしょうか? Northwind のような単純なデータベースの場合でも、これは複雑に思えるかもしれません。元のProductに関連するOrder_Detail、Category、Supplierの元のクラスを取得して、それぞれ現在のDataContextにAttachする必要があるようですが、実際にはこれを実行してもNotSupportExceptionがスローされます。
では、更新操作を実装するにはどうすればよいでしょうか?簡単にするために、Northwind.dbml 内の他のエンティティ クラスを削除し、Product のみを保持します。このようにして、最も単純なケースから分析を開始できます。
問題により他のクラスを削除した後、リスト 1 のコードを再度実行しましたが、データベースは製品の名前を変更しませんでした。 Attach メソッドのオーバーロードされたバージョンを確認すると、問題を簡単に見つけることができます。
Attach(entity) メソッドは、デフォルトで Attach(entity, false) オーバーロードを呼び出します。これにより、対応するエンティティが未変更の状態でアタッチされます。 Product オブジェクトが変更されていない場合は、このオーバーロードされたバージョンを呼び出して、後続の操作のために Product オブジェクトを未変更の状態で DataContext にアタッチする必要があります。この時点では、Product オブジェクトのステータスは「変更済み」になっており、Attach(entity, true) メソッドのみを呼び出すことができます。
そこで、リスト 1 の関連コードを Attach(product, true) に変更して、何が起こったかを確認してみましょう。
// 例外 2 InvalidOperationException:
エンティティがバージョン メンバーを宣言している場合、または更新チェック ポリシーがない場合は、元の状態を持たない変更されたエンティティとしてのみアタッチできます。
エンティティは、元の状態を持たずに変更された状態でのみアタッチできます
バージョン メンバーを宣言している場合、または更新チェック ポリシーがない場合。
LINQ to SQL は RowVersion 列を使用してデフォルトのオプティミスティック同時実行性チェックを実装します。そうでない場合、変更された状態でエンティティを DataContext にアタッチするときに上記のエラーが発生します。 RowVersion 列を実装するには 2 つの方法があります。1 つはデータベース テーブルのタイムスタンプ タイプの列を定義する方法で、もう 1 つはテーブルの主キーに対応するエンティティ属性に IsVersion=true 属性を定義する方法です。 TimeStamp 列と IsVersion=true 属性を同時に持つことはできないことに注意してください。そうしないと、InvalidOprationException がスローされます。メンバー "System.Data.Linq.Binary TimeStamp" と "Int32 ProductID" は両方とも行バージョンとしてマークされます。この記事では、例としてタイムスタンプ列を使用します。
TimeStamp という名前の列を作成し、Products テーブルの timestamp と入力した後、それをデザイナーにドラッグして戻し、リスト 1 のコードを実行します。神様に感謝します、ついにうまくいきました。
次に、カテゴリ テーブルをデザイナーにドラッグします。今回はその教訓を生かして、まずカテゴリテーブルにタイムスタンプ列を追加しました。テストした結果、やはり例外 1 のエラーであることが判明しました。カテゴリのタイムスタンプ列を削除しても、問題は残ります。なんと、恐ろしいAttachメソッドでは一体何が行われているのでしょうか?
あ、ちなみに、Attach メソッドのオーバーロード版もあるので、試してみましょう。
public void UpdateProduct(Product 製品)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
製品 oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(product, oldProduct);
db.SubmitChanges();
または、例外 1 エラー!
落ちちゃうよ!付けて、付けて、どうしたの?
LINQ to SQL ソース コードを調査するには、Reflector の FileDisassembler プラグインを使用して System.Data.Linq.dll を cs コードに逆コンパイルし、プロジェクト ファイルを生成します。これは、Visual Studio での検索と場所の特定に役立ちます。
例外 1 はいつスローされますか?
まず、例外 1 で説明されている情報を System.Data.Linq.resx から検索し、キー「CannotAttachAddNonNewEntities」を取得します。次に、System.Data.Linq.Error.CannotAttachAddNonNewEntities() メソッドを検索し、このメソッドへのすべての参照を検索して、このメソッドは、StandardChangeTracker.Track メソッドと InitializeDeferredLoader メソッドの 3 か所で使用されます。
Table.Attach(entity, bool) のコードを再度開くと、予想どおり、StandardChangeTracker.Track メソッドが呼び出されていることがわかります (Attach(entity, entity) メソッドにも同じことが当てはまります)。
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); Track メソッドで、次のコードは例外 1 をスローします。
if (trackedObject.HasDeferredLoaders)
{
System.Data.Linq.Error.CannotAttachAddNonNewEntities(); をスローします。
そこで、StandardTrackedObject.HasDeferredLoaders プロパティに注目します。
内部オーバーライド bool HasDeferredLoaders
{
得る
{
foreach (this.Type.Associations の MetaAssociation アソシエーション)
{
if (this.HasDeferredLoader(association.ThisMember))
{
true を返します。
}
}
foreach (this.Type.PersistentDataMembers の p からの MetaDataMember メンバー
ここで、p.IsDeferred && !p.IsAssociation
p)を選択してください
{
if (this.HasDeferredLoader(メンバー))
{
true を返します。
}
}
false を返します。
}
このことから、エンティティ内に遅延ロードされた項目がある限り、Attach 操作で例外 1 がスローされることが大まかに推測できます。これは、例外 1 が発生するシナリオと正確に一致しています。つまり、Product クラスには遅延ロードされる項目が含まれています。
そこで、この例外を回避する方法が登場しました。それは、製品にロードするのを遅らせる必要がある項目を削除するというものです。削除するにはどうすればよいですか? DataLoadOptions を使用してすぐにロードすることも、遅延ロードが必要な項目を null に設定することもできます。しかし、最初の方法は機能しなかったため、2 番目の方法を使用する必要がありました。
// リスト 2
クラス ProductRepository
{
public Product GetProduct(int id)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
public Product GetProductNoDeffered(int id)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
//DataLoadOptions オプション = new DataLoadOptions();
//options.LoadWith<Product>(p => p.Category);
//db.LoadOptions = オプション;
var product = db.Products.SingleOrDefault(p => p.ProductID == id);
製品.カテゴリ = null;
返品製品。
}
public void UpdateProduct(Product 製品)
{
NorthwindDataContext db = 新しい NorthwindDataContext();
db.Products.Attach(product, true);
db.SubmitChanges();
}
}
//クライアントコード
ProductRepository リポジトリ = new ProductRepository();
製品製品 = リポジトリ.GetProductNoDeffered(1);
product.ProductName = "チャイが変わりました";
リポジトリ.UpdateProduct(製品);
例外 2 はいつスローされますか?
前のセクションの方法に従って、例外 2 をスローするコードをすぐに見つけました。幸いなことに、プロジェクト全体で例外 2 があったのは次のコードだけでした。
if (asModified && ((inheritanceType.VersionMember == null) && inventoryType.HasUpdateCheck))
{
System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState(); をスローします。
}
ご覧のとおり、Attach の 2 番目のパラメーター asModified が true で、RowVersion 列 (VersionMember=null) が含まれておらず、更新チェック列 (HasUpdateCheck) が含まれている場合、例外 2 がスローされます。 HasUpdateCheck のコードは次のとおりです。
パブリック オーバーライド ブール HasUpdateCheck
{
得る
{
foreach (this.PersistentDataMembers の MetaDataMember メンバー)
{
if (member.UpdateCheck != UpdateCheck.Never)
{
true を返します。
}
}
false を返します。
}
これは、私たちのシナリオとも一致しています。Products テーブルには RowVersion 列がなく、デザイナーによって自動的に生成されたコードでは、すべてのフィールドの UpdateCheck プロパティがデフォルトの Always、つまり HasUpdateCheck プロパティが true です。
例外 2 を回避する方法はさらに簡単で、すべてのテーブルに TimeStamp 列を追加するか、すべてのテーブルの主キー フィールドに IsVersion=true フィールドを設定します。後者の方法は自動生成されたクラスを変更し、いつでも新しい設計で上書きできるため、前者の方法を使用することをお勧めします。
Attachメソッドの使い方は?
上記の分析の後、Attach メソッドに関連する 2 つの条件、RowVersion 列があるかどうか、および外部キーの関連付けがあるかどうか (つまり、遅延ロードする必要がある項目) がわかります。これら 2 つの条件と、Attach のいくつかのオーバーロードの使用法を表にまとめました。以下の表を見るときは、十分な心の準備が必要です。
シリアルナンバー
アタッチ方法
RowVersion 列に関連する説明があるかどうか
1 Attach(entity) No No 変更なし
2 Attach(entity) いいえ はい NotSupportException: エンティティの添付または追加が試行されました。エンティティは新しいエンティティではなく、他の DataContext からロードされた可能性があります。この操作はサポートされていません。
3 Attach(entity) 修正の有無
4 Attach(entity)は変更されません。サブセットに RowVersion 列がない場合は 2 と同じです。
5 Attach(entity, true) No No InvalidOperationException: エンティティがバージョン メンバーを宣言している場合、または更新チェック ポリシーがない場合は、元の状態を持たない変更されたエンティティとしてのみアタッチできます。
6 Attach(entity, true) いいえ はい NotSupportException: エンティティの添付または追加が試行されました。エンティティは新しいエンティティではなく、他の DataContext からロードされた可能性があります。この操作はサポートされていません。
7 Attach(entity, true) 変更が正常かどうか (RowVersion 列を強制的に変更するとエラーが報告されます)
8 Attach(entity, true) はい NotSupportException: エンティティの接続または追加が試行されました。エンティティは新しいエンティティではなく、他の DataContext からロードされた可能性があります。この操作はサポートされていません。
9 Attach(entity,entity) × × DuplicateKeyException: キーが既に使用されているエンティティは追加できません。
10 Attach(entity,entity) No Yes NotSupportException: エンティティの添付または追加が試行されました。エンティティは新しいエンティティではなく、他の DataContext からロードされた可能性があります。この操作はサポートされていません。
11 Attach(entity,entity) DuplicateKeyException: キーが既に使用されているエンティティを追加できません。
12 Attach(entity,entity) はい NotSupportException: エンティティの接続または追加が試行されました。エンティティは新しいエンティティではなく、他の DataContext からロードされた可能性があります。この操作はサポートされていません。
アタッチは 7 番目の状況 (RowVersion 列を含み、外部キーの関連付けがない場合) でのみ正常に更新できます。このような状況は、データベースベースのシステムではほぼ不可能です。これはどのような API ですか?
まとめ 落ち着いてまとめを始めましょう。
リスト 0 のように UI に LINQ to SQL コードを直接記述しても、不幸なことは何も起こりません。しかし、個別のデータ アクセス層を抽象化しようとすると、惨事が発生します。これは、LINQ to SQL がマルチレイヤー アーキテクチャの開発には適していないということですか? LINQ to SQL は小規模システムの開発に適していると言われることが多いですが、サイズが小さいからといって階層化されていないわけではありません。これほど多くの例外を回避する方法はあるのでしょうか?
この記事は実際にいくつかのヒントを提供しました。このシリーズの次のエッセイでは、誰もが選択できるいくつかの解決策を提供したいと思います。