連載:Entity Framework 4.1入門

第3回 Fluent APIとDbContextの機能

WINGSプロジェクト 土井 毅(監修:山田 祥寛)
2011/08/16
Page1 Page2

DbContextクラスとDbSetクラスの使用方法

 EF 4.1で導入されたDbContextは、データベースに接続するための軽量のコンテキスト・クラスであり、EF 4まで使用されていたObjectContextクラスの機能のうち、頻繁に使用されるものが実装されている。また、DbContextクラスに合わせて導入された、エンティティの集合を表すDbSetクラスも同様に、EF 4までのObjectSetクラスのサブセットとなっている。

 ここでは、DbContextクラスとDbSetクラスによる基本的なクエリ方法と、エンティティの変更履歴管理の方法および同時実行制御について解説する。

基本的なクエリの方法

 DbContextでのクエリには、いくつかの方法がある。

  1. DbSet型のプロパティに対するLINQ(LINQ to Entities)
  2. DbSetクラスのFindメソッド

 1はこれまでのサンプルでも使用してきたように、DbSet型のプロパティに対し、LINQを使ってクエリを行うものである。LINQの使用方法は対象とするデータの種類に依存しないため、これまでLINQを使ってきた開発者であれば、違和感なくLINQ to Entitiesを使用できるだろう。

 ここでは2のDbSetクラスのFindメソッドによるクエリ方法について示す。

 DbSetクラスのFindメソッドは、主キーを指定し、対応する1つのエンティティを取得するメソッドである。複合主キーを持つエンティティの場合、複数のフィールドを順番に並べることでクエリを行える。

 リスト4は、Findメソッドの使用例である。前述の複合主キーを持つCompositeKeyEntitiesのFindメソッドで複数のキーを指定して検索していることに注目してほしい。

using (var db = new ItemCatalog())
{
  // Findメソッドで主キーを使って検索
  var item = db.Items.Find(1);
  Console.WriteLine("Id: {0} , Name: {1} , Member Name: {2} {3}",
                    item.Id, item.Name,
                    item.Member.Name.LastName,
                    item.Member.Name.FirstName);

  // 複合主キーを使ったFindメソッド
  var entity = db.CompositeKeyEntities.Find(1, "foo");
  Console.WriteLine("Key1: {0} , Key2: {1} , Data: {2}",
                    entity.Key1,entity.Key2,entity.Data);

  // 入力待ち
  Console.ReadKey();
}
Using db As New ItemCatalog

  ' Findメソッドで主キーを使って検索
  Dim item = db.Items.Find(1)
  Console.WriteLine("Id: {0} , Name: {1} , Member Name: {2} {3}",
                    item.Id, item.Name,
                    item.Member.Name.LastName,
                    item.Member.Name.FirstName)

  ' 複合主キーを使ったFindメソッド
  Dim entity = db.CompositeKeyEntities.Find(1, "foo")
  Console.WriteLine("Key1: {0} , Key2: {1} , Data: {2}",
                    entity.Key1, entity.Key2, entity.Data)

  ' 入力待ち
  Console.ReadKey()

End Using
リスト4 Findメソッドを使った主キーによる検索(上:Program.cs、下:Module1.vb)

 実行結果は図8のようになる。

図8 Findメソッドの実行結果

 Findメソッドのほか、DbSetクラスには表2のようなメソッドが準備されている。

メソッド名 意味
Add(Object entity) エンティティをコレクションに追加する
Create() 新しいエンティティを作成する。作成したエンティティはコレクションには追加されていないことに注意
Remove(Object entity) エンティティをコレクションから削除する
SqlQuery(string sql,params Object[] parameters) エンティティを返すパラメータ付きSQLを実行する
表2 DbSetクラスのメソッド

