すでに我々はモニカなのかもしれない。

EF Core自体の話

いいところ

  • コードファーストで記述できる
  • CLR型でデータを取り扱うことができ、データ型変換の煩わしさがない
  • カラム名の不一致などをコードベースで検査でき、ポカミスを避けられる
    • SQLのクエリを意識することなくLinq式でクエリを発行できる
    • Linq式で済むような内容であればとても簡単に記述できる
    • 実際そんなわけはなく、どういうSQLのクエリに変換されるか考えながらコードを書いている
    • Linq式が一般的なIterableの関数名と異なる点もよくない
  • DataModelやConfigurationを記述するとマイグレーションファイルを作成してくれる
    • 何回もマイグレーションをしていると凄まじい行数に膨れ上がるのである程度のところで統合が必要

だめなところ

  • JOINの性能は終わっている
    • 1×Nや1×N×MのJOINが発生するときはクエリを分割しないと馬鹿正直にクエリを発行してデータ効率が悪い
    • LeftJoin()は用意されていないが、後述の方法で実現できる。LinqKitを入れることもできる。EF Core 10.0で正式導入された
    • Join()後のSelect()でオブジェクトを平坦化すると列挙するオブジェクトの数に比例してコードの見通しが悪くなる
    • EF Core 5.0からは QuerySplittingBehaviorでデカルト積になることを防ぐことができる
  • ウインドウ関数が発行できない
    • 外部ライブラリを導入することで発行できる?
      • 8.0では解決されている?わからない
    • この時点で結構複雑になっていることが多いので、諦めてDapperを使ったほうがいいかもしれない
  • 変換できない組み込み関数が多い
    • コンパイル時にはわからない
    • 実行したときにはじめてInvalidOperationExceptionでわかる
    • 思ったよりもSQLに変換できないコードが多く、どうして変換できないのかにらめっこが始まる
  • タプルに対するIN句が発行できない
    • キーがふたつ以上のときに、タプル等を作ってもContains()で抽出できない
    • key1 + "-" + key2 のように新しく文字列を作ってマッチさせる方法もあるが、インデックスが効かないので、レコード数が増えるにつれて通用しなくなる
    • Expressionを使ったメソッド拡張が必要になる
  • トラッキングのために何回もクエリが発行される
    • 読み取り専用コンテキストであれば、AsNoTracking()を使うことを検討する
    • ログ出力も性能に響いてくるので、不要であればオプションで抑制する

ハマるポイント

  • 基本的にIQueryableをIEnumerableに変換してはいけない
    • IQueryableはデータベース上で実行されるクエリで、IEnumerableはクライアントのメモリ上で操作している
    • あちこちの開発者コミュニティでクエリをAsEnumerable()するとうまくいくことが書かれているが、それは間違いである
    • 安易にIEnumerableに変換すると期待した結果が得られないことがあるので要注意
  • LEFT JOIN (左外部結合)は用意されていない
    • GroupJoin()SelectMany()組み合わせで表現する
    • 右辺が存在しなかった場合、右辺自体がnullになるのではなく、右辺内のオブジェクトがすべてdefaultになる
      • nullableなオブジェクトであればnull, そうでないなら0相当の値になる
      • LEFT JOINのもともとの挙動からすれば妥当ではあるが、プリミティブ型はあまり納得いかない結果となる
  • AND条件とOR条件
    • 複数のWhere()を使うことでAND条件を表現できる
    • Where()の中で &&|| を使うことでもAND条件やOR条件を表現できる
    • 動的にOR条件を指定したいときは、Expressionが必要
      • 情報が少ないので厳しい
  • ソート方法
    • OrderBy()OrderByDescending()に続けてソート条件を付け加えるには、ThenBy()ThenByDescending()を使う。最初と2番目以降では使うメソッドが変わる
    • Expressionを使うとOrderOrThenBy()のような、最初でも2番目以降でも使えるメソッドを作り出すことが可能
      • むしろ作らないとコード量ばかり増えて読みづらい

IEnumerable<T>について

インタフェースとしての機能

  • ほぼすべての集合クラスのインタフェースである
  • IEnumerable<T>で受けることはコード上データを扱いやすくなるものの、最低限保証しなければならない性質を見極めること
    • IEnumerable<T>: 列挙可能
    • ICollection<T>: 項目の追加と削除
    • IList<T>: 項目の追加と削除と順序性
    • IReadOnlyList<T>: 項目の順序性
    • ISet<T>: 項目の追加と削除と一意性
    • IReadOnlySet<T>: 項目の一意性
    • IDictionary<T>: Key-Valueペアの追加と削除
    • IReadOnlyDictionary<T>: Key-Valueペアの保持

列挙可能(イテレータ)としての機能

  • IEnumerable<T>のメソッドは遅延評価される
    • Guidを生成する等で、最初の結果と2回目の結果が変わるようなことが起きる
    • 一度しか評価して欲しくなければ都度 ToList() を使う
    • 値を評価(確定)するコストも発生するので、なんでもかんでも ToList() するべきではない
  • Append()Concat()Union() は列挙をチェインした結果を返す
    • その結果を使わないと意味がない
  • 列挙する値を変更するは主に次の2通り
    1. Select() する
    2. ToList() してから該当添字の要素を変更する
  • 各要素の型 TTResult と互換性がある場合には、Cast<TResult>() で変換できる

マイグレーションファイルの蓄積

マイグレーションファイルは現在のデータベースのすべてのスナップショットとこれまでの差分を保持している。これは定期的にマージしないと静的解析に影響を及ぼすので、ある程度の単位ごとに整理したほうがいい。

実際に私の環境ではdotnetプロセスが20GB以上のメモリを消費し、VS Codeプロセスを終了しても依然ゾンビとして残り続けたので、この影響は大きいものと考えられるが、Stackoverflow等を見ても見つけられなかった答えなので、もしVS Codeの静的解析が遅いということがあればお試しあれ。