データ ポイント

抵抗を受けないよう Entity Framework によるテーブルへのアクセスを拒否する

Julie Lerman

Julie LermanEntity Framework がコマンドを作成するしくみを見てデータベース所有者がもらす最初の感想の 1 つは、「えっ、テーブルへのアクセスを許可しなければならないのですか」というものです。データベース所有者がこのように反応するのも、Entity Framework の中核機能の 1 つが SELECT、UPDATE、INSERT、および DELETE の各コマンドを生成することだからです。

今月のコラムでは、こうしたデータベースの管理者向けに Entity Framework のコマンド生成のしくみを解説し、Entity Framework からの操作をビューとストアド プロシージャに限定して許可することでデータベースへのアクセスを制限する機能を紹介します。この機能は、アプリケーション コードに影響を及ぼすことも、同じチームの開発者を敵に回すこともなく使用できます。

既定のコマンド生成について

このコマンド生成のしくみはどのようになっているのでしょう。Entity Framework で重要なのは、Entity Data Model (EDM)、つまりアプリケーションのドメイン オブジェクトを記述する概念モデルです。Entity Framework を使用すると、開発者はデータベースの詳細を意識することなくモデルに対するクエリを記述できます。モデル、モデルのエンティティ、およびエンティティ間のリレーションシップは XML 形式で定義されるため、開発者はモデルのエンティティに基づいて厳密に型指定されたクラスを操作できます。Entity Framework ランタイムでは、モデルの XML を追加のメタデータ (データベース スキーマ、およびモデルをデータベース スキーマに変換するマッピングの記述) と組み合わせて使用し、クラスとデータベースの間の橋渡しを行います (図 1 参照)。

Figure 1 The Entity Framework Runtime Metadata Is Used to Build Database Commands

図 1 データベース コマンドの構築に使用する Entity Framework ランタイムのメタデータ

実行時に、Entity Framework はデータベース固有の ADO.NET プロバイダーを使用して、モデルに対して作成されたクエリをストア クエリ (T-SQL など) に変換し、データベースに送信します。Entity Framework は、クエリ結果を、厳密に型指定されたエンティティ クラスによって定義されるオブジェクトに変換します (図 2 参照)。

Figure 2 The Entity Framework Executes Queries and Processes Their Results

図 2 Entity Framework でのクエリの実行とクエリ結果の処理

ユーザーがこのようなオブジェクト対して行う操作に応じて、Entity Framework は ID キーを使用して、プロパティやオブジェクト間のリレーションシップへの変更を追跡します。最後に、コードから Entity Framework の SaveChanges メソッドを呼び出してデータベースに変更を保存すると、Entity Framework ランタイムは収集したすべての変更追跡情報を読み取ります。変更、追加、または削除されたエンティティごとに、Entity Framework はモデルを再度読み取り、プロバイダーを使用してストア コマンドを作成してから、作成したコマンドをデータベース上で 1 つの復元可能なトランザクションとして実行します。

このように Entity Framework の既定の動作を説明すると、データベース所有者は悲鳴を上げて逃げ出しそうですが、ここでは "既定" というところを強調しておきます。Entity Framework では、多くの既定の動作が変更可能です。

Entity Framework がデータを取得または保存する要求を処理する方法も変更可能な動作の 1 つです。データ テーブルへのアクセスを許可する、Entity Framework に依存するモデルを構築する必要はありません。データベースのビューとストアド プロシージャだけを認識するモデルを構築できます。このとき、このモデルを使用するアプリケーション コードには影響しません。Entity Framework のストアド プロシージャに対するサポートとデータベース ビューに対するサポートを組み合わせると、あらゆるデータベース操作をストアド プロシージャとビューに基づいて実行できます。

エンティティをテーブルではなくデータベース ビューにマップする

モデルの構築方法はいくつかあります。ここでは、データベースのリバース エンジニアリングによってレガシ データベースから構築するモデルに重点を置いて説明します。Visual Studio には、この目的を果たすウィザードが用意されています。

ウィザードでは、ユーザーがデータベース テーブル、ビュー、およびストアド プロシージャを選択できます。ストアド プロシージャのセクションには、モデルに含めることができるスカラー値関数やユーザー定義関数も一覧されます。

通常、開発者はテーブルを選択し、ウィザードがそのテーブルからエンティティを作成します。先ほど説明した変更追跡と SaveChanges メソッドの処理では、Entity Framework はテーブルに基づいて、エンティティの INSERT コマンド、UPDATE コマンド、および DELETE コマンドを自動的に生成します。

まずは、テーブルではなくビューに対するクエリを Entity Framework に強制的に生成させる方法を見てみましょう。