変更履歴管理

 変更履歴管理とは、特定のエンティティがデータベースから読み込んだ状態のままか、あるいは変更/削除/追加されたものか、といった情報を管理することである。EF 4において、自己追跡エンティティという機能が追加され、POCOベースのエンティティ・クラスを基に、変更履歴の自動的な記録が可能となった。

 ただし、EF 4でエンティティの変更履歴管理を行うためには、自己追跡エンティティという、POCOベースではあるものの、特別な機能を持つクラスでエンティティを定義する必要があった。EF 4での変更履歴管理については、「連載:ADO.NET Entity Framework入門 第6回 EF4によるN層アーキテクチャと自己追跡エンティティ【前編】」を参照してほしい。

 それに対し、EF 4.1のDbContextクラスでは、任意のエンティティ・クラスについて、変更履歴の管理が可能となった。

 使用方法はシンプルで、DbContextクラスのEntryメソッドで取得できるDbEntityEntryオブジェクトから、エンティティの変更情報を取得できる。DbEntityEntryオブジェクトのStateプロパティはそのエンティティの状態を表す(表2)。

状態 意味
Detached エンティティは作成直後で、まだコレクションに追加されていない
Unchanged エンティティは変更されていない。SaveChangesメソッドで変更を反映した後もこの状態になる
Added エンティティはコレクションに追加されたが、まだ変更は反映されていない
Deleted エンティティは削除された
Modified エンティティは変更された
表2 Stateプロパティの意味

 また、DbEntityEntryオブジェクトのPropertyメソッドから取得できるDbPropertyEntryオブジェクトでは、表3のようなプロパティにより、特定のプロパティの変更前/変更後の値、および変更状態を取得できる。

プロパティ 意味
CurrentValue 変更前のプロパティの値
OriginalValue 変更後のプロパティの値
IsModified プロパティの値が変更されたかどうか
表3 DbPropertyEntryクラスのプロパティ

 変更履歴管理はデフォルトで有効になっており、便利な機能であるが、大量のエンティティを追加する場合などは、処理のオーバーヘッドを避けるため明示的に変更履歴管理を無効化することが望ましい。

 変更履歴管理の有効/無効はDbContextオブジェクトのConfiguration.AutoDetectChangesEnabledプロパティで指定できる。このプロパティで履歴管理を無効化すると、エンティティのStateプロパティや、プロパティのIsModifiedプロパティが正しく動作しなくなる。ただし、変更履歴管理を無効にしても、プロパティごとのCurrentValueプロパティとOriginalValueプロパティは異なる値を保持し続ける。

 リスト5は、変更履歴管理により、エンティティの追加や変更内容を取得するサンプルである。

// 変更履歴管理サンプル
using (var db = new ItemCatalog())
{
  // 新しいエンティティの作成
  var newItem = db.Items.Create();
  // コレクションに追加
  db.Items.Add(newItem);
  Console.WriteLine("新しいアイテムの状態:" + db.Entry<Item>(newItem).State);

  // 次のコメントアウトを外すと、変更履歴管理が無効になる
  // db.Configuration.AutoDetectChangesEnabled = false;

  var item = db.Items.Find(1);
  // プロパティの書き換え
  item.Name = "冷蔵庫";

  // レコードの状態を取得
  var state = db.Entry<Item>(item).State;

  // プロパティの元の値と現在の値を取得
  string originalValue = db.Entry<Item>(item).Property(i => i.Name).OriginalValue;
  string currentValue = db.Entry<Item>(item).Property(i => i.Name).CurrentValue;

  // プロパティが変更されたかどうかを取得
  bool modified = db.Entry<Item>(item).Property(i => i.Name).IsModified;

  Console.WriteLine(
    "original : {0} , current : {1} , modified: {2} , state : {3}"
    ,originalValue,currentValue,modified,state);
}

