インドカレーファンクラブ

パソコン、カメラ

【C#】Dapperを用いて一対多の関係にある2テーブルのレコードを階層状のクラスにマッピングする

基本的にこれのJeroen Kさんの丸パクリの話を噛み砕いてるだけの記事

dapper - Multi-Mapper to create object hierarchy - Stack Overflow

タイトルだけだと意味分かんないと思う

Itemsテーブルと、それに対して一対多の関係にあるItemRemarksテーブルがあって、
そのレコードを取得する際にList<ItemRemark>型のフィールドをもつItem型に紐付けたい、という話

これでも意味分かんないと思うので、実際のレコード例とかクラスを見たほうがよい

DBとレコード

中間テーブルは設けてない

Items

ID Name
1 カレー
2 ラーメン
3 サラダ

ItemRemarks

ItemId Description
1 おすすめ
3 健康的
3 今なら半額

クラス

~Entityって名前にすべきだったけどC#書くの久しぶりで忘れてた

public class Item
{
    public int Id { get; set; }

    public string Name { get; set; }

    public List<ItemRemark> Remarks { get; set; } = new List<ItemRemark>();
}

public class ItemRemark
{
    public int ItemId { get; set; }

    public string Description { get; set; }
}

ほしいオブジェクトのイメージ

こんな感じのがほしい!

Items: [
    Item: {
        Id: 1,
        Name: 'カレー',
        ItemRemarks: [
            ItemRemark: {
                description: 'おすすめ'
            }
        ]
    },
    Item: {
        Id: 2,
        Name: 'ラーメン',
        ItemRemarks: [ ]
    },
    Item: {
        Id: 3,
        Name: 'サラダ',
        ItemRemarks: [
            ItemRemark: {
                description: '健康的'
            },
            ItemRemark: {
                description: '今なら半額'
            }
        ]
    }
]

実装

ConsoleApplicationでつくって、NugetでDapperとSystem.Data.SQLite.Coreだけインストールしたら下記サンプルが動くはず

static void Main(string[] args)
{
    // sqliteでデータを用意するところから
    var sqliteFile = "testdb.sqlite";
    File.Delete(sqliteFile);
    SQLiteConnection.CreateFile(sqliteFile);
    var config = new SQLiteConnectionStringBuilder(){ DataSource = sqliteFile };
    using (var connection = new SQLiteConnection(config.ToString()))
    {
        connection.Open();
        // データを入れる
        var createDbSql = @"
CREATE TABLE Items (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name TEXT NOT NULL
);
CREATE TABLE ItemRemarks (
    itemId INTEGER NOT NULL,
    description TEXT NOT NULL,
    foreign key(itemId) references Items(id)
);
INSERT INTO Items(id, name) VALUES(1, 'カレー');
INSERT INTO Items(id, name) VALUES(2, 'ラーメン');
INSERT INTO Items(id, name) VALUES(3, 'サラダ');
INSERT INTO ItemRemarks(itemId, description) VALUES(1, 'おすすめ');
INSERT INTO ItemRemarks(itemId, description) VALUES(3, '健康的');
INSERT INTO ItemRemarks(itemId, description) VALUES(3, '今なら半額');
";
        connection.Execute(createDbSql);

        // ここからが本題!
        var lookup = new Dictionary<int, Item>();
        connection.Query<Item, ItemRemark, Item>(@"
SELECT
    I.Id
    ,I.Name
    ,IR.ItemId
    ,IR.Description
FROM Items I
LEFT JOIN ItemRemarks IR
    ON IR.itemId = I.id",
            (i, r) => {
                if (!lookup.TryGetValue(i.Id, out Item item)) { lookup.Add(i.Id, item = i); }
                if (r != null) { item.Remarks.Add(r); }
                return item;
            }, splitOn: nameof(ItemRemark.ItemId)
        ).AsQueryable();

        // ここに期待するデータが入る
        IEnumerable<Item> resultList = lookup.Values;
    }
}

ややこしい!

クエリでItemsとItemRemarksをまるっととって、その後ごちゃごちゃマッピングしてるイメージ
必要に応じてsplitOnに気をつけよう
細かいことはDapperのドキュメントから...

もっといいやり方がありそうな気はしつつ取り敢えず動く

これは冒頭に記述したStackoverflowのJeroen Kさんのやり方の通り(ちょっとアレンジはあるけど)

他の人は2テーブルを取得してforeachで回してマッピング、っていう一連の処理をそういうUtilなメソッドにしちゃうといいよ、みたいな感じ

何回も書くならそっちのほうがいいけど、1箇所だけピンポイントで使うなら上のコードでいいと思う でも数ヶ月後に上のコード見たら混乱しそうだし、普通に2テーブルとってService層とかでループしたほうが楽で良い気もする