モデルに含められるデータベース ビューも、エンティティになります。Entity Framework は、テーブルにマップされるエンティティの場合とまったく同様に、データベース ビューにマップされるエンティティへの変更を追跡します。ビューを使用する場合は、ID キーに関して注意が必要です。データベース テーブルには主キーとしてマークされる列が少なくとも 1 つ存在するため、ウィザードは既定でテーブルの主キーからエンティティの ID キーを作成します。主キーのないビューにマップされるエンティティを作成するとき、ウィザードは、テーブルに含まれるすべての null 非許容の値から複合キーを作成して、この ID キーを最大限推測します。ContactID、FirstName、LastName、および TimeStamp という 4 つの null 非許容の列が含まれるビューから作成されるエンティティを考えてみましょう。

生成される 4 つのプロパティが EntityKey としてマークされます (デザイナーでは鍵のアイコンで EntityKey プロパティが示されます)。つまり、エンティティには 4 つのプロパティで構成された EntityKey が 1 つ含まれていることになります。

このエントリを一意に識別するのに必要なプロパティは、ContactID だけです。したがって、モデルの作成完了後に、デザイナーを使用して他の 3 つのプロパティの EntityKey 属性を False に変更し、ContactID だけを EntityKey として残すことができます。

また、可能であれば、事前に計画をたて、適切な null 非許容の列を提供するデータベース ビューを設計することもできます。

適切なキーがあれば、Entity Framework は各エンティティを一意に識別できるので、これらのエンティティの変更追跡を実行してから、SaveChanges メソッドの呼び出し時にデータベースに変更を保存できます。

独自のストアド プロシージャでコマンド生成をオーバーライドする

データベースに保存して戻すには、既定のコマンド生成をオーバーライドして、データベースに変更を保存する際に Entity Framework に独自の Insert、Update、および Delete の各ストアド プロシージャを使用させることができます。これを、"ストアド プロシージャ マッピング" と呼びます。では、ストアド プロシージャ マッピングのしくみについて説明しましょう。

Entity Data Model ウィザードで選択して (または後で更新ウィザードで選択して) モデルに含めたすべてのストアド プロシージャは、モデルの XML メタデータの、データベース スキーマを記述するセクションでは関数になります。ストアド プロシージャは自動的には概念モデルに含められないので、デザイン サーフェイスに表示されません。

データベースの 1 つに含まれる Person テーブルを対象とする、簡単な INSERT ストアド プロシージャを以下に示します。

ALTER procedure [dbo].[InsertPerson]
           @FirstName nchar(50),
           @LastName nchar(50),
           @Title nchar(50)
AS
INSERT INTO [Entity FrameworkWorkshop].[dbo].[Person]
           ([FirstName]
           ,[LastName]
           ,[Title]           )
     VALUES
(@FirstName,@LastName,@Title)
SELECT @@IDENTITY as PersonID

このストアド プロシージャは、データベースの挿入を実行するだけでなく、SQL Server によって新しい行に作成された主キーの値を返します。

このプロシージャをウィザードで選択すると、モデルのデータベース スキーマでは次のように関数として表されます。

<Function Name="InsertPerson" Aggregate="false" BuiltIn="false"   
 NiladicFunction="false" IsComposable="false" 
 ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo">
  <Parameter Name="FirstName" Type="nchar" Mode="In" />
  <Parameter Name="LastName" Type="nchar" Mode="In" />
  <Parameter Name="Title" Type="nchar" Mode="In" />
</Function>

続いて、デザイナーの [マッピングの詳細] ウィンドウを使用して、Person テーブルに基づいて作成された Person エンティティにこの InsertPerson 関数をマップできます (図 3 参照)。

Figure 3 Mapping Stored Procedures to an Entity

図 3 エンティティへのストアド プロシージャのマッピング

図 3 では、PersonID プロパティがストアド プロシージャの戻り値にマップされているのがわかります。このようにマップすることで、データベースで挿入が実行されると、Entity Framework はデータベースから生成されたキーを使ってメモリ内の Person オブジェクトを更新します。

関数のマッピングに不可欠な要件として、関数のすべてのパラメーターがエンティティのプロパティにマップされていなければなりません。数式や値はパラメーターにマップできません。ただし、開発者は、このようなエンティティを表す Microsoft .NET Framework のクラスをカスタマイズするチャンスがたくさんあります。

Update 関数や Delete 関数をマップすることもできます。3 つの操作 (Insert、Update、および Delete) をすべてマップする必要はありませんが、開発者は、一部の関数だけを対象としたマッピングに関するドキュメントに記載されている、いくつかの規則に注意する必要があるでしょう。

図 3 で、[元の値を使用する] と [処理行数パラメーター] という 2 つの列がプロパティの右側にあるのがわかります (列幅のために列名が切り詰められています)。Entity Framework はオプティミスティック同時実行をサポートしているため、この属性を使用して、Update 関数と Delete 関数に同時実行チェックを提供できます。この機能の詳細については、MSDN のドキュメント「チュートリアル: ストアド プロシージャへのエンティティのマッピング (Entity Data Model ツール)」(英語) を参照してください。

