C#: EFCoreで動的にテーブル名・キー・列名等を取得

概要

  • xUnit等でデータベース(EFCore)を使ったテストケースを作成する際、エンティティやテーブル定義に基づいてテストデータを作成したい場合があります。そのようなユースケースをサポートするためのサンプルを紹介します。
  • 使用環境は次の通りです。
    OSWindows 10(64ビット)
    IDEMicrosoft Visual Studio Community 2022(17.6.0)
    言語C#(10.0) + .NET6
    パッケージMicrosoft.EntityFrameworkCore (7.0.10)
    Microsoft.EntityFrameworkCore.Design (7.0.10)
    Microsoft.EntityFrameworkCore.SqlServer (7.0.10)
    データベースSQL Server 2022 Developer (16.0)
  • 完全なソースコードはgithubで公開しています。
    • サンプルコードの全てはEfCoreExampleTests.csに含まれています。想定しているテーブル定義はddl_dml.sqlで定義されています。
    • サンプルのコードは全て「null 許容未指定」にしており、これは.csprojファイルのNullableディレクティブで設定しています。お使いの環境でサンプルを使用するとnull許容関連の警告が出力される場合がありますが、適宜変更してください。
    • ここで掲載しているサンプルコードは、可読性を高めるために一部コメントを消しています。

基本サンプル

テーブル名、カラム名、型の一覧の取得

  • MEmployeeに対応するテーブル名・カラム名、カラム型、NULL許容をダンプするサンプルです。
    • EFCoreでは、テーブル定義の情報はXXXDBContext.OnModelCreating()で実装します。これらの情報は、XXXDBContext.Modelから取得できます。
    • エンティティ・テーブルの情報はIEntityTypeで表現されます。
      XXXDBContext.ModelのFindEntityType()GetEntityTypes()を使って、特定のエンティティや全てのエンティティ・テーブルのIEntityTypeを取得できます。
    • プロパティ・カラムの情報はIPropertyで表現されます。
      IEntityTypeのGetProperties()GetProperties(string)を使って、特定プロパティや全てのプロパティのIPropertyを取得できます。
    • カラム名の取得方法に関して、EFCore6のGetColumnName()は非推奨(Obsolete)になっていますが、EFCore7のGetColumnName()は非推奨になっていません。
      EFCore6でカラム名を取得する場合、GetColumnName(StoreObjectIdentifier.Table(tableName)等のように取得できます。
  • 実行結果は次の通りです。

主キーの取得

  • TSalesエンティティ・テーブルの主キープロパティ名・カラム名を出力するサンプルです。
    • 主キーの情報はIKeyで表現され、IEntityType.FindPrimaryKey()を実行して取得できます。
    • エンティティのプロパティ(IProperty)毎に主キーかどうかを判定したい場合、IPropertyのIsPrimaryKey()を使用します。
    • レアケースだと思いますが、主キーが設定されていないテーブルの場合、FindPrimaryKey()がnullを返却することに注意が必要です。
  • 実行結果は次の通りです。

外部キーの取得

  • MOrderDetailの各カラムの外部キー参照先を出力するサンプルです。
    • 外部キーの情報はIForeignKeyで表現されます。
    • エンティティ・テーブルに設定された全ての外部キー情報や特定プロパティの外部キー情報を取得したい場合、IEntityType.GetForeignKeys()を使用します。
    • 参照元となるプロパティ・カラム情報の一覧はIForeignKey.Propertiesプロパティから取得できます。参照先となるエンティティは同様にIEntityTypeで表現され、IForeignKey.PrincipalEntityTypeプロパティから取得できます。(参照先となるエンティティ・キーはPrincipalXXXというネーミングのプロパティです。)
    • これらの情報の取得方法は、EFCoreソースコードIReadOnlyForeignKey.csのToDebugString()を参考にしています。
  • 実行結果は次の通りです。
    サンプルのMOrderDetailテーブルでは、order_id列からm_orderテーブルのorder_id列に外部キーを設定しています。また、product_type, product_id列からm_productテーブルのtype, id列に外部キーを設定しています。

応用サンプル

全テーブルデータの削除

  • データベース上の全てのテーブルからデータを削除するサンプルです。
    • データベース上の全てのテーブルに対してtruncate文を実行します。外部キーの参照先テーブルはtruncateできないので、deleteします。
    • 単体テスト実行時、最新のエンティティに対応するDB環境を準備するために、DbContext.DatabaseのEnsureCreated(), EnsureDeleted()を使って都度DBを再構築する方法が考えられます。都度のデータベースの再作成はコストが高いので、2回目以降はこのようなユーティリティメソッドを使ってデータベースを初期状態に戻す使い方を想定しています。
  • 実行結果の例は次の通りです。
  • なお、外部キーの参照先テーブルをtruncateすると次のエラー(MSG: 4712)になります。
    SqlException : FOREIGN KEY 制約でテーブル 'テーブル名' が参照されているので、このテーブルは切り捨てられません。(Cannot truncate table 'table-name' because it is being referenced by a FOREIGN KEY constraint.)

既定値ありカラムをnullに更新

  • 既定値が設定されたカラムをnullに更新するサンプルです。
    • 既定値の設定があるプロパティ・カラムでnull値のテストを行いたい場合に使用する想定です。
    • 既定値が設定された項目にnullを設定する場合、データ登録後にUPDATE文でnullに更新する必要があります。そのUPDATE文を自動的に生成するサンプルです。
    • “update XXX set c1={0}, c2={0}, … where k1={1} and k2={2}, …”等のように、エンティティに設定されたキー値を条件に、該当項目をnullに更新するクエリとなります。
    • CreateNullUpdateSql()では、既定値が設定されており、その値がnullのプロパティ・カラムをnull更新対象としたSET句を生成します。エンティティの主キーとなっているプロパティからwhere句を生成します。
    • SaveChangesAsync()を実行すると、既定値がエンティティに反映されてしまうので、どのプロパティをnullにするのか判別が困難になります。そのため、CreateNullUpdateSql()の呼び出しはSaveChangesAsync()前にしています。
  • 実行結果の例は次の通りです。

INSERT可能なエンティティの自動生成

  • エンティティ・テーブル定義に基づいてINSERT可能なEntityを自動的に生成するサンプルです。
    • NOT NULLカラムが多数あるようなテーブル用のテストデータを作成するのは結構な手間がかかります。業務システムだと数百のカラムがある(設計が良くない)テーブルもあり、手動で値を設定するのは現実的ではありません。
    • このような問題を解決するためのユーティリティメソッドです。このメソッドで生成したエンティティを雛形として、テストに必要なカラムに値を設定する使い方を想定しています。
  • 実行結果の例は次の通りです。

実行対象テーブルの動的な変更

  • 動的にDBContextのテーブル(DBSet)を切り替えてレコード件数を表示するサンプルです。