はじめに
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ではIDbConnection
のQuery
メソッドなどが拡張されていて、これらのメソッドを用いてオブジェクトにDBから取得した値をマッピングできます。
拡張されたQuery
メソッドのジェネリクスにマッピングしたい型を、map
引数に渡すコールバックにてマッピング方法をそれぞれ指定できます。
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が入っていると認識していただければ大丈夫です。
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として返したときの出力を以下に示します。
[
{
"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
オブジェクトのリストを取得しています。
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つのときとやっていることは同じみたいですね。面白い実装ですが、ここまで使うことはあんまりなさそう、、というか使わないほうがわかりやすそうです。
// マッピング対象のテーブルが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を使ってマッピングする手法自体は応用も利いて役に立つ場面もあると思うので、この記事が誰かの役に立ったら嬉しいです!読んでいただきありがとうございました!
参考