2015年10月25日

EntityFramework(12):行の挿入、削除

   このエントリーをはてなブックマークに追加 Clip to Evernote
前回、データの更新については、説明しましたので、今回は、行の挿入、削除の例を示します。

行をデータベースに挿入するには 、以下の手順を踏みます。
  1. 送信する列データを含む新しいオブジェクトを作成します。 
  2. データベース内の挿入先テーブルに関連付けられた EntityFramework Table コレク ションに新しいオブジェクトを追加します。 
  3. データベースに変更内容を送信します。 
コードにするとこんな感じ。
 
 using (var db = new NorthwindContext()) {
     Category cat = new Category {
         CategoryName = "Side Dish",
         Description = "salad, croquette, tempura, cooked beans",
     };
     db.Categories.Add(cat);
     db.SaveChanges();
 }

通常のコレクションと同じに Addメソッドで、Categoryを追加しています。
その後、SaveChangesメソッドを呼び出すことで、Categoryを挿入することができます。
なお、CategoryテーブルのCategoryIDは、IDENTITYの指定がされていますので、Categoryクラスのインスタンスを生成するときに、CategoryIDに値を設定する必要はありません。


複数の行をまとめて挿入したい場合には、AddRangeメソッドを使います。

 using (var db = new NorthwindContext()) {
     Category[] categories = new Category[] {
         new Category { CategoryName = "Side Dish", 
                        Description = "salad, croquette, tempura, cooked beans" },
         new Category { CategoryName = "Retort Pack", 
                        Description = "noodles, curry, stew, cooked rice" },
     };
     db.Categories.AddRange(categories);
     db.SaveChanges();
 }
ここでは、配列を渡すコードを示しましたが、AddRangeメソッドは、 IEnumerable<T>を受け取りますので、List<T>なども渡すことができます。



削除するには、いったんテーブルから該当する行を取得し、その後、 Removeメソッドを呼び出します。
 
 using (var db = new NorthwindContext()) {
     var category = (from c in db.Categories
                     where c.CategoryID == 9
                     select c).Single();
     db.Categories.Remove(category);
     db.SaveChanges();
 }

一括して複数の行を削除するには、RemoveRange()メソッドを使います。

 using (var db = new NorthwindContext()) {
     var categories = from c in db.Categories
                      where c.CategoryID >= 9
                      select c;
     db.Categories.RemoveRange(categories);
     db.SaveChanges();
 }

では、取得していないオブジェクト(行)を削除することはできるでしょうか?
試しに以下のコードを書いてみました。
 using (var db = new NorthwindContext()) {
     Category cat = new Category {
         CategoryID = 9,
         CategoryName = "Side Dish",
         Description = "salad, croquette, tempura, cooked beans",
     };
     db.Categories.Remove(cat);
     db.SaveChanges();
 }
実行すると、例外 System.InvalidOperationException が発生します。

実は、この例外を回避して、強制的に削除することもできます。
それには、以下のように Attach()メソッドで、このオブジェクトをDbContextに関連付けしておかなければいけません。

 using (var db = new NorthwindContext()) {
     Category cat = new Category {
         CategoryID = 9
     };
     db.Categories.Attach(cat);
     db.Categories.Remove(cat);
     db.SaveChanges();
 }

クエリ式で読み込んだオブジェクトは、自動的にアタッチされています。
なお、Categoryの主キー以外は設定しておく必要はありません。
発行されるSQLは、以下の通りです。

 DELETE [dbo].[Categories]
 WHERE ([CategoryID] = @0)
 -- @0: '9' (Type = Int32)
  

Posted by gushwell at 22:00Comments(0)TrackBack(0)

2015年10月21日

EntityFramework(11):エンティティオブジェクトへの操作

   このエントリーをはてなブックマークに追加 Clip to Evernote
今回は、更新、挿入、削除の操作の基礎となるエンティティオブジェクトの操作について。