// ▼実行結果
// 新しいアイテムの状態:Added
// original : パソコン , current : 冷蔵庫 , modified: True , state : Modified
// --- AutoDetectChangesEnabled = falseにして変更履歴管理を無効化した場合下記のようになる ---
// original : パソコン , current : 冷蔵庫 , modified: False , state : Unchanged
// ↑新旧のプロパティの値は正しく保持されているが、プロパティのIsModifiedとエンティティのStateは正しくない
' 変更履歴管理サンプル
Using db As New ItemCatalog

  ' 新しいエンティティの作成
  Dim newItem = db.Items.Create()
  ' コレクションに追加
  db.Items.Add(newItem)
  Console.WriteLine("新しいアイテムの状態:{0}" _
   , db.Entry(Of Item)(newItem).State)

  ' 次のコメントアウトを外すと、変更履歴管理が無効になる
  'db.Configuration.AutoDetectChangesEnabled = false

  Dim Item = db.Items.Find(1)
  ' プロパティの書き換え
  Item.Name = "冷蔵庫"

  ' レコードの状態を取得
  Dim state = db.Entry(Of Item)(Item).State

  ' プロパティの元の値と現在の値を取得
  Dim originalValue = db.Entry(Of Item)(Item).Property(Function(i) i.Name).OriginalValue
  Dim currentValue = db.Entry(Of Item)(Item).Property(Function(i) i.Name).CurrentValue

  ' プロパティが変更されたかどうかを取得
  Dim modified = db.Entry(Of Item)(Item).Property(Function(i) i.Name).IsModified

  Console.WriteLine(
    "original : {0} , current : {1} , modified: {2} , state : {3}" _
    , originalValue, currentValue, modified, state)

End Using

' ▼実行結果
' 新しいアイテムの状態:Added
' original : パソコン , current : 冷蔵庫 , modified: True , state : Modified
' --- AutoDetectChangesEnabled = falseにして変更履歴管理を無効化した場合下記のようになる ---
' original : パソコン , current : 冷蔵庫 , modified: False , state : Unchanged
' ↑新旧のプロパティの値は正しく保持されているが、プロパティのIsModifiedとエンティティのStateは正しくない
リスト5 変更履歴管理により、エンティティの追加や変更内容を取得するサンプル(上:Program.cs、下:Module1.vb)

楽観的同時実行制御

 同時実行制御とは、データベースに更新をかける際に、書き込みの競合を避けるための処理のことである。同時実行制御を適切に行わない場合、図9のように書き込んだはずのデータが失われてしまいかねない。同時実行制御の詳細については、「できるエンジニアになる! ちょい上DB術・基礎編 第2回 【DB概論】DBMSに求められるもの(1)排他制御とACID属性」などを参照してほしい。

図9 同時実行制御を行わない場合のデータ損失

 EF 4.1では、簡単に楽観的(オプティミスティック)同時実行制御を行うための仕組みが用意されている。楽観的同時実行制御とは、図10のようにデータの書き込み時に、自分が読み込んだ時のデータと、データベース上のデータを比較し、自分が読み込んで以降、誰も変更していないかどうかを確認する手法である。楽観的同時実行制御により、自分以外のユーザーの更新を適切に検出できるようになり、データの損失を避けることができる。

図10 楽観的同時実行制御の仕組み

 EF 4.1では、リスト6のようにTimestampというアノテーションにより、データベースのレコードのバージョンを表すプロパティを表現できる。Timestampアノテーションを付加したプロパティは、SQL Server上ではrowversion型(以前のTimestamp型)のフィールドとなり、データベース側でレコードの更新ごとに自動的にバージョンを表す数値が格納される。このフィールドを使うことで、自分がそのレコードを読み込んで以降、変更されていないかどうかを確認できる。

public class Item
{
  ……省略……

  // 同時実行制御用のフィールド
  [Timestamp]
  public Byte[] Timestamp { get; set; }
}
Public Class Item

  ……省略……

  ' 同時実行制御用のフィールド
  <Timestamp()>
  Public Property Timestamp As Byte()
