2015-03-26 15 views
7

Sto utilizzando l'ereditarietà del database Tabella per gerarchia in cui le colonne per tutti i tipi derivati ​​sono in una singola tabella. Ogni tabella derivata viene identificato utilizzando un campo Discriminatore stringa che contiene il nome della classe derivata:Interrogazione modelli astratti 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; } 
} 

Come previsto, durante il recupero di questo metodo di interrogazione tramite di Dapper riceverò Instances of abstract classes cannot be created. Spero che questo restituisca un elenco di Animal con i loro valori come rispettivi tipi derivati.

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

I miei tentativi di aggiungere supporto per questo non sono riusciti. Prima SqlMapper.cs :: GetTypeDeserializer() viene chiamato se il tipo di essere passata in è una classe astratta poi sostituisco il tipo con quello restituito nel seguente metodo:

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"]); 
} 

Tuttavia sembra che a questo punto il il lettore non è stato aperto, quindi fallisce con Invalid attempt to read when no data is present.

È questo l'approccio corretto da prendere? C'è stato qualche sforzo per supportarlo altrove?

+0

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

risposta

3

È possibile eseguire questa operazione ma sarà meno efficiente rispetto all'utilizzo del comportamento predefinito di Dapper con tabelle separate.

GetDeserializer deve essere chiamato per ogni riga, il che significa che deve accadere all'interno while (reader.Read())

Modificando QueryImpl<T> è possibile ottenere il risultato desiderato. Supponendo che si sta ottenendo i risultati con:

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

Poi l'inizio del blocco di try {}QueryImpl<T> saranno:

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 

questo renderà il lavoro metodo solo con il campo discriminatore, quindi si può vuoi creare il tuo QueryImpl<T> se hai bisogno che funzioni normalmente con altre query. Inoltre non posso garantire che funzionerà in ogni caso, solo testato con due righe, una per tipo, ma questo dovrebbe essere un buon punto di partenza.

+0

Fantastico! Grazie per questo :) Anche se stavo usando l'ereditarietà di TPT con tabelle separate, avresti ancora un problema a interrogare i tipi astratti. Ho creato un problema su github se volevi pubblicare anche questo: https://github.com/StackExchange/dapper-dot-net/issues/262 – ajbeaven

+0

Sì, avresti ancora un problema con i tipi astratti, e la prestazione colpita con il codice precedente potrebbe essere trascurabile se si dispone di un set di dati di piccole dimensioni. – embee

2

Voglio condividere anche la mia soluzione. Ingressi:

C#

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

SQL

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

In SQL Ho una colonna Descriminator che determina C# tipo per ogni riga "netto" o "Bond". Fondamentalmente, questa è un'implementazione standard se la strategia Tabella per Gerarchia.

ho usato la sintassi di query paremeter-meno di Dapper

connection.Query(sql); 

per ottenere un oggetto che dynamic Dapper vede come DapperRow. Sebbene DapperRow sia una classe privata, implementa IDictionary<string, object>. String - nome di una proprietà, Object - valore delle proprietà.

Funzione Convert IDictionary<string, object> to class (fortemente tipizzato):

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; 
} 

E Mapper tra la colonna descriminator e C# classe:

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; 
    } 
} 

Uso del convertitore:

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 grazie mille per questo. Presumibilmente questo sarebbe abbastanza inefficiente quando si ha a che fare con un set di dati di grandi dimensioni, ma è bello avere qualcosa qui lo stesso. – ajbeaven