Richtige Verwendung von Multimapping in Dapper

111

Ich versuche, die Multimapping-Funktion von dapper zu verwenden, um eine Liste der ProductItems und der zugehörigen Kunden zurückzugeben.

[Table("Product")]
public class ProductItem
{
    public decimal ProductID { get; set; }        
    public string ProductName { get; set; }
    public string AccountOpened { get; set; }
    public Customer Customer { get; set; }
} 

public class Customer
{
    public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}

Mein adretter Code lautet wie folgt

var sql = @"select * from Product p 
            inner join Customer c on p.CustomerId = c.CustomerId 
            order by p.ProductName";

var data = con.Query<ProductItem, Customer, ProductItem>(
    sql,
    (productItem, customer) => {
        productItem.Customer = customer;
        return productItem;
    },
    splitOn: "CustomerId,CustomerName"
);

Dies funktioniert einwandfrei, aber ich muss anscheinend die vollständige Spaltenliste zum Parameter splitOn hinzufügen, um alle Kundeneigenschaften zurückzugeben. Wenn ich "CustomerName" nicht hinzufüge, wird null zurückgegeben. Verstehe ich die Kernfunktionalität der Multimapping-Funktion nicht? Ich möchte nicht jedes Mal eine vollständige Liste der Spaltennamen hinzufügen müssen.

Richard Forrest
quelle
Wie können Sie dann tatsächlich beide Tabellen in der Datagrid-Ansicht anzeigen? Ein kleines Beispiel wird sehr geschätzt.
Ankur Soni

Antworten:

184

Ich habe gerade einen Test durchgeführt, der gut funktioniert:

var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(1 as decimal) CustomerId, 'name' CustomerName";

