Entity Frameworkを使ったプログラマの末路・・・

はじめに

最近取り組んでいたEntity Framework Coreを使っているプロジェクトがひと段落したので、今後EFを使おうか検討している人向けに1つの事例として残しておこうと思います。

結論としてはキャッチーなタイトルとは裏腹に、今回のプロジェクトではメリットがデメリットを上回ったケースとなりました。

プロジェクトの概要

もともとはOracleに格納していたデータをPostgreSQLに移行に移行する際に、そのデータアクセス部分を書き直した感じになります。 そこのデータアクセスにEFを採用したイメージです。

採用した技術スタックはざっくりと以下の感じです。

また、データベースの規模感は以下の通りで、そんなに大きくはありません。 データも多いテーブルでも100万行程度でした。

  • テーブル:15程度
  • カラム:200程度

採用に至った経緯とか

概要でも書いていますが、今回のプロジェクトのデータベースは既存データベースの移植になります。

最終的には上の通りのデータベース規模になりましたが、オリジナルはテーブルは倍の30テーブル、カラムに至っては1000を超えるご機嫌な仕様となっていました。 また、期間もあまり長くなく手作業でクエリとDapperからデータを受けるクラスを書いていると間に合わなくなる可能性がありました。

そこで、オリジナルであるOracleのデータベースのテーブル定義からPostgreSQLのテーブル・ビューのDDLC#側のクラス定義を生成し、クエリはEFを使用して錬成するというアプローチを取るようにしたのが採用の経緯です。

もちろんEFなりO/Rマッパーなりの懸念はありましたが、克服できる or 影響を無視できるという算段のあっての採用です。

  • 複雑な操作をするとすぐに異形なSQLが錬成される
    • 予備調査でプロジェクトで使用するクエリのほとんどが非常に単純な操作で済むという見立てがあった
    • 少しでも複雑な処理はすべて生のSQLを書く覚悟を持つ
  • EFを使うと遅くなる
    • そもそもそこまで処理時間がクリティカルに効いてくる処理ではない
    • フルスキャンが走ってないかなどはデータベースの統計情報を確認してヤバそうなクエリは早めに何とかする

また、今回は新規でコードベースを起こすプロジェクトだったため、厄介なN+1問題を回避するためにLazy Loadingは全面的に禁止にしました。

得られた知見とか

DbContextは投げ捨てるもの

ネット上のサンプルなどは分かりやすさのために単一のDbContextインスタンスを使用して操作する例が多いですが、基本的にはDbContextは使い捨てたほうがいいです。

DbContextは変更のトラッキングのために内部にクエリ結果のキャッシュを保持します。そのため、一度結果セットを取得してしまうとソート順を変えて再取得しようとしても前回の結果をそのまま返してきます。 キャッシュの破棄なども出来るみたいですが、そんなことをするくらいなら処理毎にDbContextを作り直したほうが変な事故を起こしずらいです。(1敗)

セッションなどの単位でコネクションやトランザクションを生成して、そのコネクションを使用してEFのDbContextを生成・使用するイメージですね。 そのため、個人的にはEFを使うならIoCコンテナはほぼ必須だと思います。

EF(O/Rマッパ)はSQLが書けない人のためのフレームワークではない

この辺なんかはちょっと調べると死ぬほど記事が出てきますが、まさにその通りだと思います。

あくまでEFはC#LINQにのっとってクエリを生成しているだけで、生成されたクエリそのものの品質はプログラマが担保しなくてはいけません。 かつ、EFは割とカジュアルに異形クエリを錬成してくださりやがるのでカジュアルに死ねます。

そのため、生成されたクエリがインデックスを使用するかなどの『好ましい』クエリか否かを判断できるくらいのスキルは必要かもしれません。

EFでは基本的に頑張らない

上で書いている内容とだいぶ重複しますが、EFでは頑張れば頑張るほど異形クエリを吐き出してくれるため、基本的に単純なCRUDにとどめる勇気が必要です。

そのうえで、ちょっと凝ったことをしたいのであれば躊躇せずに生SQLを書いたほうが後々幸せになれるかもしれません。

もちろん、全部LINQ経由で書けば固有の製品に依存しないコードが書けるという意見もあるかと思います。 じゃあプログラムさえ対応させれば明日そのOracleをPostgresに変換出来んの?と聞かれた場合、『Yes』と答えられる環境は多くは無いでしょう。*1

ウェッブ系ならともかく生産性アプリケーションの場合はプログラムよりもデータ(ベース)のほうが寿命が長いんだからそんなこと考えてもしょうがなくない?というお気持ちです。

生成されたSQLの確認は必須

前提としてEF側のクエリログの設定は必須です。じゃないと生成されたSQLの確認が出来ませんし、本番に乗っかってからパフォーマンスに問題が出るとつらみポイントが高いです。

ログに吐かれたSQLexplain analyzeをくっつけて実行計画を見ておくのは無駄ではないと思います。

また、PostgreSQL側のpg_statio_*系のビューを見て、フルスキャンが走っていないかも確認しておくとより確実です。

まとめ

別にEFの機能を全部使いこなさないといけないというわけではないので、自分の欲しい機能を自分のコントロールできる範囲でつまみ食いする程度でも別に問題ないと思います。

あと、生成されたSQLは絶対に確認しましよう

*1:というかOracleユーザDBLINKとかMVIEWとか節操なく使いすぎでしょ