早速、以下のコードを見てください。

 using (var db = new NorthwindContext()) {
     var p = db.Products.Where(x => x.ProductID == 5).SingleOrDefault();
     if (p != null) {
         Console.WriteLine("{0} {1}", p.ProductID, p.UnitPrice); // ★
         // 値を変更
         p.UnitPrice = 20.5m;   
         // 再度取り出し。
         var p2 = db.Products.Where(product => product.ProductID == 5).Single();
         Console.WriteLine($"{p2.ProductID} {p2.UnitPrice}");
     }
 }

取得した ProductオブジェクトのUnitPriceプロパティの値を更新後、再度クエリを実行しています。 このコードを実行すると、コンソールには、
5 21.3500
5 20.5

と表示されます。
UnitPriceプロパティに値を代入した後に、NorthwindContextオブジェクトから再度データを取得すると、変更前の値ではなく、変更後の値が取得されます。
これは、EntityFrameworkが変更を追跡しており、自動的にDbContextと同期が取れるようになっているのです。
ただ注意しなければならないのは、まだ、DBへの更新は行われていいないということです。 試しに、using ブロックの後に、以下のコードを続けて実行すると、

 using (var db = new NorthwindContext()) {
     var p = db.Products.Where(x => x.ProductID == 5).SingleOrDefault();
     if (p != null) {
        Console.WriteLine($"{p2.ProductID} {p2.UnitPrice}"); // ★
     }
 }

変更したはずの値ではなく、変更前の値 21.3500 が表示されまます。
実際に、データベースに対して更新を行うには、SaveChanges メソッドを呼び出す必要があります。

 using (var db = new NorthwindContext()) {
     var p = db.Products.Where(x => x.ProductID == 5).SingleOrDefault();
     if (p != null) {
          p.UnitPrice = 20.5m; 
          db.SaveChanges();  // これで変更がDBに反映される。
     }
 }
 
以下の行を追加してみれば、UPDATE文が発行されているのが確認できるはずです。
 
 db.SetLogging();

関連テーブルも同様の方法で更新が可能です。SetLoggingメソッドは以前お見せしたメソッド。

 using (var db = new NorthwindContext()) {
     var prod = db.Products.Where(p => p.ProductID == 34).Single();
     var cat = db.Categories.Where(c => c.CategoryID == 3).Single();
     prod.Category = cat;
     db.SaveChanges();
 }

このコードは、製品のカテゴリを変更するコードです。具体的には

 1). ProductID == 34 のProductを取り出す
 2). CategoryId == 3 のCategoryを取り出す
 3). 取り出したCategoryを、1)で取り出した ProductのCategoryにセットする。

ということをやっています。 prod.CategoryID の値の変更はやっていませんが、この変更はEentityFrameworkが自動でやってくれます。
SaveChanges() は、メモリ内で行ったすべての変更を同等の SQL コマンドに変換し、対応するテーブルに対し、挿入、更新、削除を行います。
挿入、削除の方法については、次回に持ち越します。

▪️おまけ
ConsoleアプリケーションやWindowsFormsアプリケーションの場合、Visual Studio からデバッグ実行すると、Northwind.mdfは、毎回、デバッグ実行の出力フォルダにコピーされることになります。
プログラムで行ったデータベースへの更新は、出力フォルダーにコピーされたデータベースに対して更新が行われます。 そのため、更新、削除、挿入したとしても、再度プログラムを実行すると、変更前の状態のデータベースに対する処理となりますので、デバッグがしやすくなっています。
なお、サーバーエクスプローラに表示されている NORTHWND.MDFは、プロジェクトフォルダにあるNORTHWND.MDFですので、デバッグ実行後に、このNORTHWND.MDFの内容を確認しても、プログラムで変更した内容は反映されていませんので注意してください。  
Posted by gushwell at 22:00Comments(0)TrackBack(0)

2015年10月18日

EntityFramework(10):関連エンティティの明示的読み込み (Explicitly Loading)

   このエントリーをはてなブックマークに追加 Clip to Evernote
