【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層とかでループしたほうが楽で良い気もする