前言
長い間ブログを書いていませんでしたが、今日は久しぶりに一記事補足します。たまにブログを書くのが億劫になることもありますが、時間がかかるので、それでも続けています。もしかしたら書いた内容が誰かの役に立つかもしれないので。では本題に入ります。EF 6.x でも EF Core でも、生のクエリ(Raw Query)に対する API はかなり貧弱です。たとえば単一の値だけを取得したい場合や、一部のカラムだけを取得したい場合など、それらはサポートされていません。数えきれないほどの制限があります。唯一サポートされているのは、テーブルの全カラム、つまりクラスの全フィールドを返すことだけです。そのため、ほとんどの場合、私はネイティブ SQL を書いています。生のクエリはほとんど使ったことがありません。最近、EF に熱心な同僚から、SqlQuery を使って動的クエリを実現する方法を質問されましたが、答えは持っていませんでした。この方法を使おうとも思ったことがなかったからです。個人的に調べてみて、少しだけ考えをまとめてみました。お役に立てれば幸いです。役に立たなくても、私がブログを一つ補足したと思ってください。
EF 6.x と EF Core で動的クエリを実現
public static IEnumerable<dynamic> SqlQueryDynamic(this DbContext db, string Sql, params SqlParameter[] parameters)
{
using (var cmd = db.Database.Connection.CreateCommand())
{
cmd.CommandText = Sql;
if (cmd.Connection.State != ConnectionState.Open)
{
cmd.Connection.Open();
}
foreach (var p in parameters)
{
var dbParameter = cmd.CreateParameter();
dbParameter.DbType = p.DbType;
dbParameter.ParameterName = p.ParameterName;
dbParameter.Value = p.Value;
cmd.Parameters.Add(dbParameter);
}
using (var dataReader = cmd.ExecuteReader())
{
while (dataReader.Read())
{
var row = new ExpandoObject() as IDictionary<string, object>;
for (var fieldCount = 0; fieldCount < dataReader.FieldCount; fieldCount++)
{
row.Add(dataReader.GetName(fieldCount), dataReader[fieldCount]);
}
yield return row;
}
}
}
}
上記のように動的コレクションを取得した後、それをどのようにオブジェクトのコレクションに変換するのでしょうか?私は迷わずまずシリアライズしてからデシリアライズする方法を取りました。もしより良い解決策をお持ちの方は、ご自身で実装してください。
using (var ctx = new EfDbContext())
{
ctx.Database.Log = Console.WriteLine;
var dynamicOrders = ctx.SqlQueryDynamic("select * from dbo.Orders");
var ordersJson = JsonConvert.SerializeObject(dynamicOrders);
var orders = JsonConvert.DeserializeObject<List<Order>>(ordersJson);
};

もちろん、上記は単純に1つのテーブルをクエリしただけです。複数のテーブルでも同様に動作します。最終的に異なるオブジェクトにデシリアライズすれば問題ありません。未テストですが、ご自身でお試しください。
EF Core で複数のコンテキストインスタンスプールを使用する
多くの開発者は、EF 6.x でも EF Core でも常に1つのコンテキストしか使用していませんが、複数のコンテキストを使用することを考えたことはあるでしょうか?例えば、ECサイトのプロジェクトで、製品関連の操作には製品コンテキスト、カートに入れる操作にはカートコンテキスト、注文操作には注文コンテキストを使用する、といった具合です。これの利点は何でしょうか?データベーステーブル、つまりエンティティを異なるビジネスに分割できることです。今までこのようなアプローチを見たことはありませんが、もし私だったら少なくともこうするでしょう。
//Add DbContext
var dbConnetionString = Configuration.GetConnectionString("DbConnection");
services.AddDbContextPool<ShopCartDbContext>(options =>
{
options.UseSqlServer(dbConnetionString);
}).AddDbContextPool<BookDbContext>(options =>
{
options.UseSqlServer(dbConnetionString);
}).AddDbContextPool<OrderDbContext>(options =>
{
options.UseSqlServer(dbConnetionString);
});
EF Core 2.0 ではコンテキストインスタンスプールが導入されました。これは ADO.NET のコネクションプールに似ていますが、表面的に理解すると大きな誤解を招きます。コンテキストインスタンスプールの本質(昨年から EF 6.x と EF Core に関する本を執筆しており、近日出版予定です)については、ADO.NET のコネクションプールとは異なるものです。では、上記のように複数のコンテキストインスタンスプールを登録することは常に有効なのでしょうか?残念ながら、この設定は誤りです。プログラムを実行すると、以下のような例外が発生します。
Exception message:
System.ArgumentException: Expression of type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MultiContext.Contexts.BContext]' cannot be used for constructor parameter of type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MultiContext.Contexts.AContext]' Parameter name: arguments[0]
Stack trace:
...........
この機能が出た当初は、パフォーマンスが向上するとみんな歓声を上げましたが、コンテキストインスタンスプールはある程度パフォーマンスを向上させるかもしれませんが、あくまで「可能性がある」という範囲にとどまります。もし EF Core がコンテキストインスタンスプールを実装している原理を知っているか、見たことがあれば、その本質を理解し、「可能性のあるパフォーマンス改善」という意味が腑に落ちるでしょう。なぜ複数のコンテキストインスタンスプールを登録できないのかについては、実際にプロジェクトで遭遇しました。詳細は GitHub の Issue を参照してください。
https://github.com/aspnet/EntityFrameworkCore/issues/9433。
まとめ
今日はここまでです。過剰な説明や叙述はなく、すぐに本題に入りました。最近は思考が飛びがちで、ブログを書くことへの興味が薄れつつあります。たまに感傷的になりますが、また元気になって気分を整えたら、引き続き技術を共有していきます。私は常にここにいます。しばらくブログを書いていないのは、疲れているか、あるいは個人的に IdentityServer や他の技術を学んでいるからかもしれません。この業界にいる限り、転職しない限りは、素直に経験を積み、多くの技術を学ぶべきです。若いうちに奮闘しなければ、いつ奮闘するのでしょうか。今日は何を言っているのか、考えがまとまらず、お許しください。
あなたが見ているものは物事そのものではなく、解釈によって与えられた意味である。