C# – Dapper を使用してクエリ結果を複数のオブジェクトにマップする

結合されたテーブルをクエリする場合、Dapper のマルチ マッピング機能を使用して、各行を複数のオブジェクトにマッピングできます。

マルチマップするには、Dapper に以下を提供する必要があります:

  • マッピングするタイプ
  • 分割する列。これにより、Dapper がどの列をどの型にマップしようとするかが決まります。
  • Dapper がマップされたオブジェクトを渡し、それらをリンクできるマッピング関数。

この記事では、マルチ マッピングの例を示します。

注:分割列を指定しない場合、デフォルトの「Id」が使用されます。分割列を常に明示的に指定することをお勧めします。

1 対 1 の関係マルチ マッピング

Orders テーブルは Customers テーブルと 1 対 1 の関係にあり、CustomerId 列によってリンクされています:

次の SQL クエリは、注文と関連する顧客を選択します:

SELECT o.OrderId, o.[Status], c.CustomerId, c.[Name]                         
FROM Orders o
INNER JOIN Customers c
ON o.CustomerId = c.CustomerId
WHERE o.OrderId = @Id
Code language: SQL (Structured Query Language) (sql)

クエリの結果は次のとおりです。

OrderId	Status	CustomerId	Name
43672	New	30067		Corey LuoCode language: plaintext (plaintext)

これらの結果を Order および Customer オブジェクトにマップするには、マルチ マッピングを使用し、CustomerId 列で分割します。

using (var con = new SqlConnection(ConnectionString))
{
	return con.Query<Order, Customer, Order>(GET_SQL, 
		map: (order, customer) =>
		{
			order.Customer = customer;
			return order;
		},
		param: new { id },
		splitOn: "CustomerId").FirstOrDefault();
}
Code language: C# (cs)

Query は、最初に列を Order オブジェクトにマップし、次に Customer オブジェクトにマップして、IEnumerable を返すことを意味します。

行ごとに、Order オブジェクトと Customer オブジェクトを作成します。次のように、分割列 (CustomerId) に基づいて列をオブジェクトにマップします:

  • 注文列 =CustomerId (OrderId、Status) の左側のすべての列
  • 顧客列 =残りの列 (CustomerId、Name)

最終結果は、Customer オブジェクトを含む Order オブジェクトです:

{
  "OrderId": 43659,
  "Customer": {
    "CustomerId": 29825,
    "Name": "Corey Luo"
  },
  "Status": "New"
}Code language: JSON / JSON with Comments (json)

1 対多の関係マルチ マッピング

Orders テーブルは OrderLines テーブルと 1 対多の関係にあり、OrderId 列によってリンクされています:

次の SQL クエリは、注文と関連する注文明細を選択します:

SELECT o.OrderId, o.Status, ol.OrderLineId, ol.Product, ol.Quantity
FROM Orders o
INNER JOIN OrderLines ol
ON o.OrderId = ol.OrderId
WHERE o.OrderId IN @Ids
Code language: SQL (Structured Query Language) (sql)

クエリ結果は次のとおりです (単一の注文 ID の場合):

OrderId	Status	OrderLineId	Product				Quantity
43672	New	126		Mountain Bike Socks, M		6
43672	New	127		Mountain-100 Black, 42		2
43672	New	128		Mountain-100 Silver, 48		1Code language: plaintext (plaintext)

これらの結果を Order/OrderLine オブジェクトにマップするには、OrderLineId 列で複数のマップと分割を行います。 1 対多のシナリオでは、map 関数はより複雑になります。

var orderMap = new Dictionary<int, Order>();

using (var con = new SqlConnection(ConnectionString))
{
	con.Query<Order, OrderLine, Order>(GET_LINES_SQL,
		map: (order, orderLine) =>
		{
			orderLine.OrderId = order.OrderId; //non-reference back link

			//check if this order has been seen already
			if (orderMap.TryGetValue(order.OrderId, out Order existingOrder))
			{
				order = existingOrder;
			}
			else
			{
				order.Lines = new List<OrderLine>();
				orderMap.Add(order.OrderId, order);

			}

			order.Lines.Add(orderLine);
			return order;
		},
		splitOn: "OrderLineId",
		param: new { ids }
	);
}

