Ich habe diesen Code geschrieben, um eine bis viele Beziehungen zu projizieren, aber er funktioniert nicht:
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);
}
}
Kann jemand den Fehler erkennen?
BEARBEITEN:
Dies sind meine Entitäten:
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>();
}
}
BEARBEITEN:
Ich ändere die Abfrage in:
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");
und ich werde Ausnahmen los! Mitarbeiter werden jedoch überhaupt nicht zugeordnet. Ich bin mir immer noch nicht sicher, mit welchem Problem es bei der IEnumerable<Employee>
ersten Abfrage war.
Antworten:
Dieser Beitrag zeigt, wie Sie eine stark normalisierte SQL-Datenbank abfragen und das Ergebnis einer Reihe hoch verschachtelter C # POCO-Objekte zuordnen.
Zutaten:
Die Einsicht, die es mir ermöglichte, dieses Problem zu lösen, besteht darin, das
MicroORM
von zu trennenmapping the result back to the POCO Entities
. Daher verwenden wir zwei separate Bibliotheken:Im Wesentlichen verwenden wir Dapper , um die Datenbank abzufragen, und dann Slapper.Automapper , um das Ergebnis direkt in unsere POCOs abzubilden.
Vorteile
List<MyClass1>
der wiederum enthältList<MySubClass2>
, usw.).inner joins
, um flache Ergebnisse zurückzugeben, viel einfacher ist als das Erstellen mehrerer select-Anweisungen mit Stitching auf der Clientseite.Nachteile
inner join
(wodurch Duplikate zurückgebracht werden), sondern stattdessen mehrereselect
Anweisungen verwenden und alles wieder zusammenfügen Client-Seite (siehe die anderen Antworten auf dieser Seite).Leistungstest
In meinen Tests hat Slapper.Automapper den von Dapper zurückgegebenen Ergebnissen einen kleinen Overhead hinzugefügt, was bedeutete, dass es immer noch 10x schneller als Entity Framework war und die Kombination immer noch ziemlich nahe an der theoretischen Höchstgeschwindigkeit liegt, zu der SQL + C # fähig ist .
In den meisten praktischen Fällen würde der größte Teil des Overheads in einer nicht optimalen SQL-Abfrage liegen und nicht mit einer gewissen Zuordnung der Ergebnisse auf der C # -Seite.
Leistungstestergebnisse
Gesamtzahl der Iterationen: 1000
Dapper by itself
: 1,889 Millisekunden pro Abfrage mit3 lines of code to return the dynamic
.Dapper + Slapper.Automapper
: 2,463 Millisekunden pro Abfrage unter Verwendung einer zusätzlichen3 lines of code for the query + mapping from dynamic to POCO Entities
.Gearbeitetes Beispiel
In diesem Beispiel haben wir eine Liste von
Contacts
und jederContact
kann eine oder mehrere habenphone numbers
.POCO-Einheiten
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; } }
SQL-Tabelle
TestContact
SQL-Tabelle
TestPhone
Beachten Sie, dass diese Tabelle einen Fremdschlüssel hat,
ContactID
der sich auf dieTestContact
Tabelle bezieht (dies entspricht demList<TestPhone>
im obigen POCO).SQL, das ein flaches Ergebnis erzeugt
In unserer SQL-Abfrage verwenden wir so viele
JOIN
Anweisungen, wie wir benötigen, um alle benötigten Daten in einer flachen, denormalisierten Form abzurufen . Ja, dies kann zu Duplikaten in der Ausgabe führen, aber diese Duplikate werden automatisch entfernt, wenn wir Slapper.Automapper verwenden , um das Ergebnis dieser Abfrage automatisch direkt in unsere POCO-Objektzuordnung abzubilden.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
C # -Code
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); } } } }
Ausgabe
POCO-Entitätshierarchie
In Visual Studio können wir sehen, dass Slapper.Automapper unsere POCO-Entitäten ordnungsgemäß ausgefüllt hat, dh wir haben eine
List<TestContact>
und jedeTestContact
hat eineList<TestPhone>
.Anmerkungen
Sowohl Dapper als auch Slapper.Automapper speichern alles intern, um die Geschwindigkeit zu erhöhen. Wenn Sie auf Speicherprobleme stoßen (sehr unwahrscheinlich), stellen Sie sicher, dass Sie gelegentlich den Cache für beide leeren.
Stellen Sie sicher, dass Sie die zurückkommenden Spalten mit der Unterstrich-
_
Notation ( ) benennen, um Slapper.Automapper Hinweise zu geben, wie das Ergebnis den POCO-Entitäten zugeordnet werden kann.Stellen Sie sicher, dass Sie Slapper.Automapper-Hinweise zum Primärschlüssel für jede POCO-Entität geben (siehe Zeilen
Slapper.AutoMapper.Configuration.AddIdentifiers
). Sie können dies auchAttributes
auf dem POCO verwenden. Wenn Sie diesen Schritt überspringen, kann dies (theoretisch) schief gehen, da Slapper.Automapper nicht weiß, wie das Mapping ordnungsgemäß ausgeführt wird.Update 14.06.2015
Diese Technik wurde erfolgreich auf eine riesige Produktionsdatenbank mit über 40 normalisierten Tabellen angewendet. Es funktionierte perfekt eine erweiterte SQL - Abfrage mit mehr als 16 abzubilden
inner join
undleft join
in die richtige POCO Hierarchie (mit 4 Ebenen der Verschachtelung). Die Abfragen sind unglaublich schnell, fast so schnell wie die manuelle Codierung in ADO.NET (normalerweise waren es 52 Millisekunden für die Abfrage und 50 Millisekunden für die Zuordnung vom flachen Ergebnis zur POCO-Hierarchie). Dies ist wirklich nichts Revolutionäres, aber es übertrifft Entity Framework in Bezug auf Geschwindigkeit und Benutzerfreundlichkeit, insbesondere wenn wir nur Abfragen ausführen.Update 19.02.2016
Code läuft seit 9 Monaten einwandfrei in der Produktion. Die neueste Version von
Slapper.Automapper
enthält alle Änderungen, die ich vorgenommen habe, um das Problem zu beheben, das mit der Rückgabe von Nullen in der SQL-Abfrage zusammenhängt.Update 2017-02-20
Code läuft seit 21 Monaten einwandfrei in der Produktion und hat fortlaufende Anfragen von Hunderten von Benutzern in einem FTSE 250-Unternehmen bearbeitet.
Slapper.Automapper
eignet sich auch hervorragend zum Zuordnen einer CSV-Datei direkt zu einer Liste von POCOs. Lesen Sie die CSV-Datei in eine IDictionary-Liste und ordnen Sie sie dann direkt der Zielliste der POCOs zu. Der einzige Trick besteht darin, dass Sie eine Eigenschaft hinzufügenint Id {get; set}
und sicherstellen müssen , dass sie für jede Zeile eindeutig ist (andernfalls kann der Automapper nicht zwischen den Zeilen unterscheiden).Update 2019-01-29
Kleinere Aktualisierung, um weitere Codekommentare hinzuzufügen.
Siehe: https://github.com/SlapperAutoMapper/Slapper.AutoMapper
quelle
Ich wollte es so einfach wie möglich halten, meine Lösung:
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(); }
Ich rufe immer noch einmal die Datenbank auf, und während ich jetzt 2 Abfragen anstelle von einer ausführe, verwendet die zweite Abfrage einen INNER-Join anstelle eines weniger optimalen LEFT-Joins.
quelle
.Join(
, erzeugt jedoch ein Objektdiagramm anstelle eines abgeflachten Ergebnisses.Eine geringfügige Änderung von Andrews Antwort, bei der ein Func verwendet wird, um den übergeordneten Schlüssel anstelle von auszuwählen
GetHashCode
.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; }
Anwendungsbeispiel
conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
quelle
class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
Nach dieser Antwort ist in Dapper.Net niemand zu viele Mapping-Unterstützung integriert. Abfragen geben immer ein Objekt pro Datenbankzeile zurück. Es gibt jedoch eine alternative Lösung.
quelle
(contact, phones) => { contact.Phones = phones; }
Ich müsste einen Filter für Telefone schreiben, deren Kontakt-ID mit der Kontakt-ID des Kontakts übereinstimmt. Das ist ziemlich ineffizient.Hier ist eine grobe Problemumgehung
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; }
Es ist keineswegs der effizienteste Weg, aber es wird Sie zum Laufen bringen. Ich werde versuchen, dies zu optimieren, wenn ich eine Chance bekomme.
benutze es so:
conn.Query<Product, Store>("sql here", prod => prod.Stores);
Denken Sie daran, dass Ihre Objekte implementiert werden müssen
GetHashCode
, vielleicht so:public override int GetHashCode() { return this.Id.GetHashCode(); }
quelle
Hier ist eine andere Methode:
Order (one) - OrderDetail (viele)
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(); }
Quelle : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many
quelle