私はこのコードを1対多の関係を投影するように書きましたが、うまくいきません:
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
(@"Select Stores.Id as StoreId, Stores.Name,
Employees.Id as EmployeeId, Employees.FirstName,
Employees.LastName, Employees.StoreId
from Store Stores
INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
(a, s) => { a.Employees = s; return a; },
splitOn: "EmployeeId");
foreach (var store in stores)
{
Console.WriteLine(store.Name);
}
}
誰でも間違いを見つけることができますか?
編集:
これらは私のエンティティです:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public IList<Store> Stores { get; set; }
public Product()
{
Stores = new List<Store>();
}
}
public class Store
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Employee> Employees { get; set; }
public Store()
{
Products = new List<Product>();
Employees = new List<Employee>();
}
}
編集:
クエリを次のように変更します。
IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
(@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,Employees.FirstName,
Employees.LastName,Employees.StoreId from Store Stores INNER JOIN Employee Employees
ON Stores.Id = Employees.StoreId",
(a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");
そして、私は例外を取り除きます!ただし、従業員はまったくマッピングされません。最初のクエリでIEnumerable<Employee>
にどのような問題があったのかまだわかりません。
この投稿では、 高度に正規化されたSQLデータベース を照会し、結果を高度にネストされたC#POCOオブジェクトのセットにマッピングする方法を示します。
材料:
この問題を解決することができた洞察は、MicroORM
をmapping the result back to the POCO Entities
から分離することです。したがって、2つの個別のライブラリを使用します。
基本的に、 Dapper を使用してデータベースを照会し、次に Slapper.Automapper を使用して結果を直接POCOにマッピングします。
List<MyClass1>
が含まれ、さらにList<MySubClass2>
など)。inner joins
を使用してフラット化されたクエリを作成し、フラットな結果を返すことは、クライアント側でステッチして複数のselectステートメントを作成するよりもはるかに簡単です。inner join
(重複を戻す)を使用してフラット化しないで、代わりに複数のselect
ステートメントを作成し、クライアント側ですべてをつなぎ合わせます(このページの他の回答を参照)。私のテストでは、 Slapper.Automapper はDapperが返す結果に小さなオーバーヘッドを追加しました。つまり、Entity Frameworkよりも10倍高速であり、それでも、SQL + C#が可能な理論上の最大速度にかなり近い。
ほとんどの実際の場合、オーバーヘッドの大部分は、C#側での結果のマッピングではなく、最適ではないSQLクエリにあります。
反復の総数:1000
Dapper by itself
:1.889クエリごとのミリ秒、3 lines of code to return the dynamic
を使用。Dapper + Slapper.Automapper
:2.463クエリごとのミリ秒。追加の3 lines of code for the query + mapping from dynamic to POCO Entities
を使用します。この例では、Contacts
のリストがあり、各Contact
には1つ以上のphone numbers
を含めることができます。
public class TestContact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public List<TestPhone> TestPhones { get; set; }
}
public class TestPhone
{
public int PhoneId { get; set; }
public int ContactID { get; set; } // foreign key
public string Number { get; set; }
}
TestContact
TestPhone
このテーブルには、ContactID
テーブルを参照する外部キーTestContact
があることに注意してください(これは上記のPOCOのList<TestPhone>
に対応します)。
SQLクエリでは、必要なすべてのデータを取得するために必要な数のJOIN
ステートメントを flat、denormalized form で使用します。はい、これは出力に重複を生成する可能性がありますが、これらの重複は Slapper.Automapper を使用してこのクエリの結果をPOCOオブジェクトマップに自動的にマッピングすると自動的に削除されます。
USE [MyDatabase];
SELECT tc.[ContactID] as ContactID
,tc.[ContactName] as ContactName
,tp.[PhoneId] AS TestPhones_PhoneId
,tp.[ContactId] AS TestPhones_ContactId
,tp.[Number] AS TestPhones_Number
FROM TestContact tc
INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId
const string sql = @"SELECT tc.[ContactID] as ContactID
,tc.[ContactName] as ContactName
,tp.[PhoneId] AS TestPhones_PhoneId
,tp.[ContactId] AS TestPhones_ContactId
,tp.[Number] AS TestPhones_Number
FROM TestContact tc
INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";
string connectionString = // -- Insert SQL connection string here.
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
// Can set default database here with conn.ChangeDatabase(...)
{
// Step 1: Use Dapper to return the flat result as a Dynamic.
dynamic test = conn.Query<dynamic>(sql);
// Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
// - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
// let it know the primary key for each POCO.
// - Must also use underscore notation ("_") to name parameters in the SQL query;
// see Slapper.Automapper docs.
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });
var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();
foreach (var c in testContact)
{
foreach (var p in c.TestPhones)
{
Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);
}
}
}
}
Visual Studioを見ると、Slapper.AutomapperがPOCOエンティティに適切に入力されていることがわかります。つまり、List<TestContact>
があり、各TestContact
にはList<TestPhone>
があります。
DapperとSlapper.Automapperは両方とも、速度のためにすべてを内部的にキャッシュします。メモリの問題に遭遇した場合(非常にまれ)、両方のキャッシュを時々クリアするようにしてください。
アンダースコア(_
)表記 を使用して、返される列に名前を付けて、Slapper.Automapperが結果をPOCOエンティティにマップする方法の手がかりを与えるようにします。
Slapper.Automapperが各POCOエンティティのプライマリキーの手がかりを与えるようにしてください(Slapper.AutoMapper.Configuration.AddIdentifiers
行を参照)。このために、POCOでAttributes
を使用することもできます。この手順をスキップすると、Slapper.Automapperはマッピングを適切に行う方法を知らないため、(理論的には)間違った方向に進む可能性があります。
40以上の正規化されたテーブルを持つ巨大な実稼働データベースにこの手法をうまく適用しました。 16以上のinner join
およびleft join
を含む高度なSQLクエリを適切なPOCO階層(4レベルのネスト)にマッピングするのに完全に機能しました。クエリは、ADO.NETで手作業でコーディングするのとほぼ同じくらい高速です(通常、クエリでは52ミリ秒、フラットな結果からPOCO階層へのマッピングでは50ミリ秒でした)。これは本当に画期的なものではありませんが、特にクエリを実行するだけの場合は、速度と使いやすさでEntity Frameworkに勝ります。
コードは9か月間実稼働で問題なく実行されています。 Slapper.Automapper
の最新バージョンには、SQLクエリで返されるnullに関連する問題を修正するために適用したすべての変更が含まれています。
コードは21か月間実稼働で問題なく実行されており、FTSE 250企業の何百人ものユーザーからの継続的なクエリを処理しています。
Slapper.Automapper
は、.csvファイルをPOCOのリストに直接マッピングするのにも最適です。 .csvファイルをIDictionaryのリストに読み込んでから、POCOのターゲットリストに直接マップします。唯一のトリックは、適切なint Id {get; set}
を追加し、すべての行で一意であることを確認する必要があることです(そうしないと、オートマッパーは行を区別できなくなります)。
コードコメントを追加するためのマイナーアップデート。
私はそれをできるだけシンプルにしたかった、私の解決策:
public List<ForumMessage> GetForumMessagesByParentId(int parentId)
{
var sql = @"
select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login,
d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies,
d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key]
from
t_data d
where d.cd_data = @DataId order by id_data asc;
select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal
from
t_data d
inner join T_data_image di on d.id_data = di.cd_data
inner join T_image i on di.cd_image = i.id_image
where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;";
var mapper = _conn.QueryMultiple(sql, new { DataId = parentId });
var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v);
var images = mapper.Read<ForumMessageImage>().ToList();
foreach(var imageGroup in images.GroupBy(g => g.DataId))
{
messages[imageGroup.Key].Images = imageGroup.ToList();
}
return messages.Values.ToList();
}
私はまだデータベースへの呼び出しを1回行い、1つではなく2つのクエリを実行していますが、2番目のクエリは最適でないLEFT結合ではなくINNER結合を使用しています。
この答え によると、Dapper.Netに組み込まれているマッピングサポートは1対多ではありません。クエリは、データベース行ごとに常に1つのオブジェクトを返します。ただし、代替ソリューションが含まれています。
GetHashCode
の代わりにFuncを使用して親キーを選択するAndrewの答えのわずかな修正。
public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
this IDbConnection connection,
string sql,
Func<TParent, TParentKey> parentKeySelector,
Func<TParent, IList<TChild>> childSelector,
dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();
connection.Query<TParent, TChild, TParent>(
sql,
(parent, child) =>
{
if (!cache.ContainsKey(parentKeySelector(parent)))
{
cache.Add(parentKeySelector(parent), parent);
}
TParent cachedParent = cache[parentKeySelector(parent)];
IList<TChild> children = childSelector(cachedParent);
children.Add(child);
return cachedParent;
},
param as object, transaction, buffered, splitOn, commandTimeout, commandType);
return cache.Values;
}
使用例
conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
これは大まかな回避策です
public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
var cache = new Dictionary<int, TOne>();
cnn.Query<TOne, TMany, TOne>(sql, (one, many) =>
{
if (!cache.ContainsKey(one.GetHashCode()))
cache.Add(one.GetHashCode(), one);
var localOne = cache[one.GetHashCode()];
var list = property(localOne);
list.Add(many);
return localOne;
}, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
return cache.Values;
}
決して最も効率的な方法というわけではありませんが、起動して実行できます。機会があれば、これを最適化してみます。
次のように使用します。
conn.Query<Product, Store>("sql here", prod => prod.Stores);
オブジェクトはGetHashCode
を実装する必要があることに留意してください。おそらく次のようになります。
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
別の方法を次に示します。
注文(1)-OrderDetail(多く)
using (var connection = new SqlCeConnection(connectionString))
{
var orderDictionary = new Dictionary<int, Order>();
var list = connection.Query<Order, OrderDetail, Order>(
sql,
(order, orderDetail) =>
{
Order orderEntry;
if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
{
orderEntry = order;
orderEntry.OrderDetails = new List<OrderDetail>();
orderDictionary.Add(orderEntry.OrderID, orderEntry);
}
orderEntry.OrderDetails.Add(orderDetail);
return orderEntry;
},
splitOn: "OrderDetailID")
.Distinct()
.ToList();
}
ソース: http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping- 1対多