return orderMap.Values;
Code language: C# (cs)

Query は、最初に列を Order オブジェクトにマップし、次に OrderLine オブジェクトにマップして、IEnumerable を返すことを意味します。

行ごとに、Order オブジェクトと OrderLine オブジェクトを作成し、次のように分割列 (OrderLineId) に基づいて列をマップします。

  • 注文列 =OrderLineId (OrderId、Status) の左側のすべての列
  • OrderLine 列 =残りの列 (OrderLineId、Product、Quantity)。

マップされたオブジェクトを map 関数に渡します。 Dapper は、注文列をすべての行の新しい Order オブジェクトにマップします。そのため、辞書を使用して重複を排除し、一意の Order オブジェクトを追跡する必要があります。

これにより、OrderLine オブジェクトの配列を持つ次の Order オブジェクトが生成されます:

{
  "OrderId": 43672,
  "Lines": [
    {
      "OrderLineId": 126,
      "OrderId": 43672,
      "Product": "Mountain Bike Socks, M",
      "Quantity": 6
    },
    {
      "OrderLineId": 127,
      "OrderId": 43672,
      "Product": "Mountain-100 Black, 42",
      "Quantity": 2
    },
    {
      "OrderLineId": 128,
      "OrderId": 43672,
      "Product": "Mountain-100 Silver, 48",
      "Quantity": 1
    }
  ],
  "Status": "New"
}Code language: JSON / JSON with Comments (json)

注:Dapper が注文列を各行の新しい Order オブジェクトにマッピングするのは効率が悪いようです。別の方法として、複数のクエリ (Orders 用と OrderLines 用に 1 つずつ) を実行し、結果をループしてリンクする方法があります。私のテストに基づくと、マルチ マッピングとほぼ同じパフォーマンスです。

3 つ以上のオブジェクトへのマルチ マッピング

Orders テーブルには、Customers テーブルおよび Stores テーブルと 1 対 1 の関係があります。

次の SQL クエリは、注文と関連する顧客と店舗を選択します:

SELECT o.OrderId, o.[Status], c.CustomerId, c.[Name], s.StoreId, s.[Location]
FROM Orders o
INNER JOIN Customers c
ON o.CustomerId = c.CustomerId
INNER JOIN Stores s
ON o.StoreId = s.StoreId
WHERE o.OrderId = @Id
Code language: SQL (Structured Query Language) (sql)

結果は次のとおりです。

OrderId	Status	CustomerId	Name		StoreId	Location
43672	New	30067		Corey Luo	1	Main StCode language: plaintext (plaintext)

これらの結果を Order/Customer/Store オブジェクトにマルチマップする方法は次のとおりです:

using (var con = new SqlConnection(ConnectionString))
{
	return con.Query<Order, Customer, Store, Order>(GET_SQL,
		map: (order, customer, store) =>
		{
			order.Customer = customer;
			order.Store = store;
			return order;
		},
	param: new { id },
	splitOn: "CustomerId,StoreId").FirstOrDefault();
}
Code language: C# (cs)

Query は、最初に列を Order オブジェクト、次に Customer オブジェクト、次に Store オブジェクトにマップし、最後に IEnumerable を返すことを意味します。

3 つ以上のオブジェクトにマッピングする場合は、カンマ区切りの文字列 ("CustomerId,StoreId") で複数の分割列を指定する必要があります。次のように、分割された列 (CustomerId と StoreId) に基づいて列を 3 つのオブジェクトにマップします。

  • 注文列 =CustomerId (OrderId、Status) の左側のすべての列
  • Customer 列 =StoreId (CustomerId, Name) の左側の残りの列
  • 店舗列 =残りの列 (StoreId、場所)

これは、Customer/Store オブジェクトがリンクされた結果の Order オブジェクトです:

{
  "OrderId": 43659,
  "Customer": {
    "CustomerId": 29825,
    "Name": "Corey Luo"
  },
  "Status": "New",
  "Store": {
    "StoreId": 1,
    "Location": "Main St"
  }
}Code language: JSON / JSON with Comments (json)