End Class
リスト6 Timestampアノテーション(上:Item.cs、下:Item.vb)

 EF 4.1では、Timestampアノテーションが付加されたプロパティがある場合、DbContextクラスのSaveChangesメソッドを呼び出した際に、そのプロパティの値とデータベース上の値の比較が行われる。もし異なっている場合(=自分が読み込んで以降、誰かが書き込んだ場合)には、DbUpdateConcurrencyException例外(System.Data.Entity.Infrastructure名前空間)が発生し、書き込みは行われない。

 リスト7は楽観的同時実行制御のサンプルである。

using (var db = new ItemCatalog())
{
  // 読み込んだエンティティのプロパティを書き換え
  var item = db.Items.Find(1);
  item.Name = "冷蔵庫";

  // 同時実行制御のテストのためのキー入力待ち
  Console.ReadKey();
  try
  {
    // 保存処理
    db.SaveChanges();
  }
  catch (DbUpdateConcurrencyException)
  {
    // 保存失敗
    Console.WriteLine("楽観的同時実行制御により、書き込み失敗");
  }
}
Using db As New ItemCatalog

  ' 読み込んだエンティティのプロパティを書き換え
  Dim Item = db.Items.Find(1)
  Item.Name = "冷蔵庫"

  ' 同時実行制御のテストのためのキー入力待ち
  Console.ReadKey()
  Try

    ' 保存処理
    db.SaveChanges()

  Catch e As DbUpdateConcurrencyException

    ' 保存失敗
    Console.WriteLine("楽観的同時実行制御により、書き込み失敗")

  End Try
End Using
リスト7 楽観的同時実行制御のサンプル(上:Program.cs、下:Module1.vb)

 ビルド後、以下の手順で同時実行制御のテストを行える。

  1. コマンド・プロンプトから「bin\Debug\EFCodeFirstSampleCS.exe」(VBは「EFCodeFirstSampleVB.exe」)を実行(プロセスA)

  2. 別のコマンド・プロンプトから同じ実行ファイルを実行(プロセスB):
    この時点で、両プロセスで同じレコードが読み込まれ、両方ともプログラム中で書き換えられているが、データベースには反映されていない。

  3. プロセスAでキー入力:
    これにより、プロセスAがSaveChangesメソッドでデータベースに変更を反映する。保存は成功し、Timestampフィールドの値は更新される。

  4. プロセスBでキー入力:
    プロセスBもSaveChangesメソッドで保存しようとするが、2の時点で読み込んだTimestampフィールドの値は、3で書き換えられたため、楽観的同時実行制御により、保存は失敗し、DbUpdateConcurrencyException例外が発生する。

 楽観的同時実行制御はEF 4においてもサポートされていたが、このサンプルからも分かるとおり、EF 4.1ではTimestampアノテーションにより、簡潔な記述で行えるようになっている。

まとめ

 全3回のシリーズで、EF 4.1の新機能について解説してきた。EF 4.1の目玉であるコード・ファースト機能は、これまでになくスピーディなデータベース開発を可能にしてくれる。また、今回解説したDbContextクラスの新機能である、自動的な変更履歴管理と楽観的同時実行制御は、コード・ファースト以外でも使用可能で、実際のアプリケーションにおいて有用な機能である。EF 4までの記述よりもずっとシンプルに機能を使えるため、ぜひ活用してほしい。

 マイクロソフトの一押しフレームワークであるASP.NET MVCが、Entity Framework 4.1との強力な連携機能を搭載していることからも分かるように、Entity Frameworkは今後も重要なデータ・アクセス技術と位置付けられるだろう。本連載がEntity Framework活用の一助になれば幸いである。end of article

 

 INDEX
  [連載]Entity Framework 4.1によるコード・ファースト開発
  第3回 Fluent APIとDbContextの機能
    1.外部からデータベース構造を設定するFluent API
  2.DbContext、DbSetクラスの使用方法

インデックス・ページヘ  「連載:Entity Framework 4.1入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間