Explicitly Loadingはあまり使わないと思うけど、いちおう書いておきます。

遅延読み込みをせずに、自分ですべて関連エンティティの読み込みを制御したい時に利用します。

using (var db = new NorthwindContext()) { db.Database.Log = sql => { Console.Write(sql); }; db.Configuration.LazyLoadingEnabled = false; // ★ var query = from ord in db.Orders where ord.ShipVia == 3 select ord; var order = query.First(); Console.WriteLine(order.Customer.CompanyName); }

★の行で、関連エンティティの遅延読み込みをオフにしているので、 最後の、order.Customer.CompanyName の参照で、NullReferenceException が発生します。

次が、明示的読み込みで、Customer を読み込む例です。

 using (var db = new NorthwindContext()) {
     db.Database.Log = sql => { Console.Write(sql); };
     db.Configuration.LazyLoadingEnabled = false;
     var query = from ord in db.Orders
                 where ord.ShipVia == 3
                 select ord;
     var order = query.First();
     db.Entry(order).Reference(o => o.Customer).Load();
     Console.WriteLine(order.Customer.CompanyName);
 }

Reference メソッドで、読み込みたいエンティティを指定し、続けてLoadメソッド呼び出します。
以下のSQLが発行されます。

SELECT
    [Extent1].[CustomerID] AS [CustomerID],
    [Extent1].[CompanyName] AS [CompanyName],
    [Extent1].[ContactName] AS [ContactName],
    [Extent1].[ContactTitle] AS [ContactTitle],
    [Extent1].[Address] AS [Address],
    [Extent1].[City] AS [City],
    [Extent1].[Region] AS [Region],
    [Extent1].[PostalCode] AS [PostalCode],
    [Extent1].[Country] AS [Country],
    [Extent1].[Phone] AS [Phone],
    [Extent1].[Fax] AS [Fax]
    FROM [dbo].[Customers] AS [Extent1]
    WHERE [Extent1].[CustomerID] = @EntityKeyValue1


関連エンティティが、コレクションの場合は、Referenceメソッドの代わりに、 Collection メソッドを使います。

db.Entry(order).Collection(o => o.Order_Details).Load();

以下発行されるSQLです。

SELECT
  [Extent1].[OrderID] AS [OrderID],
  [Extent1].[ProductID] AS [ProductID],
  [Extent1].[UnitPrice] AS [UnitPrice],
  [Extent1].[Quantity] AS [Quantity],
  [Extent1].[Discount] AS [Discount]
  FROM [dbo].[Order Details] AS [Extent1]
  WHERE [Extent1].[OrderID] = @EntityKeyValue1
  
Posted by gushwell at 22:00Comments(0)TrackBack(0)

2015年10月13日

EntityFramework(9):関連エンティティの一括読み込み (Eagerly Loading)

   このエントリーをはてなブックマークに追加 Clip to Evernote
前回に引き続き、関連エンティティの読み込みについて見ていきます。

Productを読み込んだ時には、関連するCategory も必ず読み込むんだから、 同時に読み込んだほうが効率がいい、という場合もあると思います。
そんなときは、Ibclude メソッドを使います。 前回のコードを書き換えてみます。

書き換え前
 using (var db = new NorthwindContext()) {
     db.SetLogging();
     var query = db.Products
                   .Where(p => p.UnitPrice < 10);
     var product = query.First();
     Console.WriteLine(product.Category.CategoryName);
 }

書き換え後
 using (var db = new NorthwindContext()) {
     db.SetLogging();
     var query = db.Products
                   .Where(p => p.UnitPrice < 10);
     var product = query.Include(nameof(Category)).First();
     Console.WriteLine(product.Category.CategoryName);
 }

includeの引数には、一緒に読み込みたいテーブル名を指定します。
nameof演算子は、C#6.0で追加された演算子です。 Include("Category") と書いても同じ結果になります。

なお、もう一つのオーバーロードメソッドを使って、以下のように書くこともできます。

  product = query.Include(p => p.Category).First();

VS2013以前での開発では、こちらの書き方ほうがインテリセンスが効くので楽かもしれませんね。

発行されるSQLは以下の通りです。

書き換え前
 SELECT TOP (1)
    [Extent1].[ProductID] AS [ProductID],
    [Extent1].[ProductName] AS [ProductName],
    [Extent1].[SupplierID] AS [SupplierID],
    [Extent1].[CategoryID] AS [CategoryID],
    [Extent1].[QuantityPerUnit] AS [QuantityPerUnit],
    [Extent1].[UnitPrice] AS [UnitPrice],
    [Extent1].[UnitsInStock] AS [UnitsInStock],
    [Extent1].[UnitsOnOrder] AS [UnitsOnOrder],
    [Extent1].[ReorderLevel] AS [ReorderLevel],
    [Extent1].[Discontinued] AS [Discontinued]
    FROM [dbo].[Products] AS [Extent1]
    WHERE [Extent1].[UnitPrice] < cast(10 as decimal(18))

 SELECT
    [Extent1].[CategoryID] AS [CategoryID],
    [Extent1].[CategoryName] AS [CategoryName],
    [Extent1].[Description] AS [Description],
    [Extent1].[Picture] AS [Picture]
    FROM [dbo].[Categories] AS [Extent1]
    WHERE [Extent1].[CategoryID] = @EntityKeyValue1

2つのSQLが発行されています。


書き換え後
 SELECT TOP (1)
    [Extent1].[ProductID] AS [ProductID],
    [Extent1].[ProductName] AS [ProductName],
    [Extent1].[SupplierID] AS [SupplierID],
    [Extent1].[CategoryID] AS [CategoryID],
    [Extent1].[QuantityPerUnit] AS [QuantityPerUnit],
    [Extent1].[UnitPrice] AS [UnitPrice],
    [Extent1].[UnitsInStock] AS [UnitsInStock],
    [Extent1].[UnitsOnOrder] AS [UnitsOnOrder],
    [Extent1].[ReorderLevel] AS [ReorderLevel],
    [Extent1].[Discontinued] AS [Discontinued],
    [Extent2].[CategoryID] AS [CategoryID1],
    [Extent2].[CategoryName] AS [CategoryName],
    [Extent2].[Description] AS [Description],
    [Extent2].[Picture] AS [Picture]
    FROM  [dbo].[Products] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Categories] AS [Extent2] ON [Extent1].[CategoryID] =
    [Extent2].[CategoryID]
    WHERE [Extent1].[UnitPrice] < cast(10 as decimal(18))

