EF Core 2.0リリースには批判すべき点が多いが、好ましい部分も少なくはない。本記事では、今回のリリースのハイライトをいくつか紹介する。
テーブル分割
ORMに対する一般的な批判は、効率の悪いデータ要求が実行される傾向のあることだ。ほとんどのORMがデフォルトで“SELECT *”クエリを実行するため、アプリケーションに必要なのがごく小さなサブセットであっても、データベースに対してすべての列が要求されることになる。
これまでのEFでは、これに対処する方法として、生成されるSQLを必要な列のみに限定する.Select句を使用していた。しかしこれには、サードパーティ製のマッピングライブラリ(AutoMapperのような)か、あるいは投影されたオブジェクト毎に必要な列を明示的にリストする必要がある。後者は面倒でエラーも起こしやすいので、先を急ぐ場合にはこのステップを省略することも多い。その結果、データベースがカバリングインデックス(covering indexes)を利用できないため、パフォーマンスの乏しいクエリが実行されることになる。
もうひとつの問題は、投影されたオブジェクトがCRUD操作に参加できないことだ。エンティティにマップするか、SQLコールを直接使用する必要がある。
EF Coreで範囲の狭いクエリを生成するためのよい方法は、“テーブル分割(table splitting)”を使用することだ。この方法では、複数のクラスを同じテーブルにマップする。EF Coreでこれを使うためには、“(外部キーが主キーを形成する)識別関係が、テーブルを共有するすべてのエンティティ間で設定されている”ことが必要だ。
グローバルフィルタ
期待通りのグローバルフィルタを生成できないという、EFにあった機能面での大きな欠如は、EF Coreでは解決されている。
グローバルフィルタを使用すると、特定のテーブルにヒットするすべてのクエリにフィルタを適用することができる。これが使用されるのは、主としてソフトデリートのシナリオである。データベースからの物理的な削除を行なわずに、削除フラグの付けられた行を返さないようにすることが可能になる。グローバルフィルタは新しい概念ではなく、NHibernateやToruga Chainなど、他のORMにも見られる概念だ。
EF Coreでは、OnModelCreatingイベント内で次のようなコードを使って実装する。
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
この構文には、isDeleteフラグをサポートするすべてのテーブルについて、同じことを繰り返さなければならないという問題がある。テーブルの数が多い場合はエラーの原因ともなるが、クエリ毎に忘れずにチェックするという方法に比べれば、はるかに優れている。
グローバルフィルタは、DbContextオブジェクトで宣言されたフィールドにアクセスすることができる。これはつまり、マルチテナンシなどの高度なシナリオでも利用可能である、ということだ。コンテキストプールでこれを行なう場合は、細心の注意が必要である(下記参照)。
ドキュメントには2つの制限が記されている。
- ナビゲーション参照は許可されない。この機能は、開発者からのフィードバックによって追加される可能性がある。
- フィルタが定義可能なのは、階層のルートエンティティタイプ上に限られる。
コンテキストプール
データベースの接続に比べればはるかに小さいが、DbContextオブジェクトの生成においてもパフォーマンス低下の可能性がある。理想的なソリューションはDbContextをスレッドセーフにすることだが、EFの設計上それは不可能だ。
ASP.NET Coreアプリケーションでこの問題を軽減するため、EF CoreではDbContextプールが提供されるようになった。これにはASP.NET Coreの依存性注入フレームワークが使用されている。
ドキュメントには注意点として、マルチテナンシ用のグローバルフィルタと併用できないことが記されている。
DbContext派生クラスで保持する独自の状態(プライベートフィールドなど)をリクエスト間で共有したくない場合は、DbContextプールを使用しないでください。EF CoreはDbContextインスタンスをプールに追加する前に、自身が認識している状態のみをリセットするからです。
これはDIフレームワークからコールされる“reset”メソッドあるいはイベントを用意すれば簡単に解決する問題だが、将来のバージョンでは修正されることを望みたい。
スカラ関数
リレーショナルデータベースサーバの大きな特徴は、データの存在する場所でコードを実行できることだ。分別を持って利用すれば、処理前にアプリケーションにデータを転送する場合に比べて、大幅なパフォーマンス改善が実現できる。
EF Core 2はこれを、スカラ関数を公開することでサポートする。スカラ関数は、DbFunction属性でタグ付けされた空の静的関数を使って、EFモデル内に定義されている。LINQプロバイダがこれらの利用を検出して、サーバ上の対応する関数を使用するSQLを生成する仕組みだ。
現在のEF Coreでは、スカラ関数のみをこの方法でサポートしている。これよりも強力なテーブル値関数を使うには、現時点では生のSQLを使用する以外に手段はない。
SQLメソッドにおける文字列補間
非常に興味深い特徴のひとつが、EF Coreにおける文字列補間の動作方法だ。単純にString.Formatコールに変換するのではなく、EF CoreではパラメタライズドSQL文字列を生成する。SQLインジェクション攻撃を回避する上で、このことは非常に重要だ。
var city = "Redmond";
context.Customers.FromSql($"SELECT * FROM Customers WHERE City = {city}");
SELECT * FROM Customers WHERE City = @p0
今後はさまざまなマイクロORMが、このテクニックを採用するものと期待される。
明示的にコンパイルされたクエリ
クエリはデリゲート(すなわち関数ポインタ)の形でキャッシュされるようになった。通常これは、匿名メソッドによって定義される。パフォーマンス向上に加えて、複数の場所でクエリを参照する上でも都合のよい方法だ。デリゲートはDbContextオブジェクトをパラメータとして受け取るので、マルチテナントのトランザクション内で使用することができる。
キャッシングは自動的に実行される場合もあるため、明示的なコンパイルによるパフォーマンス向上は限定的である点に注意が必要だ。
通常EF Coreは、クエリを自動的にコンパイルした上で、クエリ式のハッシュ表現に基づいてキャッシュしますが、このメカニズムでは、ハッシュ計算とキャッシュ検索をバイパスして、デリゲートの呼び出しを通じてコンパイル済みクエリを使用することにより、わずかですがパフォーマンスを向上することが可能になります。
これらの新機能は無償ではない。EF Coreシリーズのパート3では、EF Core 2.0の非互換変更について取り上げる。
この記事を評価
- 編集者評
- 編集長アクション