Dapperでクラス型のプロパティを持つクラスオブジェクトにマッピングする

# はじめに
RDBでデータの管理を行うアプリケーションの制作において、DBにアクセスしデータのやり取りを行う処理も実装していました。
その際、クラス型のオブジェクトをプロパティに持つクラスにマッピングするのに、少し詰まったので、備忘録的に記事にします。

実際に行った内容としては、複数のテーブルに分けて保存されている関連したデータを一つのオブジェクトにまとめて返す処理をDapperによるマッピングで一息で行う処理を実装しました。

# 具体例
本記事では例として、登録されている「メンバー」で構成された複数の「ペア」を、「ペアリスト」のオブジェクトとしてマッピングする方法を紹介します。

## マッピング対象となるクラス

```csharp
public class Team {
    long Id,
    string Name
}

public class Member {
    long Id,
    string Name,
    Team team
}

public class PairInfo {
    long Id,
    long ParentId,
    long Member1id,
    long Member2Id
}

public class Pairs {
    List<PairInfo> PairList,
    string GenerateDate
}
```

## DBのテーブル構成
DBはSQLiteを使用しています。

<b>team</b>
|カラム名|型|
|:-:|:-:|
|id|integer|
|name|text|

<b>member</b>
|カラム名|型|
|:-:|:-:|
|id|integer|
|name|text|
|teamid|integer|

<b>pairinfo</b>
|カラム名|型|
|:-:|:-:|
|id|integer|
|parentid|integer|
|member1id|integer|
|member2id|integer|

<b>pairs</b>
|カラム名|型|
|:-:|:-:|
|id|integer|
|name|text|


## 実際のマッピング処理
Dapperでは`IDbConnection`の`Query`メソッドなどが拡張されていて、これらのメソッドを用いてオブジェクトにDBから取得した値をマッピングできます。
拡張された`Query`メソッドの型引数にマッピングしたい型を、`map`引数に渡すコールバックにてマッピング方法をそれぞれ指定できます。

```csharp:拡張されたQueryメソッドのシグネチャ
IEnumerable<TReturn> Query<TFirst, TSecond, TReturn>(
    this IDbConnection cnn,
    string sql,
    Func<TFirst, TSecond, TReturn> map,
    object param = null,
    IDbTransaction transaction = null,
    bool buffered = true,
    string splitOn = "Id",
    int? commandTimeout = null,
    CommandType? commandType = null)
```

以下に実際にマッピングを行っている箇所を抜き出して記載します。