書き換え後は、一つのSQL文の発行です。

Includeメソッドを使うもう一つのケースは、DbContextを破棄した後に、 関連テーブルのプロパティにアクセスしたい場合です。
例えば、以下のようなコードを書いたとします。

 static void Main(string[] args) {
      Database.SetInitializer<NorthwindContext>(null);
      var product = GetProduct();
      Console.WriteLine(product.Category.CategoryName);  // ★
      Console.ReadLine();
  }
  private static Product GetProduct() {
      using (var db = new NorthwindContext()) {
          db.SetLogging();
          var query = db.Products
                        .Where(p => p.UnitPrice < 10);
          return query.First();
      }
  }

このコードを実行すると、★の行で、'System.ObjectDisposedException' 例外が発生してしまいます。
これは、「すでに、DbContextが破棄されてるから、Categoryを取得する SQL文を発行できないよ」というエラー です。
こんな時に、Includeメソッド を使います。 GetProductメソッドを以下のように書き換えればOKです。

  private static Product GetProduct() {
      using (var db = new NorthwindContext()) {
          db.SetLogging();
          var query = db.Products
                        .Where(p => p.UnitPrice < 10)
                        .Include(nameof(Category));
          return query.First();
      }
  }
  
Posted by gushwell at 22:30Comments(0)TrackBack(0)

