2015-03-26 6 views
7

Ich verwende Tabellenvererbung für die Datenbankvererbung, wobei Spalten für alle abgeleiteten Typen in einer einzigen Tabelle sind. Jede abgeleitete Tabelle identifiziert eine Zeichenfolge Diskriminatorfilter Feld verwenden, das den Namen der abgeleiteten Klasse gilt:Abfrage abstrakter Modelle in Dapper

--------------------- 
| tanimal   | 
--------------------- 
| animalid   | 
| discriminator  | 
| furcolour   | 
| feathercolour  | 
--------------------- 

public abstract class Animal 
{ 
    public int AnimalId { get; set; } 
    public string Discriminator { get { return GetType().Name; } } 
} 

public class Bird : Animal 
{ 
    public string FeatherColour { get; set; } 
} 

public class Dog : Animal 
{ 
    public string FurColour { get; set; } 
} 

Wie erwartet, wenn diese über Dapper Abfrage Methode Abrufen I erhalten Instances of abstract classes cannot be created. Ich würde hoffen, dass dies eine Liste von Tier mit ihren Werten zurückgeben würde, die die jeweiligen abgeleiteten Typen sind.

var animals = Connection.Query<Animal>("SELECT * FROM tanimal") 

Meine Versuche, Unterstützung dafür hinzuzufügen, waren nicht erfolgreich. Vor SqlMapper.cs :: GetTypeDeserializer() aufgerufen wird, wenn der Typ in eine abstrakte Klasse ist geben wird dann ersetze ich die Art mit dem in der folgenden Methode zurückgegeben:

static Type GetDerivedType(Type abstractType, IDataReader reader) 
{ 
    var discriminator = abstractType.GetProperty("Discriminator"); 
    if (discriminator == null) 
     throw new InvalidOperationException("Cannot create instance of abstract class " + abstractType.FullName + ". To allow dapper to map to a derived type, add a Discriminator field that stores the name of the derived type"); 

    return Type.GetType((string)reader["Discriminator"]); 
} 

Doch es wie die an diesem Punkt sieht Leser wurde nicht geöffnet, so dass es mit Invalid attempt to read when no data is present fehlschlägt.

Ist dies der richtige Ansatz zu nehmen? Gab es irgendwelche Anstrengungen, dies anderswo zu unterstützen?

+0

https: // GitHub. com/StackExchange/dapper-dot-net/issues/262 – ajbeaven

Antwort

3

Sie können dies durchführen, aber es wird weniger effizient sein als die Verwendung von Dappers Standardverhalten mit separaten Tabellen.

GetDeserializer Bedürfnisse für jede Zeile aufgerufen werden, was bedeutet, es Durch Modifizieren QueryImpl<T> innerhalb while (reader.Read())

muss geschehen können Sie das gewünschte Ergebnis erzielen. Angenommen, Sie bekommen die Ergebnisse mit:

var results = connection.Query<Animal>("SELECT * FROM tanimal"); 

Dann wird der Beginn des try {} Block QueryImpl<T> wird:

try 
{ 
cmd = command.SetupCommand(cnn, info.ParamReader); 

if (wasClosed) cnn.Open(); 

// We can't use SequentialAccess any more - this will have a performance hit. 
reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); 
wasClosed = false; 

// You'll need to make sure your typePrefix is correct to your type's namespace 
var assembly = Assembly.GetExecutingAssembly(); 
var typePrefix = assembly.GetName().Name + "."; 

