Entity Framework Coreについて認知されている多くの欠陥を修正するため、Microsoftは、EF Core 3に40箇所の重大な変更を導入した。変更の一覧はMicrosoft Docsで見ることができるが、注目すべき部分を以下に挙げる。
クライアント側でのクエリ実行
EF CoreのSQLジェネレータの制限を回避するため、デフォルトでは、クライアント側で部分的にクエリを実行するように設定されていた。これはつまり、LINQクエリをSQLに変換できないということになる — データベースからテーブルをダウンロードして、残りの操作は単純にメモリ内で実行されるのだ。バージョン2.1までは、GROUP BYさえもクライアント側で実行されていた。
この動作の欠点は、Where()
句のたったひとつの問題によって、EF Coreが、文字通りテーブル全体をダウンロードする可能性があることだ。相関サブクエリを生成できない場合、代わりに数百から数千というセカンダリクエリが実行される、という開発者からの報告もある。
新たなデフォルトでは、クライアント側で実行される可能性があるのは、最終的なSelect()
操作のみになった。EF Coreが正しいSQLを生成できなければ、代わりに例外がスローされる。この動作はオーバーライド可能だが、Microsoftとしては、バグリクエストの送信を希望している。
この変更の詳細は、"Outline guidingprinciples and decide on direction for queries in 3.0"というチケットで確認が可能だ。
SQLのパラメータ化と補間
我々が2017年に報告したように、EF Coreの文字列補間機能には数多くの懸念があった。この機能を使用すると、補間された文字列を自動的にパラメータ化されたSQLに変換できるのだが、それが可能なのは、最初に一時変数に保存されていない場合に限定されていた。
v1 = context.Customers.FromSql($"SELECT * FROM Customers WHERE City = {city}");
var sql = $"SELECT * FROM Customers WHERE City = {city}";
v2 = context.Customers.FromSql(sql);
上記の例では、v1は正しくパラメータ化されるが、v2にはSQLインジェクションの脆弱性がある。
このつまずきを回避するため、FromSql
関数が削除されてFromSqlRaw
とFromSqlInterpolated
に置き換えられ、開発者の意図をより明確にできるようになった。
一時キー
新たなエンティティを追跡する目的で、EF Coreでは多くの場合、一時的なプライマリキーが作成される。このようなキーは負の数として、通常のキープロパティ(CustomerKeyやOrderIdなど)に保存される。理屈としては、実際のキーが生成された時に置き換えるのだが、残念ながら、UIに表示されたり、データベースに保存されたりするなど、ネガティブな副作用がいくつか存在していた。
EF Core 3では、この追跡情報をエンティティの追跡情報に移動して、キープロパティにはクリーンなデータのみを残すようになった。
カスケード削除のタイミング
context.Remove()
などのメソッドがコールされると、カスケード削除がすぐに発生する。これまでのEF Coreでは、SaveChanges
が呼び出されるまで削除される子レコードを計算しなかったため、何が起こるかを正確に予測することは困難だった。今回の変更が主に影響するのは、どのレコードが変更/削除されようとしているのかをログ記録するコードである。
CascadeDeleteTiming
およびDeleteOrphansTiming
オプションをCascadeTiming.OnSaveChanges
に設定すれば、以前の動作を復元することができる。
クエリタイプの廃止
Entity Frameworkの従来バージョンとは異なり、EF Coreは、主キーを公開するテーブルでのみ動作するように設計されている。しかしながら、ビューやストアドプロシージャの実行結果には主キーがないため、この点が問題になる。そのためEF Core 2.1で、クエリタイプ(query type)という概念が導入された。
基本的にクエリタイプでは、並列オブジェクトモデルを使用する。定義にはDbSet<T>
の代わりに、DbQuery<T>
を使用する。ModelBuilder.Entity<>()
ではなく、ModelBuilder.Query<>()
を使って登録し、DbContext.Set<>()
ではなくDbContext.Query<>()
を使用して呼び出しを行う。
この2つの不要な混乱に、多くの開発者が不満を示したため、これらはいずれも削除されることになった。EF Core 3以降は、すべてのデータソースにおいて通常のDbSetモデルの使用が推奨されている。主キーのない場合は、エンティティ登録時に、.HasNoKey
をアノテーションとして付けるだけでよい。
プロパティゲッタとセッタの無視
これまでのEF Coreでは、クエリの結果をマテリアライズ(materialize)する場合を除き、プロパティゲッタまたはセッタを呼び出していたが、クエリにおいて下位のフィールドが既知の場合には、直接フィールドに書き込まれるようになった。
この変更により、EF Coreがインタラクションする理由に関わらず、基盤となるバッキングフィールドが既知の場合は、常にそれが使用されるようになる。メリットは、ビジネスロジックが誤ってトリガされることの防止だ。
逆にデメリットは、計算フィールドの更新などのビジネスロジックが起動されないことだ。そのため、場合によってはUsePropertyAccessMode
の設定を変更して、所定の動作を行うように設定しておく必要がある。
バッキングフィールド検出
上記の変更においてバッキングフィールドが検出されると、コードの動作があいまいになる場合がある。従来のEF Coreでは、内部的なランキングシステムに基づいて、どのフィールドを設定すべきかを推測していた。
EF Core 3では、このようなあいまいさに対して、例外がスローされるようになった。その場合は、開発者がモデルビルダで使用するフィールドを指定する必要がある。
ValueTaskによるTaskの置き換え
Taskをオブジェクトにしたことは、.NETの最大の間違いのひとつと考えられていた。長時間実行されるタスクでは許容されるが、短命なタスクが多数生成されると、多くの場合、過度のメモリ負荷が発生するのだ。そのため、構造ベースの代替として、ValueTask
が導入された。
この新しい型をサポートするために、FindAsyncやNextValueAsyncなどいくつかのメソッドが、Task
ではなく、ValueTask
を返すようにアップデートされている。単に結果を待つだけのコードには影響しないが、taskで何かを実行する場合には、AsTask()
をコールして、ValueTask
からTask
への変換が必要な可能性がある。
IEntityTypeおよびIPropertyの簡略化
これらのインターフェイスから5つのメソッドが削除され、拡張メソッドに置き換えられた。この変更を正当化する理由は、インターフェイスが小さくなって実装が容易になる、ということだ。