2015年10月08日

EntityFramework(8):遅延読み込み

   このエントリーをはてなブックマークに追加 Clip to Evernote
以下のコードを見てください。

  private static void ProcessCustomer(Customer customer) {
      Console.WriteLine(customer.CompanyName);
  }
public static void Sample() { using (var db = new NorthwindContext()) { db.Database.Log = sql => { Console.Write(sql); }; var query = from ord in db.Orders where ord.ShipVia == 3 select ord; foreach (Order order in query) { if (order.ShipCountry == "Brazil") { Console.WriteLine("//////////////////"); ProcessCustomer(order.Customer); } } } }

今回は、処理のSQLの発行タイミングを見るために、ログをコンソールに出力しています。

foreachの中で、orderオブジェクトのCustomerプロパティを参照しています。 このOrderクラスのCustomerプロパティの値はどこで設定されるのでしょうか?

query変数に設定されたクエリが実行されると、要求したオブジェクトだけが実際 に取得されます。関連オブジェクトが自動で同時に取得されることはありません。 つまり、if 文が実行されたときには、まだ、Customerプロパティの値は不定です。
ProcessCustomerメソッドを呼び出したとき(つまり、関連オブジェクトである Customerプロパティにアクセスしようとしたとき)に、初めてCustomerのデータ が取得されます。 これを遅延読み込みと言います。

※ 遅延実行と遅延読み込みは別の概念です。

さらに素晴らしいのは、同一の Customer については、最初の一回のみ SQL が発行されてるという点です。
一度該当する Order の Customer プロパティに値が設定されれば、 2度目のアクセスでは、キャッシュされた値が利用されます。
上のコードを実行してみてもらえればわかりますが、 同一顧客の読み込み(SQL文の発行)は最初の一回だけであることが確認できます。



では、以下のコードはどうでしょうか。

 using (var db = new NorthwindContext()) {
     db.SetLogging();
     var query = db.Products
                   .Where(p => p.UnitPrice < 10);
     var product = query.First();
     Console.WriteLine(product.Category.CategoryName);
 }

このコードでは、First()で即時実行されますが、 WriteLine()実行まで、該当するCategoryテーブルが読み込まれることはありません。
実際に必要になったときに、必要になった分だけを取得するということを Entity Framework がやってくれます。

Logファイルを確認すると、以下の2つのSQLが実行されているのがわかります。

SELECT TOP (1) 
    [Extent1].[ProductID] AS [ProductID], 
    [Extent1].[ProductName] AS [ProductName], 
    [Extent1].[SupplierID] AS [SupplierID], 
    [Extent1].[CategoryID] AS [CategoryID], 
    [Extent1].[QuantityPerUnit] AS [QuantityPerUnit], 
    [Extent1].[UnitPrice] AS [UnitPrice], 
    [Extent1].[UnitsInStock] AS [UnitsInStock], 
    [Extent1].[UnitsOnOrder] AS [UnitsOnOrder], 
    [Extent1].[ReorderLevel] AS [ReorderLevel], 
    [Extent1].[Discontinued] AS [Discontinued]
    FROM [dbo].[Products] AS [Extent1]
    WHERE [Extent1].[UnitPrice] < cast(10 as decimal(18))

SELECT 
    [Extent1].[CategoryID] AS [CategoryID], 
    [Extent1].[CategoryName] AS [CategoryName], 
    [Extent1].[Description] AS [Description], 
    [Extent1].[Picture] AS [Picture]
    FROM [dbo].[Categories] AS [Extent1]
    WHERE [Extent1].[CategoryID] = @EntityKeyValue1

でも、場合によっては、複数のSQL文が発行されることで、 パフォーマンスに影響を与える場合も有り得ます。
そんなときの対策も用意されています。それは次回に。



おまけ - 遅延読み込みをオフにする

 db.Configuration.LazyLoadingEnabled = false;

