LoginSignup
7
7

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

Last updated at Posted at 2023-05-24

はじめに

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

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

具体例

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

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

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を使用しています。

team

カラム名
id integer
name text

member

カラム名
id integer
name text
teamid integer

pairinfo

カラム名
id integer
parentid integer
member1id integer
member2id integer

pairs

カラム名
id integer
name text

実際のマッピング処理

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

拡張された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)

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

メンバー情報のみマッピング

まず、チュートリアルのページでも紹介されているオブジェクトが1重にネストされたオブジェクトへのマッピングの例を紹介します。
この例ではTeamオブジェクトがネストされたMemberオブジェクトのリストを取得しています。

また、SQLの生成にはクエリビルダーのSqlKataを使用していますが、sql変数に生のSQLが入っていると認識していただければ大丈夫です。

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番目のteamsTeamInfoにそれぞれマッピングされるということですね。
Select句のmembers.id. members.nameは、MemberInfoId, Nameプロパティにそれぞれマッピングされ、コールバックの引数のmemberとして渡されます。この際、値の指定がないTeamプロパティはnullとなっています。
同様にteams.id, teams.nameはコールバックにTeamInfo型のteam`として渡されています。

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

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

得られるMemberInfoのリストの例を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オブジェクトのリストを取得しています。

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対多になっているため、データの重複なく上手くマッピングするために利用しています。この手法は上記でも登場したチュートリアルに書いてあったものです。

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

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の各テーブルに分けられたデータが一つのオブジェクトにまとめられたことがわかりますね。

出力
[
  {
    "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を使ってマッピングする手法自体は応用も利いて役に立つ場面もあると思うので、この記事が誰かの役に立ったら嬉しいです!読んでいただきありがとうございました!

参考

7
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7