var item = connection.Query<ProductItem, Customer, ProductItem>(sql,
    (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();

item.Customer.CustomerId.IsEqualTo(1);

Der splitOn-Parameter muss als Split-Punkt angegeben werden. Der Standardwert ist Id. Wenn mehrere Teilungspunkte vorhanden sind, müssen Sie diese in eine durch Kommas getrennte Liste einfügen.

Angenommen, Ihr Recordset sieht folgendermaßen aus:

ProductID | Produktname | AccountOpened | Kunden-ID | Kundenname
--------------------------------------- ----------- --------------

Dapper muss wissen, wie die Spalten in dieser Reihenfolge in zwei Objekte aufgeteilt werden. Ein flüchtiger Blick zeigt , dass der Kunde beginnt an der Säule CustomerId, also splitOn: CustomerId.

Hier gibt es eine große Einschränkung, wenn die Spaltenreihenfolge in der zugrunde liegenden Tabelle aus irgendeinem Grund umgedreht wird:

ProductID | Produktname | AccountOpened | Kundenname | Kundennummer  
--------------------------------------- ----------- --------------

splitOn: CustomerId führt zu einem Null-Kundennamen.

Wenn Sie CustomerId,CustomerNameals Teilungspunkte angeben , geht dapper davon aus, dass Sie versuchen, die Ergebnismenge in drei Objekte aufzuteilen. Der erste beginnt am Anfang, der zweite beginnt am CustomerId, der dritte am CustomerName.

Sam Safran
quelle
2
Danke Sam. Ja, Sie haben Recht, es war die Rückgabereihenfolge der Spalten, die das Problem mit CustomerName | war Zurückgegebene Kunden-ID Kundenname kam auf null zurück.
Richard Forrest
18
Eine Sache, an die Sie sich erinnern sollten, ist, dass Sie keine Leerzeichen in der Zeichenfolge haben können spliton, dh CustomerId,CustomerNamenicht CustomerId, CustomerName, da Dapper Trimdie Ergebnisse der Zeichenfolgenaufteilung nicht berücksichtigt . Es wird nur der generische Spliton-Fehler ausgelöst. Hat mich eines Tages verrückt gemacht.
Jesus
2
@vaheeds Sie sollten IMMER Spaltennamen verwenden und niemals einen Stern verwenden. Dies gibt SQL weniger Arbeit und Sie erhalten keine Situationen, in denen die Spaltenreihenfolge falsch ist, wie in diesem Fall.
Harag
3
@vaheeds - In Bezug auf die ID, ID, ID, die den Dapper-Code betrachtet, wird nicht zwischen Groß- und Kleinschreibung unterschieden, und es wird auch der Text für splitOn gekürzt. Dies ist Version 1.50.2.0 von Dapper.
Harag
2
Wenn Sie sich fragen, ob Sie eine Abfrage in drei Objekte aufteilen müssen: Stellen Sie in einer Spalte mit dem Namen "Id" und in einer Spalte mit dem Namen "SomethingId" sicher, dass die erste "Id" in der Split-Klausel enthalten ist. Obwohl Dapper standardmäßig auf "Id" aufgeteilt wird, muss es in diesem Fall explizit festgelegt werden.
Sbu
27

Unsere Tabellen haben einen ähnlichen Namen wie Ihre, wobei so etwas wie "CustomerID" möglicherweise zweimal mit einer Operation "select *" zurückgegeben wird. Daher macht Dapper seine Arbeit, teilt sich aber (möglicherweise) zu früh auf, weil die Spalten wie folgt lauten würden:

(select * might return):
ProductID,
ProductName,
CustomerID, --first CustomerID
AccountOpened,
CustomerID, --second CustomerID,
CustomerName.

Dies macht den spliton: -Parameter nicht so nützlich, insbesondere wenn Sie nicht sicher sind, in welcher Reihenfolge die Spalten zurückgegeben werden. Natürlich können Sie Spalten manuell angeben ... aber es ist 2017 und wir tun dies nur noch selten für grundlegende Objektabrufe.

Was wir tun und es hat viele, viele Jahre lang für Tausende von Abfragen hervorragend funktioniert, ist einfach einen Alias ​​für die ID zu verwenden und niemals Spliton anzugeben (unter Verwendung der Standard-ID von Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

... voila! Dapper teilt standardmäßig nur die ID auf, und diese ID wird vor allen Spalten des Kunden angezeigt. Natürlich wird Ihrer Rückgabe-Ergebnismenge eine zusätzliche Spalte hinzugefügt, aber das ist ein äußerst minimaler Aufwand für das zusätzliche Dienstprogramm, genau zu wissen, welche Spalten zu welchem ​​Objekt gehören. Und Sie können dies leicht erweitern. Benötigen Sie Adress- und Länderinformationen?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Das Beste ist, dass Sie in einer minimalen Menge von SQL deutlich zeigen, welche Spalten welchem ​​Objekt zugeordnet sind. Dapper erledigt den Rest.

BlackjacketMack
quelle
Dies ist ein prägnanter Ansatz, solange keine Tabelle über ID-Felder verfügt.
Bernard Vander Beken
Bei diesem Ansatz kann eine Tabelle immer noch ein ID-Feld haben ... aber es sollte die PK sein. Sie müssten den Alias ​​einfach nicht erstellen, damit es tatsächlich etwas weniger Arbeit gibt. (Ich würde es für sehr ungewöhnlich halten (schlechte Form?), Eine Spalte namens 'Id' zu haben, die nicht die PK ist.)
BlackjacketMack
5

Angenommen, die folgende Struktur ist '|' ist der Punkt der Aufteilung und Ts sind die Entitäten, auf die das Mapping angewendet werden soll.

       TFirst         TSecond         TThird           TFourth
------------------+-------------+-------------------+------------
col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8
------------------+-------------+-------------------+------------

Es folgt die adrette Abfrage, die Sie schreiben müssen.

Query<TFirst, TSecond, TThird, TFourth, TResut> (
    sql : query,
    map: Func<TFirst, TSecond, TThird, TFourth, TResut> func,
    parma: optional,
    splitOn: "col_3, col_n, col_A, col_9")

Wir möchten also, dass TFirst col_1 col_2 col_3 abbildet, für TSecond das col_n col_m ...

Der splitOn-Ausdruck bedeutet:

Starten Sie die Zuordnung aller Spalten zu TFrist, bis Sie eine Spalte mit dem Namen oder Alias ​​'col_3' finden, und fügen Sie auch 'col_3' in das Zuordnungsergebnis ein.

Starten Sie dann die Zuordnung in TSecond alle Spalten ab 'col_n' und setzen Sie die Zuordnung fort, bis ein neues Trennzeichen gefunden wird. In diesem Fall ist dies 'col_A' und markiert den Beginn der TThird-Zuordnung und so weiter.

Die Spalten der SQL-Abfrage und die Requisiten des Zuordnungsobjekts stehen in einer 1: 1-Beziehung (was bedeutet, dass sie den gleichen Namen haben sollten). Wenn die Spaltennamen, die sich aus der SQL-Abfrage ergeben, unterschiedlich sind, können Sie sie mit 'AS [aliasen Some_Alias_Name] 'Ausdruck.

Boris
quelle
2

Es gibt noch eine Einschränkung. Wenn das Feld CustomerId null ist (normalerweise bei Abfragen mit Linksverknüpfung), erstellt Dapper ProductItem mit Customer = null. Im obigen Beispiel:

var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(null as decimal) CustomerId, 'n' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql, (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
Debug.Assert(item.Customer == null); 

Und noch eine Einschränkung / Falle. Wenn Sie das in splitOn angegebene Feld nicht zuordnen und dieses Feld null enthält, erstellt und füllt Dapper das zugehörige Objekt (in diesem Fall Kunde). Um zu demonstrieren, verwenden Sie diese Klasse mit vorherigem SQL:

public class Customer
{
    //public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}
...
Debug.Assert(item.Customer != null);
Debug.Assert(item.Customer.CustomerName == "n");  
Frantisek Bachan
quelle
Gibt es eine Lösung für das zweite Beispiel neben dem Hinzufügen der Kunden-ID zur Klasse? Ich habe ein Problem, bei dem ich ein Nullobjekt benötige, aber es gibt mir ein leeres Objekt. ( stackoverflow.com/questions/27231637/… )
jmzagorski
1

Ich mache das generisch in meinem Repo, funktioniert gut für meinen Anwendungsfall. Ich dachte, ich würde teilen. Vielleicht wird jemand dies weiter ausbauen.

Einige Nachteile sind:

  • Dies setzt voraus, dass Ihre Fremdschlüsseleigenschaften der Name Ihres untergeordneten Objekts + "Id" sind, z. B. UnitId.
  • Ich habe es nur 1 untergeordnetes Objekt dem übergeordneten Objekt zugeordnet.

Der Code:

    public IEnumerable<TParent> GetParentChild<TParent, TChild>()
    {
        var sql = string.Format(@"select * from {0} p 
        inner join {1} c on p.{1}Id = c.Id", 
        typeof(TParent).Name, typeof(TChild).Name);

        Debug.WriteLine(sql);

        var data = _con.Query<TParent, TChild, TParent>(
            sql,
            (p, c) =>
            {
                p.GetType().GetProperty(typeof (TChild).Name).SetValue(p, c);
                return p;
            },
            splitOn: typeof(TChild).Name + "Id");

        return data;
    }
Dylan Hayes
quelle
0

Wenn Sie eine große Entität schreiben müssen, muss jedes Feld eine schwierige Aufgabe sein.

Ich habe versucht, @BlackjacketMack zu beantworten, aber eine meiner Tabellen hat eine ID-Spalte, andere nicht (ich weiß, es ist ein DB-Design-Problem, aber ...), dann wird eine zusätzliche Aufteilung auf dapper eingefügt, deshalb

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Funktioniert bei mir nicht Dann habe ich mit einer kleinen Änderung dieser beendet ist , legen Sie einfach eine Split - Punkt mit einem Namen, der mit jedem Feld auf Tabellen nicht übereinstimmt, kann in geändert Fall as Iddurch as _SplitPoint_, die letzten SQL - Skript sieht wie folgt aus :

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Fügen Sie dann in dapper nur einen splitOn hinzu

cmd =
    "SELECT Materials.*, " +
    "   Product.ItemtId as _SplitPoint_," +
    "   Product.*, " +
    "   MeasureUnit.IntIdUM as _SplitPoint_, " +
    "   MeasureUnit.* " +
    "FROM   Materials INNER JOIN " +
    "   Product ON Materials.ItemtId = Product.ItemtId INNER JOIN " +
    "   MeasureUnit ON Materials.IntIdUM = MeasureUnit.IntIdUM " +
List < Materials> fTecnica3 = (await dpCx.QueryAsync<Materials>(
        cmd,
        new[] { typeof(Materials), typeof(Product), typeof(MeasureUnit) },
        (objects) =>
        {
            Materials mat = (Materials)objects[0];
            mat.Product = (Product)objects[1];
            mat.MeasureUnit = (MeasureUnit)objects[2];
            return mat;
        },
        splitOn: "_SplitPoint_"
    )).ToList();
Juan Pablo Gomez
quelle