### メンバー情報のみマッピング
まず、[チュートリアル](https://dapper-tutorial.net/query)のページでも紹介されているオブジェクトが1重にネストされたオブジェクトへのマッピングの例を紹介します。
この例では`Team`オブジェクトがネストされた`Member`オブジェクトのリストを取得しています。

また、SQLの生成にはクエリビルダーの[SqlKata](https://sqlkata.com)を使用していますが、`sql`変数に生のSQLが入っていると認識していただければ大丈夫です。

```csharp:MemberのListの取得
public List<MemberInfo> GetMembers()
{
    const string membersTable = "members";
    const string temasTable = "teams";

    var connection = new SqliteConnection(database);
    var compiler = new SqliteCompiler();
    var db = new QueryFactory(connection, compiler);

    var query = db.Query(membersTable)
        .Select($"{membersTable}.id", $"{membersTable}.name", $"{teamsTable}.id", $"{teamsTable}.name")
        .Join(teamsTable, $"{membersTable}.team_id", $"{teamsTable}.id")
        .Where("del_flg", 0);
        .OrderBy($"{membersTable}.id");

    // sql = SELECT "members"."id", "members"."name", "teams"."id", "teams"."name" FROM "members" 
    //     INNER JOIN "teams" ON "members"."team_id" = "teams"."id"
    //     WHERE "del_flg" = 0 
    //     ORDER BY "members"."id";
    var sql = compiler.Compile(query).ToString();

    var result = connection.Query<MemberInfo, TeamInfo, MemberInfo>(sql, (member, team) =>
    {
        member.Team = team;
        return member;
    });

    return result.ToList();
}
```

`Query`メソッドの型引数で指定している型は、Select句で登場しているテーブルの順番に対応しています。Select句で1番目に登場する`members`テーブルは1番目の型引数に指定している`MemberInfo`に、2番目の`teams`は`TeamInfo`にそれぞれマッピングされるということですね。
Select句の`members.id`. `members.name`は、`MemberInfo`の`Id`, `Name`プロパティにそれぞれマッピングされ、コールバックの引数の`member`として渡されます。この際、値の指定がない`Team`プロパティは`null`となっています。
同様に`teams.id`, `teams.name`はコールバックに`TeamInfo`型の`team`として渡されています。

コールバック内では、`member`オブジェクトの`Team`プロパティに入っている`null`の代わりに、`team`オブジェクトを代入することで、`member`オブジェクトを完成させ、最後の型引数で指定されている`MemberInfo`型のオブジェクトとして返しています。

このように実装することで、DB上で複数のテーブルで表現されている、クラス型のオブジェクトがネストされたオブジェクトに一息でマッピングできます。

得られる`MemberInfo`のリストの例をJSONとして返したときの出力を以下に示します。

```json:出力
[
  {
    "id": 1,
    "name": "taro",
    "team": {
      "id": 1,
      "name": "a_team"
    }
  },
  {
    "id": 2,
    "name": "jiro",
    "team": {
      "id": 2,
      "name": "b_team"
    }
  },
  {
    "id": 3,
    "name": "saburo",
    "team": {
      "id": 3,
      "name": "c_team"
    }
  },
  {
    "id": 4,
    "name": "shiro",
    "team": {
      "id": 4,
      "name": "d_team"
    }
  },
  {
    "id": 5,
    "name": "goro",
    "team": {
      "id": 5,
      "name": "e_team"
    }
  }
]
```

### ペアリスト情報にまとめて一気にマッピング
次に、オブジェクトが3重にネストされたオブジェクトへのマッピングの例を紹介します。
この例では`Team`を持つ`Member`を持つ`PairInfo`のリストを持つ`PairsList`オブジェクトのリストを取得しています。

```csharp:PairsのListの取得
public List<PairsInfo> GetPairs()
{
    var connection = new SqliteConnection(database);
    var compiler = new SqliteCompiler();
    var db = new QueryFactory(connection, compiler);

    var query = db.Query($"{pairsParentTable} as pp")
        .Select("pp.id", "pp.gen_dt as GenerateDate", "p.id", "m1.id", "m1.name", "t1.id", "t1.name", "m2.id", "m2.name", "t2.id", "t2.name")
        .Join($"{pairsTable} as p", "pp.id", "p.parent_id")
        .Join($"{membersTable} as m1", "p.mmbr1_id", "m1.id", type: "left join")
        .Join($"{teamsTable} as t1", "m1.team_id", "t1.id", type: "left join")
        .Join($"{membersTable} as m2", "p.mmbr2_id", "m2.id", type: "left join")
        .Join($"{teamsTable} as t2", "m2.team_id", "t2.id", type: "left join")
        .Where("pp.del_flg", 0)
        .OrderByDesc("pp.gen_dt");

    // sql = SELECT "pp"."id", "pp"."gen_dt" AS "GenerateDate", "p"."id",
    //     "m1"."id", "m1"."name", "t1"."id", "t1"."name",
    //     "m2"."id", "m2"."name", "t2"."id", "t2"."name" FROM "pairs_parent" AS "pp"
    //     INNER JOIN "pairs" AS "p" ON "pp"."id" = "p"."parent_id"
    //     LEFT JOIN "members" AS "m1" ON "p"."mmbr1_id" = "m1"."id"
    //     LEFT JOIN "teams" AS "t1" ON "m1"."team_id" = "t1"."id"
    //     LEFT JOIN "members" AS "m2" ON "p"."mmbr2_id" = "m2"."id"
    //     LEFT JOIN "teams" AS "t2" ON "m2"."team_id" = "t2"."id"
    //     WHERE "pp"."del_flg" = 0
    //     ORDER BY "pp"."gen_dt" DESC
    var sql = compiler.Compile(query).ToString();

    var pairsInfoDictionary = new Dictionary<long, PairsInfo>();
    var result = connection.Query<PairsInfo, PairInfo, MemberInfo, TeamInfo, MemberInfo?, TeamInfo, PairsInfo>(sql, (pairsInfo, pair, member1, team1, member2, team2) =>
    {
        member1.Team = team1;
        pair.Member1 = member1;
        if (member2 != null)
        {
            member2.Team = team2;
            pair.Member2 = member2;
        }

        var pairsInfoEntry = new PairsInfo();
        if (!pairsInfoDictionary.TryGetValue(pairsInfo.Id, out pairsInfoEntry))
        {
            pairsInfoEntry = pairsInfo;
            pairsInfoEntry.pairs = new List<PairInfo>();
            pairsInfoDictionary.Add(pairsInfo.Id, pairsInfoEntry);
        }

        pairsInfoEntry.pairs.Add(pair);
        return pairsInfoEntry;
    });

    return result.Distinct().ToList();
}
```

メンバー情報のみのマッピングのときと違い、`Dictionary`が出てきてなにやら行っていますが、これはデータテーブルのリレーションが1対多になっているため、データの重複なく上手くマッピングするために利用しています。この手法は上記でも登場した[チュートリアル](https://dapper-tutorial.net/query)に書いてあったものです。

また今回の処理では、`Query`を呼び出す際にジェネリクスで指定している型の数が大きく増えているのが見てわかると思います。DapperではSelect句のテーブルの値をマッピングするクラスが7つまでなら`Query`メソッドのオーバーロードでマッピングできるみたいです。今回は6つ指定しているので割とギリギリですね。ちなみに実装は以下のようになっていて、メンバー情報をマッピングしたときの、マッピングするクラスが2つのときとやっていることは同じみたいですね。面白い実装ですが、ここまで使うことはあんまりなさそう、、というか使わないほうがわかりやすそうです。

```csharp:DapperのQueryメソッドのオーバーロードの一部抜粋
        // マッピング対象のテーブルが2つのとき
        public static IEnumerable<TReturn> Query<TFirst, TSecond, TReturn>(this IDbConnection cnn, string sql, Func<TFirst, TSecond, TReturn> map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) =>
            MultiMap<TFirst, TSecond, DontMap, DontMap, DontMap, DontMap, DontMap, TReturn>(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType);

        ...

        // マッピング対象のテーブルが6つのとき
        public static IEnumerable<TReturn> Query<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn>(this IDbConnection cnn, string sql, Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn> map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) =>
            MultiMap<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, DontMap, TReturn>(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType);
```

出力のJSONは以下のようになり、DBの各テーブルに分けられたデータが一つのオブジェクトにまとめられたことがわかりますね。

```json:出力
[
  {
    "id": 2,
    "generateDate": "2023/02/24 19:40:10",
    "pairs": [
      {
        "id": 2,
        "member1": {
          "id": 5,
          "name": "goro",
          "team": {
            "id": 5,
            "name": "e_team"
          }
        },
        "member2": {
          "id": 0,
          "name": "",
          "team": {
            "id": 0,
            "name": ""
          }
        }
      },
      {
        "id": 5,
        "member1": {
          "id": 1,
          "name": "taro",
          "team": {
            "id": 1,
            "name": "a_team"
          }
        },
        "member2": {
          "id": 2,
          "name": "jiro",
          "team": {
            "id": 2,
            "name": "b_team"
          }
        }
      },
      {
        "id": 6,
        "member1": {
          "id": 3,
          "name": "saburo",
          "team": {
            "id": 3,
            "name": "c_team"
          }
        },
        "member2": {
          "id": 4,
          "name": "shiro",
          "team": {
            "id": 4,
            "name": "d_team"
          }
        }
      }
    ]
  },
  {
    "id": 1,
    "generateDate": "2023/02/24 19:20:30",
    "pairs": [
      {
        "id": 1,
        "member1": {
          "id": 1,
          "name": "taro",
          "team": {
            "id": 1,
            "name": "a_team"
          }
        },
        "member2": {
          "id": 3,
          "name": "saburo",
          "team": {
            "id": 3,
            "name": "c_team"
          }
        }
      },
      {
        "id": 3,
        "member1": {
          "id": 2,
          "name": "jiro",
          "team": {
            "id": 2,
            "name": "b_team"
          }
        },
        "member2": {
          "id": 4,
          "name": "shiro",
          "team": {
            "id": 4,
            "name": "d_team"
          }
        }
      },
      {
        "id": 4,
        "member1": {
          "id": 5,
          "name": "goro",
          "team": {
            "id": 5,
            "name": "e_team"
          }
        },
        "member2": {
          "id": 0,
          "name": "",
          "team": {
            "id": 0,
            "name": ""
          }
        }
      }
    ]
  }
]
```

# 最後に
今回はDapperで拡張された`Query`メソッドを使ったマッピングを紹介しましたが、特に3つ以上のテーブルをマッピングするような使い方の場合は複雑になってしまうため、マッピングするためにクラスを別途作成してそれに直接マッピングを行い、個別手動で値を分けて使うようにする方がわかりやすい気がします。。

が、Dapperを使ってマッピングする手法自体は応用も利いて役に立つ場面もあると思うので、この記事が誰かの役に立ったら嬉しいです！読んでいただきありがとうございました！

# 参考
https://dapper-tutorial.net/query