C# + Dapper.FluentMapで、DBのsnake_caseなテーブルをPascalCaseなクラスにマッピングする

前回の終わりの方で、PostgreSQLのテーブル名・カラム名に大文字が含まれている場合はダブルクオーテーションで囲んでいます。

Dapperで毎回SQLを書くときにダブルクオーテーションを付けるのは手間なので、なにかよい方法が探してみたところ、CustomPropertyTypeMapを作ってSqlMapper.SetTypeMap()を呼べば、テーブルとクラス間でマッピングしてくれそうでした。

 
それでもちょっと手間なので、そこらへんをうまいことやるライブラリがないかを探してみました。

すると、Dapper.FluentMapというライブラリがNuGetにあったため、試してみました。
henkmollema/Dapper-FluentMap · GitHub

 

環境

  • Windows7 x64
  • .NET Framework 4.5
  • C# のコンソールアプリ
  • NuGet パッケージ
    • Dapper 1.38
    • Dapper.FluentMap 1.3.3
    • FluentMigrator 1.3.1.0
    • FluentMigrator.Tools 1.3.1.0
    • System.Data.SQLite.Core 1.0.94.0

なお、データベースは手軽に使えるSQLite、その中でもEntityFrameworkに依存していないSystem.Data.SQLite.Coreを使うことにしました。

 

事前準備

  • NuGetより、上記のライブラリをインストール
  • コンソールアプリ中でマイグレーションツールのFluentMigratorを使うため、以下を「リンクとして追加」する(ファイルが存在する場所は前回の記事を参照)
    • Migrator.exe
    • FluentMigrator.Runner.dll
  • snake_caseのテーブル名とカラム名を持つ、マイグレーションファイルを用意
  • PascalCaseなクラスを用意

なお、マイグレーションファイルとクラスは以下のような内容です。

 

snake_caseのテーブル名とカラム名を持つ、マイグレーションファイル
[Migration(1)]
public class Migration_Initialization : AutoReversingMigration
{
    public override void Up()
    {
        Create.Table("user_table")
            .WithColumn("user_id").AsInt32().PrimaryKey()
            .WithColumn("user_name").AsString();

        Insert.IntoTable("user_table")
            .Row(new { user_id = 1, user_name = "hoge"} )
            .Row(new { user_id = 2, user_name = "fuga"} );
    }
}

 

PascalCaseなクラス
public class User
{
    public int UserId { get; set; }
    public string UserName { get; set; }
}

 

Dapper.FluentMapでマッピング

公式のREADMEを読めば分かりますが、いろいろと悩んだところもあったため、メモを残しておきます。

マッピングの方法は2種類あります。

  • Manual mapping
  • Convention based mapping

今回は、DBのsnake_caseなテーブルをPascalCaseなクラスにマッピングするを例に、それぞれ試してみます。

 

Manual mapping

一つ一つマッピングしていく方法です。

テーブルとクラスをマッピングするクラスを作成します。

public class UserMap : EntityMap<User>
{
    public UserMap()
    {
        Map(p => p.UserId).ToColumn("user_id");
        Map(p => p.UserName).ToColumn("user_name");
    }
}

 
そして、Dapperを実行する前に、FluentMapper.Intialize()を呼びます。

FluentMapper.Intialize(config => config.AddMap(new UserMap()));

 
この場合、マッピングクラスを1つずつAddMap()する必要があるため、少々手間です。

 

Convention based mapping

規約ベースで、一括でマッピングする方法です。

 

マッピングするクラスの用意

テーブルカラムの型と名称を指定、あるいはPrefixを指定するマッピングクラスを作ります。

public class TypePrefixConvention : Convention
{
    public TypePrefixConvention()
    {
        Properties<int>()
            .Where(c => c.Name == "UserId")                 // クラスのプロパティ
            .Configure(c => c.HasColumnName("user_id"));    // DBのテーブルのカラム

        Properties<string>()
            .Where(c => c.Name == "UserName")               // クラスのプロパティ
            .Configure(c => c.HasColumnName("user_name"));  // DBのテーブルのカラム

        Properties<int>().Configure(c => c.HasPrefix("int"));
    }
}

 
もしくは、正規表現を使ったマッピングクラスを作ります。

public class PropertyTransformConvention : Convention
{
    public PropertyTransformConvention()
    {
        // patternはクラスのプロパティ、replacementはDBのテーブルのカラムのパターン
        Properties().Configure(c => c.Transform(
            s => Regex.Replace(s, "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", "$1$3_$2$4").ToLower()));
    }
}

 

マッピングの実行

実行する方法は以下の2つがあります。

前者の場合は、

FluentMapper.Intialize(config =>
{
    // `PropertyTransformConvention`でも使える
    var type = typeof(User);
    config.AddConvention<TypePrefixConvention>().ForEntitiesInAssembly(type.Assembly, type.Namespace);
});

後者の場合は、

FluentMapper.Intialize(config =>
{
    // `TypePrefixConvention`でも使える
    // exeはハイフンだが名前空間がアンダースコアなので、名前空間用に置換(力技...)
    var currentNamespace = Assembly.GetExecutingAssembly().GetName().Name.Replace("-", "_");
    config.AddConvention<PropertyTransformConvention>().ForEntitiesInCurrentAssembly(currentNamespace);
});

のような感じになります。

 
なお、ForEntitiesInCurrentAssemblyメソッド・ForEntitiesInAssemblyメソッドの両方とも、第二引数は「An array of namespaces which filter the types in xxx assembly. This parameter is optional.」とコメントが付いています。

ただ、ForEntitiesInAssemblyの実装ForEntitiesInCurrentAssemblyの実装を見ると、

foreach (var type in assembly.GetExportedTypes()
                              .Where(type => namespaces.Any(n => type.Namespace == n)))

のような感じになっており、第二引数には名前空間を渡したほうが良さそうでした。

 

まとめ

DBのsnake_caseなテーブルをPascalCaseなクラスにマッピングするには、Transformを使ったConvention based mappingが良さそうでした。

パフォーマンスとかは見てませんが、自分の場合はそこまで大きなものは作らないため、これで十分そうでした。

 

ソースコード

GitHubに上げておきました。
thinkAmi-sandbox/DapperFluentMap-SQLite

 

参考

SQLite
Dapper.FluentMap作者のコメント