次の方法で共有


Entity Framework

Entity Framework June CTP における新機能

Srikanth Mandadi

 

先日リリースされた Microsoft Entity Framework (EF) June 2011 CTP では、列挙型、空間型、テーブル値関数 (TVF) など、要望の多かった機能が多数サポートされています。今回の記事では、これらの機能を簡単なチュートリアルを使用して説明します。説明にあたり、EF (http://bit.ly/oLbjp0) および EF 4.1 で導入された Code First 開発パターン (http://bit.ly/oQ77Hm) についての知識があることを前提にします。

この記事のサンプルを実行するための要件は、次のとおりです。

  • Visual Studio 2010 Express および SQL Server 2008 R2 Express、またはそれ以降のバージョン (Visual Studio Express エディションと SQL Server Express は、http://bit.ly/rsFvxJ (英語) からダウンロード可能)
  • Microsoft EF June 2011 CTP および EF Tools June 2011 CTP (どちらも、http://bit.ly/mZgQIS (英語) からダウンロード可能)
  • Northwind データベース (http://bit.ly/pwbDoQ (英語) からダウンロード可能)

では、始めましょう。

列挙型

最初に取り上げるのは、EF で最も要望の多かった機能の 1 つ、列挙型です。列挙型は、C# や Visual Basic などの .NET 言語を含む、多くのプログラミング言語でサポートされています。EF では、列挙型を CLR 型で使用し、基盤となる Entity Data Model (EDM) にマップし、データベースに値を保存できるようにすることを目的としています。詳細に入る前に、簡単な例を見てみましょう。列挙型は、Code First アプローチ、Database First アプローチ、および Model First アプローチでサポートされます。ここでは、まず、Database First アプローチから説明し、次に Code First アプローチの使用例を示します。

Database First アプローチの例では、Northwind データベースの Products テーブルを使用します。モデルを追加する前に、EF June 2011 CTP を対象に指定する必要があります。そのため、以下の手順を実行します。

  1. Visual Studio 2010 を起動し、C# コンソール アプリケーション プロジェクトを新規作成します。
  2. ソリューション エクスプローラーで、作成したプロジェクトを右クリックし、[プロパティ] をクリックします。
  3. [対象のフレームワーク] ボックスの一覧から [Microsoft Entity Framework June 2011 CTP] をクリックします (図 1 参照)。
  4. Ctrl キーを押しながら S キーを押し、プロジェクトを保存します。Visual Studio は、プロジェクトをいったん閉じてから再度開く許可を求めるため、[はい] をクリックします。
  5. プロジェクトに新しくモデルを追加するには、プロジェクトを右クリックし、[追加] をポイントし、[新しい項目] をクリックします (または、Ctrl キーと Shift キーと A キーを同時に押します)。次に、Visual C# アイテムの [ADO.NET Entity Data Model] をクリックし (この例では CTP1EnumsModel.edmx と名前を付け)、[追加] をクリックします。
  6. ウィザードの手順に従い、Northwind データベースを参照します。[テーブル] を展開し、[Products] チェック ボックスをオンにし、[完了] をクリックします。
  7. 1 つのエンティティを備えたエンティティ モデルが作成されます (図 2 参照)。

Targeting the Entity Framework June 2011 CTP
図 1 Entity Framework June 2011 CTP を対象に指定

Entity Model for Product Entity
図 2 Product エンティティのエンティティ モデル

次に示すのは、Beverages カテゴリに属するすべての商品を取得する LINQ クエリです。Beverages の CategoryID は 1 です。

var ctx = new NorthwindEntities();
var beverageProducts = from p in ctx.Products
                       where p.CategoryID == 1

当然ながら、Beverages の CategoryID が 1 だということを知らなければ、このクエリは作成できません。それには、CategoryID を記憶しているか、データベースを調べる必要があります。このクエリの別の問題点は、どのカテゴリを照会しているのかが、コードからわかりにくいことです。CategoryID を使用するすべての場所になんらかのドキュメントを用意するか、コードを見る人が、さまざまなカテゴリの CategoryID 値を把握している必要があります。

新しい商品を挿入しようとすると、また別の問題が生じます。Products テーブルに商品を挿入するには、次のコードを使用します。

var ctx = new NorthwindEntities();
var product = new Product() { ProductName = "place holder",
  Discontinued = false, CategoryID = 13 };
ctx.AddToProducts(product);
ctx.SaveChanges();

このコードを実行すると、データベースは外部キー制約の例外を通知します。この例外は、ID の値が 13 のカテゴリが存在しないために起こります。プログラマが、カテゴリの一覧から正しいカテゴリを割り当てる方法があれば、有効な整数値のセットを記憶するよりもはるかに簡単でしょう。

モデルに列挙型を導入すると、このシナリオがどのように改善されるのかを見てみましょう。

次に示すのは、CategoryID を列挙型に変換する手順です。

  1. CTP1EnumsModel.edmx ファイルをダブルクリックし、デザイナーでモデルを開きます。
  2. Product エンティティの [CategoryID] プロパティを右クリックし、[Convert to Enum] (列挙型に変換) をクリックします。
  3. 列挙型を作成し、新しく開いたダイアログ ボックスに列挙型メンバーの値を入力します (図 3 参照)。[Name] (名前) ボックスに「Category」と入力し、[Underlying Type] (基になる型) ボックスの一覧の [Byte] をクリックします。基になる型は、列挙型の値空間を表す整数型で、列挙型のメンバー数に基づいて選択できます。この例では、メンバー数が 8 なので、byte 型が適しています。メンバーを、CategoryID 値の昇順に入力します。最初のカテゴリ (Beverages) の値に「1」と入力し、他のメンバーの値は、データベースで自動的に 1 ずつ増えるので、空欄のままにします。これは EF での既定の動作です。ただし、データベースで異なる値を付ける場合は、すべてのカテゴリに値を入力することになります。Beverages の値が 1 ではなく 0 の場合は、空欄のままにします。これは、EF が列挙型の最初のメンバー値に既定で 0 を設定するためです。
  4. 列挙型を作成するとき、[Is Flag?] (フラグ設定) チェック ボックスを使用して、フラグに設定できます。これは、コードの生成中にだけ使用でき、チェック ボックスをオンにした場合は、列挙型は Flags 属性で生成されます (flags 列挙の詳細については、http://bit.ly/oPqiMp (英語) を参照してください)。今回の例では、このチェック ボックスをオフにします。
  5. アプリケーションをリビルドし、コードを再生成すると、コードに列挙型が含まれるようになります。

The Enum Type Creation Window
図 3 列挙型の作成画面

Beverages に属するすべての商品を取得するクエリは、次のように書き直すことができます。

var ctx = new NorthwindEntities();
var beverageProducts = from p in ctx.Products
                       where p.Category == Category.Beverages
                       select p;

IntelliSense を活用してクエリを作成でき、データベースで Beverages の値を調べる必要がなくなります。同様に、更新の際にも、IntelliSense が正しいカテゴリ値を表示します。

ここまでは、Database First アプローチを使用した列挙型について説明してきました。次に、Code First アプローチで、列挙型を使用して Beverages に属するすべての商品を取得するクエリを作成します。そのため、別のコンソール アプリケーションを作成し、図 4 に示す型を備えた C# ファイルを追加します。

図 4 Code First アプローチによる列挙型の使用

public enum Category : byte
{
  Beverages = 1,
  Condiments,
  Confections,
  Dairy,
  Grains,
  Meat,
  Produce,
  Seafood
}
 
public class Product
{
  public int ProductID { get; set; }
  public string ProductName { get; set; }
  public int? SupplierID { get; set; }
  [Column("CategoryID", TypeName = "int")]
  public Category Category { get; set; }
  public string QuantityPerUnit { get; set; }
  public decimal? UnitPrice { get; set; }
  public short? UnitsInStock { get; set; }
  public short? UnitsOnOrder { get; set; }
  public short? ReorderLevel { get; set; }
  public bool Discontinued { get; set; }
}
public class EnumsCodeFirstContext : DbContext
{
  public EnumsCodeFirstContext() : base(
    "data source=<server name>; initial catalog=Northwind;
    integrated security=True;multipleactiveresultsets=True;")
  {
  }
  public DbSet<Product> Products { get; set; }
}

EnumsCodeFirstContext クラスは、DbContext から継承します。DbContext は、EF 4.1 で新しく導入された型で、ObjectContext と似ていますが、より単純で使用しやすい型です (DbContext API の使用法の詳細については、http://bit.ly/eeEsyt (英語) を参照してください)。

図 4 で示したコードには、いくつか説明が必要な点があります。

  • Category プロパティの上にある Column 属性: CLR プロパティと列の名前または型が異なる場合に、2 つをマップするために使用します。
  • EnumsCodeFirstContext のコンストラクター (接続文字列を渡して、基本クラスのコンストラクターを呼び出します): 既定では、DbContext は、そこから派生する完全修飾形式でクラス名を付けたデータベースをローカル SqlExpress に作成します。このサンプルでは、単に既存の Northwind データベースを使用します。

これで、次に示すような Database First アプローチの場合と類似のコードで、Beverages カテゴリに属するすべての商品を取得できます。

EnumsCodeFirstContext ctx = new EnumsCodeFirstContext();
var beverageProducts = from p in ctx.Products
                       where p.Category == Category.Beverages
                       select p;

テーブル値関数 (TVF)

新しい CTP で追加されたもう 1 つ重要な機能は TVF のサポートです。TVF は、ストアド プロシージャと非常によく似ていますが、1 つ重要な違いがあり、TVF の結果は構成可能です。つまり、TVF の結果をその外側のクエリで使用できます。そのため、EF を使用する開発者にとって重要な点は、ストアド プロシージャは LINQ クエリで使用できませんが、TVF は使用できることです。EF アプリケーションで TVF を使用する方法を、例を用いて説明します。説明の過程で、SQL Server のフルテキスト検索 (FTS) 機能を活用する方法を取り上げます (詳細については、http://bit.ly/qZXG9X (英語) を参照してください)。

FTS 機能は、いくつかの述語と TVF を使用して公開します。以前のバージョンの EF では、フルテキスト TVF を使用するには、ExecuteStoreCommand を使って T-SQL スクリプトで呼び出すか、ストアド プロシージャを使用するしかありませんでした。しかし、どちらの方法も構成が不可能で、LINQ to Entities で使用できません。次の例では、新しい CTP でサポートされている TVF を用いて、これらの関数を構成可能な関数として使用する方法を紹介します。説明には、MSDN ドキュメントに掲載されている ContainsTable のクエリを使用しています (http://bit.ly/q8FFws、英語)。このクエリは、"breads"、"fish"、または "beers" という語句を含むすべての商品名を、それぞれの語句に異なる重みで検索します。検索基準に一致して返された各行について、相対的な一致度 (順位値) を表示します。

    SELECT FT_TBL.CategoryName, FT_TBL.Description, KEY_TBL.RANK
      FROM Categories AS FT_TBL
        INNER JOIN CONTAINSTABLE(Categories, Description,
        'ISABOUT (breads weight (.8),
        fish weight (.4), beers weight (.2) )' ) AS KEY_TBL
          ON FT_TBL.CategoryID = KEY_TBL.[KEY]
    ORDER BY KEY_TBL.RANK DESC;

同様のクエリを、LINQ to Entities で作成しましょう。残念ながら、ContainsTable を直接 EF に公開することはできません。なぜなら、EF は最初の 2 つのパラメーター (テーブルと列の名前) を引用符で囲まれていない識別子だと見なすからです (つまり、‘Categories’ ではなく Categories)。そのため、EF に、その 2 つのパラメーターを特別に処理させることができません。この制限を回避するには、ContainsTable を別のユーザー定義 TVF でラップします。次の SQL を実行し、ContainsTableWrapper という TVF を作成します (この TVF は、Categories テーブルの列 Description 上で ContainsTable 関数を実行します)。

    Use Northwind;
    Create Function ContainsTableWrapper(@searchstring nvarchar(4000))
    returns table
    as
    return (select [rank], [key] from ContainsTable(Categories, Description,
      @searchstring))

次に、EF アプリケーションを作成し、この TVF を使用します。列挙型の例で、コンソール アプリケーションを作成したうえで、Northwind データベースを参照し、エンティティ モデルを追加したのと同じ手順を実行します。Categories、Products、および新しく作成した TVF を含めると、図 5 に示すようなモデルになります。

Entity Model with Products and Categories from Northwind
図 5 Northwind データベースの Products と Categories を使用するエンティティ モデル

TVF は、デザイナー画面には表示されませんが、Store セクションの [Stored Procedures/Functions] (ストアド プロシージャ/関数) を展開すると、モデル ブラウザーで確認できます。

この関数を LINQ で使用するには、関数スタブを追加します (詳細については、http://bit.ly/qhIYe2 (英語) を参照してください)。ここでは、関数スタブを、ObjectContext クラスの部分クラスに追加しました (この例では NorthwindEntities)。

public partial class NorthwindEntities
  {
    [EdmFunction("NorthwindModel.Store", "ContainsTableWrapper")]
    public IQueryable<DbDataRecord> ContainsTableWrapper(string searchString)
    {
      return this.CreateQuery<DbDataRecord>(
        "[NorthwindModel.Store].[ContainsTableWrapper](@searchstring)",
        new ObjectParameter[] {
        new ObjectParameter("searchString", searchString)});
    }
  }

これで、クエリでこの関数を使用できるようになりました。次のコードでは、単純に Key を出力します (つまり、前述したフルテキストのクエリのための CategoryId と Rank です)。

var ctx = new NorthwindEntities();
var fulltextResults = from r in ctx.ContainsTableWrapper("ISABOUT (breads weight (.8),
  fish weight (.4), beers weight (.2) )")
                    select r;
foreach (var result in fulltextResults)
{
  Console.WriteLine("Category ID:" +  result["Key"] + "   Rank :" + result["Rank"]);
}

このコードを実行すると、次のようなコンソール出力が表示されます。

Category ID:1   Rank :15
Category ID:3   Rank :47
Category ID:5   Rank :47
Category ID:8   Rank :31

ただ、これは作成しようとしていたクエリではありません。本当に必要としているのは、もう少し多くのことを実行できるクエリです。つまり、CategoryName と Description を順位を付けて出力するクエリです。その方が、CategoryID だけを出力するよりも興味深いでしょう。次に、その基となるクエリを示します。

    SELECT FT_TBL.CategoryName, FT_TBL.Description, KEY_TBL.RANK
      FROM Categories AS FT_TBL
        INNER JOIN CONTAINSTABLE(Categories, Description,
        'ISABOUT (breads weight (.8),
        fish weight (.4), beers weight (.2) )' ) AS KEY_TBL
          ON FT_TBL.CategoryID = KEY_TBL.[KEY]
    ORDER BY KEY_TBL.RANK DESC;

このクエリに LINQ を使用するには、複合型またはエンティティの戻り値の型を指定して、EDM での関数インポートに TVF をマップする必要があります。なぜなら、行の種類を返す関数インポートは、構成できないためです。

マッピングの手順は、次のとおりです。

  1. モデル ブラウザーの Store セクションの関数をダブルクリックし、[関数インポートの追加] ダイアログ ボックスを開きます (図 6 参照)。
  2. [関数インポート名] ボックスに、「ContainsTableWrapperModelFunction」と入力します。
  3. [Function Import is Composable?] (関数インポートを構成可能にする) チェック ボックスをオンにします。
  4. [Stored Procedure/Function Name] (ストアド プロシージャ/関数名) ボックスの一覧の [ContainsTableWrapper] 関数をクリックします。
  5. [列情報の取得] をクリックし、ボタンの下に、関数が返した結果の型についての情報を含んだテーブルを作成します。
  6. [新しい複合型の作成] をクリックすると、[次の要素のコレクションを返します] の [複合] オプション ボタンがオンになり、その複合型に生成された名前が表示されます。
  7. [OK] をクリックします。

Mapping a TVF to a Function Import
図 6 関数インポートへの TVF のマッピング

モデルで関数インポートにマップした TVF については、関数が自動生成されるため、対応する関数をコードに追加する必要はありません。

次に示すように、FTS 機能で ContainsTable を使用する T-SQL クエリを、LINQ で作成できるようになります。

var ctx = new NorthwindEntities();
var fulltextResults = from r in ctx.ContainsTableWrapperModelFunction("ISABOUT
  (breads weight (.8), fish weight (.4), beers weight (.2) )")
                     join c in ctx.Categories
                     on r.key equals c.CategoryID
                     select new { c.CategoryName, c.Description, Rank = r.rank };
 
foreach (var result in fulltextResults)
{
  Console.WriteLine("Category Name:" + result.CategoryName + "   Description:" +
    result.Description + "   Rank:" + result.Rank);
}

このコードを実行すると、コンソール出力は次のようになります。

Category Name:Beverages   Description:Soft drinks, coffees, teas, beers, and ales   Rank:15
Category Name:Confections   Description:Desserts, candies, and sweet breads   Rank:47
Category Name:Grains/Cereals   Description:Breads, crackers, pasta, and cereal Rank:47
Category Name:Seafood   Description:Seaweed and fish   Rank:31

空間型のサポート

次に紹介する非常にすばらしい新機能は、空間型のサポートです。EDM に、DbGeometry 型と DbGeography 型の、2 つの新しい型が追加されました。次の例で示すように、Code First アプローチを使って、EF で空間型を簡単に使用できます。

EFSpatialSample というコンソール アプリケーション プロジェクトを作成し、次の型を含む C# ファイルをプロジェクトに追加します。

namespace EFCTPSpatial
{
  public class Customer
  {
    public int CustomerID { get; set; }
    public string Name { get; set; }
    public DbGeography Location { get; set; }
  }
 
  public class SpatialExampleContext : DbContext
  {
    public DbSet<Customer> People { get; set; }
  }
}

Customer の Location プロパティは、新しい CTP の System.Data.Spatial 名前空間に追加された DbGeography 型です。SQL Server の場合、DbGeography は SqlGeography にマップされます。これらの型を使用して空間データを挿入し、LINQ を使用してデータを照会します (図 7 参照)。

図 7 空間データの操作

static void Main(string[] args)
  {
    var ctx = new SpatialExampleContext();
    ctx.Customers.Add(new Customer() { CustomerID = 1, Name = "Customer1",
      Location = DbGeography.Parse(("POINT(-122.336106 47.605049)")) });
    ctx.Customers.Add(new Customer() { CustomerID = 2, Name = "Customer2",
      Location = DbGeography.Parse(("POINT(-122.31946 47.625112)")) });
    ctx.SaveChanges();
 
    var customer1 = ctx.Customers.Find(1);
    var distances = from c in ctx.Customers                           
                    select new { Name = c.Name, DistanceFromCustomer1 =
                    c.Location.Distance(customer1.Location)};
    foreach (var item in distances)
    {
      Console.WriteLine("Customer Name:" + item.Name + ",
        Distance from Customer 1:" + (item.DistanceFromCustomer1 / 1609.344 ));
    }               
  }

このコードで起こるのは、非常に簡単なことです。2 つの別々の場所に、2 つの Customer を作成し、Well-Known Text を使用して場所を指定します (このプロトコルの詳細については、http://bit.ly/owIhfu (英語) を参照してください)。これらの変更は、データベースに保存されます。次に、LINQ to Entities クエリが、Customer1 とテーブルの各 Customer の距離情報を取得します。距離は、メーターからマイルに変換するために、1609.344 で除算して求めます。プログラムの出力は、次のようになります。

Customer Name:Customer1,  Distance from Customer 1:0
Customer Name:Customer2,  Distance from Customer 1:1.58929160985881

当然ですが、Customer1 と Customer1 の距離は 0 です。Customer 1 と Customer 2 の距離は数マイルです。このクエリにおける距離の計算は、STDistance 関数を使用してデータベースで実行されます。次に示すのは、データベースに送信される SQL クエリです。

    SELECT 1 AS [C1], [Extent1].[Name] AS [Name], [Extent1].[Location].STDistance(@p__linq__0)
      AS [C2]
    FROM [dbo].[Customers] AS [Extent1]

LINQ クエリの自動コンパイル

新しい CTP には、LINQ to Entities クエリを作成するとき、EF が C# または Visual Basic コンパイラで生成される "式" ツリーを確認し、SQL に変換 (またはコンパイル) する新機能があります。ただし、式ツリーを SQL にコンパイルする際、特にクエリが複雑な場合には、いくぶんオーバーヘッドが生じます。LINQ クエリが実行されるたびに、このパフォーマンスの低下を招くことを回避するため、クエリをコンパイルし、再利用できます。CompiledQuery クラスを使用すれば、オーバーヘッドが生じるのはコンパイルの際の 1 回だけになり、EF のキャッシュでコンパイルしたクエリを直接参照するデリゲートを返します。

June CTP は、LINQ クエリの自動コンパイルと呼ばれる新機能をサポートしており、実行するすべての LINQ to Entities クエリが自動的にコンパイルされ、EF クエリのキャッシュに保存されます。次にクエリを実行するときはいつでも、EF はクエリをキャッシュから見つけるため、すべてのコンパイル プロセスを再度実行する必要はありません。また、この機能は内部で LINQ を使用するため、クエリを WCF Data Services を用いて発行するのを容易にします。式ツリーの詳細については、http://bit.ly/o5X3rA (英語) を参照してください。

まとめ

ご覧のとおり、新しくリリースされた EF には、数多くのすばらしい機能が追加されています。ここで紹介した以外にも、たとえば、Table per Type (TPT) の SQL 生成が強化されたり、ストアド プロシージャでマッピングや複数の結果セットを処理したりできるようになりました。新しい CTP では、これらの機能を試し、バグの修正や新機能のエクスペリエンスの向上に役立てるため、フィードバックを返す機会を提供しています。Microsoft Data Developer Connect の Web サイト (http://connect.microsoft.com/data) からバグを報告したり、EF ユーザーの声 Web サイト (http://ef.mswish.net、英語) から新機能の要望を伝えられます。

Srikanth Mandadi は、Entity Framework チームの開発リーダーです。

この記事のレビューに協力してくれた Entity Framework チームの技術スタッフに心より感謝いたします。