とすると、遅延読み込みをオフにできます。 この場合、関連エンティティを参照すると、System.NullReferenceException が発生します。

   
Posted by gushwell at 21:00Comments(0)TrackBack(0)

2015年10月04日

EntityFramework(7):リレーションシップその2

   このエントリーをはてなブックマークに追加 Clip to Evernote
いくつかリレーションシップを使った例を紹介しましょう。
ここでは、以下のテーブルを使います。
  1. Category 
  2. Product 
  3. Order 
  4. Order_Detail 

最初の例です。

  using (var db = new NorthwindContext()) {
      var query = (from product in db.Products
                   where product.UnitPrice <= 10
                   from detail in product.Order_Details
                   where detail.Discount > 0
                   select new { 
                       product.ProductID, 
                       product.ProductName, 
                       product.UnitPrice 
                   }).Distinct();
      foreach (var p in query)
          Console.WriteLine($"{p.ProductID} | {p.ProductName} | {p.UnitPrice}");
  }

この例は、UnitPrice が 10 以下の製品で、割り引き有りで売られたことのある 製品を抜き出す例です。

次の例は、Condimentsカテゴリに属する製品のうち、200個以上売上げのあった製 品を抜き出しています。

 using (var db = new NorthwindContext()) {
     var query = db.Products
                   .Where(p => p.Category.CategoryName == "Condiments" &&
                               p.Order_Details.Sum(d => d.Quantity) >= 200);
     foreach (var p in query) {
         Console.WriteLine("{0}  {1}",
                    p.ProductID, p.ProductName);
     }
 }



次に、OrderテーブルとOrder_Detailsの関連に注目し、4つのテーブルを参照する クエリを書いてみます。

 using (var db = new NorthwindContext()) {
     var query = db.Products
                   .Where(x => x.Category.CategoryName == "Condiments");
     var query2 = query.Where(x => x.Order_Details
                                    .Any(o => o.Order.OrderDate < new DateTime(1996, 9, 1)));
     foreach (var p in query2) {
         Console.WriteLine($"{p.ProductID} | {p.ProductName} | {p.CategoryID}");
     }
 }

これは、CategoryNameが"Condiments"である商品のうち、1996/9/1より前に売上げがあった 製品を抜き出している例です。
4つのテーブルを参照するコードも、それほど苦労なく書くことができます。
このように書けるというのは、SQL文が苦手な僕には、夢のようです。

発行されているSQL文を見てみると、Anyメソッドは、SQLのEXIST関数に置き換え られているのが分かります。

SELECT 
    [Extent1].[ProductID] AS [ProductID], 
    [Extent1].[ProductName] AS [ProductName], 
    [Extent1].[SupplierID] AS [SupplierID], 
    [Extent1].[CategoryID] AS [CategoryID], 
    [Extent1].[QuantityPerUnit] AS [QuantityPerUnit], 
    [Extent1].[UnitPrice] AS [UnitPrice], 
    [Extent1].[UnitsInStock] AS [UnitsInStock], 
    [Extent1].[UnitsOnOrder] AS [UnitsOnOrder], 
    [Extent1].[ReorderLevel] AS [ReorderLevel], 
    [Extent1].[Discontinued] AS [Discontinued]
    FROM  [dbo].[Products] AS [Extent1]
    INNER JOIN [dbo].[Categories] AS [Extent2] ON [Extent1].[CategoryID] = [Extent2].[CategoryID]
    WHERE (N'Condiments' = [Extent2].[CategoryName]) AND ( EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[Order Details] AS [Extent3]
        INNER JOIN [dbo].[Orders] AS [Extent4] ON [Extent3].[OrderID] = [Extent4].[OrderID]
        WHERE ([Extent1].[ProductID] = [Extent3].[ProductID]) AND 
              ([Extent4].[OrderDate] < convert(datetime2, '1996-09-01 00:00:00.0000000', 121))
    ))
  
Posted by gushwell at 21:30Comments(0)TrackBack(0)