while (reader.Read()) 
{ 
    // This was already here 
    if (reader.FieldCount == 0) //https://code.google.com/p/dapper-dot-net/issues/detail?id=57 
     yield break; 

    // This has been moved from outside the while 
    int hash = GetColumnHash(reader); 

    // Now we're creating a new DeserializerState for every row we read 
    // This can be made more efficient by caching and re-using for matching types 
    var discriminator = reader["discriminator"].ToString(); 
    var convertToType = assembly.GetType(typePrefix + discriminator); 

    var tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(convertToType, reader, 0, -1, false)); 
    if (command.AddToCache) SetQueryCache(identity, info); 

    // The rest is the same as before except using our type in ChangeType 
    var func = tuple.Func; 

    object val = func(reader); 
    if (val == null || val is T) 
    { 
     yield return (T)val; 
    } 
    else 
    { 
     yield return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); 
    } 
} 
// The rest of this method is the same 

Dies wird die Methode funktioniert nur mit dem Unterscheidungsfeld machen, so können Sie Möchten Sie Ihre eigenen QueryImpl<T> erstellen, wenn Sie dies normalerweise mit anderen Abfragen arbeiten möchten. Auch kann ich nicht garantieren, dass dies in jedem Fall funktioniert, nur getestet mit zwei Reihen, eine von jedem Typ - aber das sollte ein guter Ausgangspunkt sein.

+0

Super! Danke dafür :) Auch wenn ich die TPT-Vererbung mit separaten Tabellen verwendete, würde es immer noch Probleme geben, abstrakte Typen abzufragen. Ich habe ein Problem auf GitHub erstellt, wenn Sie dieses auch hier posten möchten: https://github.com/StackExchange/dapper-dot-net/issues/262 – ajbeaven

+0

Ja, Sie hätten immer noch ein Problem mit den abstrakten Typen, und die Leistung, die mit dem obigen Code erzielt wird, kann vernachlässigbar sein, wenn Sie einen kleinen Datensatz haben. – embee

2

Ich möchte meine Lösung auch teilen. Eingänge:

C#

abstract class Stock {} 
class Bond: Stock {} 
class Equity : Stock {} 

SQL

CREATE TABLE [dbo].[Stocks] (
....some columns.... 
    [Descriminator] VARCHAR (100) NOT NULL, 
); 

In SQL habe ich eine Descriminator Säule, die C# Typen für jede Zeile "Aktien" oder "Bond" bestimmt. Grundsätzlich ist dies eine Standardimplementierung für eine Tabelle-für-die-Hierarchie-Strategie.

I verwendet Dapper des paremeter lose Abfragesyntax

connection.Query(sql); 

ein dynamic Objekt zu erhalten, die Dapper als DapperRow sieht. Obwohl DapperRow eine private Klasse ist, implementiert sie IDictionary<string, object>. String - Name einer Eigenschaft, Object - Eigenschaftswert.

Funktion Convert IDictionary<string, object> to class (stark typisierte):

public static T GetObject<T>(IDictionary<string, object> dict) 
{ 
    Type type = typeof(T); 
    var obj = Activator.CreateInstance(type); 

    foreach (var kv in dict) 
    { 
     type.GetProperty(kv.Key).SetValue(obj, kv.Value); 
    } 
    return (T)obj; 
} 

Und Mapper zwischen descriminator Säule und C# Klasse:

public static Stock ConvertToStock(object value) 
{ 
    var dapperRowProperties = value as IDictionary<string, object>; 
    switch (dapperRowProperties["Descriminator"]) 
    { 
     case "Bond": 
      return GetObject<Bond>(dapperRowProperties); 
     case "Stock": 
      return GetObject<Stock>(dapperRowProperties); 
     default: 
      return null; 
    } 
} 

Verwendung des Konverters:

public Stock GetStock(int id) 
{ 
    Stock stock; 
    var sql = "select * from Stocks where Id = @id"; 
    using (var connection = ConnectionFactory.GetOpenConnection()) 
    { 
     stock = connection.Query(sql, new { id }).Select(ConvertToStock).Single(); 
    } 
    return stock; 
} 
+1

+1 vielen Dank dafür. Vermutlich wäre dies bei großen Datenmengen ziemlich ineffizient, aber es ist gut, hier etwas zu haben. – ajbeaven