実行時に、ユーザーが新しい Person 型を作成していて、SaveChanges メソッドがトリガーされると、Entity Framework は (図 3 で定義したマッピングに基づいて)、メタデータ内の Insert 関数のマッピングを確認します。Entity Framework は、実行中に独自の INSERT コマンドを生成するのではなくストアド プロシージャを実行して、次のコマンドを送信します。

exec [dbo].[InsertPerson] @FirstName=N'Julie',@LastName=N'Lerman',
@Title=N'Ms.'

ギャップを埋めて Entity Framework がテーブルにアクセスしないようにする

Entity Framework は、ビュー ベースのエンティティからデータを保存するコマンドを生成しますが、ビューは更新可能ではない場合もあります。更新可能ではないビューでは、Insert、Update、および Delete の各ストアド プロシージャをエンティティにマップでき、データベース テーブルへの直接アクセスを許可しなくても、データの取得と保存に関する一連の処理を完全に実行できます。

単にテーブルと対応するデータベース ビューを作成し、テーブル列を更新するストアド プロシージャを作成することもできます。また、複雑なビューと、更新を実行する高度なロジックを備えた複雑なストアド プロシージャを作成できます。さらに、開発者がビュー上でクエリ (ストアド プロシージャに対して実行できない操作) を構成できるビューを、読み取りストアド プロシージャの一部の代わりに使用することもできます。

この構成機能の例を次に示します。この例では、アプリケーションから CustomersInPastYear エンティティに対してクエリを要求し、顧客の LastName プロパティを使用してさらにビューをフィルター処理します。

from c in context.CustomersInPastYears
 where c.LastName.StartsWith("B")
 select c;

この結果、次のコマンドがデータベースで実行されます。

SELECT
[Extent1].[CustomerID] AS [CustomerID], [Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], [Extent1].[EmailAddress] AS [EmailAddress], 
[Extent1].[TimeStamp] AS [TimeStamp]
FROM (SELECT 
      [CustomersInPastYear].[CustomerID] AS [CustomerID], 
      [CustomersInPastYear].[FirstName] AS [FirstName], 
      [CustomersInPastYear].[LastName] AS [LastName], 
      [CustomersInPastYear].[EmailAddress] AS [EmailAddress], 
      [CustomersInPastYear].[TimeStamp] AS [TimeStamp]
      FROM [dbo].[CustomersInPastYear] AS [CustomersInPastYear]) AS [Extent1]
WHERE [Extent1].[LastName] LIKE N'B%'

.NET コンパイラは、モデルにマップされているストアド プロシージャ上に構成された同様のクエリを処理できます。しかし、Entity Framework の場合は、データベースでストアド プロシージャを実行し、実行結果をすべてアプリケーションに返してから、ストアド プロシージャから返されたメモリ内のオブジェクトにフィルターを適用します。このため、開発者が気が付かないうちにリソースが無駄になり、パフォーマンスが低下することがあります。

図 4 に、CustomersInPastYear ビューに含まれている列と同じ列を使用して Customer テーブルを更新するストアド プロシージャを示します。このストアド プロシージャは、CustomersInPastYear エンティティの Update 関数として使用できます。

図 4 UpdateCustomerFirstNameLastNameEmail ストアド プロシージャ

ALTER PROCEDURE UpdateCustomerFirstNameLastNameEmail
@FirstName nvarchar(50),
@LastName nvarchar(50),
@Email nvarchar(50),
@CustomerId int,
@TimeStamp timestamp

AS

UPDATE Customer
   SET [FirstName] = @FirstName
      ,[LastName] = @LastName
      ,[EmailAddress] = @Email
 WHERE CustomerID=@CustomerId AND TimeStamp=@TimeStamp
 
 SELECT TimeStamp 
 FROM Customer
 WHERE CustomerID=@CustomerId

これで、このストアド プロシージャをエンティティにマップできるようになります。図 5 に示すマッピングは、元の TimeStamp をストアド プロシージャに送信してから、[結果列のバインド] のマッピングを使用して、ストアド プロシージャから返された更新済みの TimeStamp を取得します。

Figure 5 Mapping a Stored Procedure to an Entity Based on a View

図 5 ビュー ベースのエンティティへのストアド プロシージャのマッピング

結論として、モデルの設計に問題がなく、ビューベースのエンティティに適切な ID キーが設定され、関数が正しくマップされている限り、データ アクセス方針として Entity Framework を使用するアプリケーションにデータベース テーブルを公開する必要はありません。データベース ビューとストアド プロシージャを使用すれば、データベースの正常な操作に必要なあらゆる機能を EDM と Entity Framework に提供できます。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女が執筆した『Programming Entity Framework』(O'Reilly Media、2009 年) は絶賛を浴びました。彼女には Twitter.com (julielerman、英語) から連絡できます。

この記事のレビューに協力してくれた技術スタッフの Noam Ben-AmiSrikanth Mandadi に心より感謝いたします。