Запрос абстрактных моделей в dapper

Я использую наследование базы данных по иерархии таблиц, где столбцы для всех производных типов находятся в одной таблице. Каждая производная таблица идентифицируется с помощью строкового поля Discriminator, которое содержит имя производного класса:

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

Как и ожидалось, при получении этого с помощью метода запроса Dapper я получаю Instances of abstract classes cannot be created. Я надеюсь, что это вернет список Animal с их значениями, являющимися соответствующими производными типами.

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

Мои попытки добавить поддержку для этого не увенчались успехом. Перед вызовом SqlMapper.cs :: GetTypeDeserializer (), если передаваемый тип является абстрактным классом, я заменяю тип на тип, возвращенный следующим методом:

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

Однако похоже, что на данный момент программа чтения еще не открыта, поэтому она не работает с Invalid attempt to read when no data is present.

Это правильный подход? Были ли какие-либо усилия поддержать это где-то еще?


person ajbeaven    schedule 26.03.2015    source источник
comment
github.com/StackExchange/dapper-dot-net/issues/262   -  person ajbeaven    schedule 20.10.2016


Ответы (4)


Вы можете выполнить эту работу, но это будет менее эффективно, чем использование поведения Dapper по умолчанию с отдельными таблицами.

GetDeserializer необходимо вызывать для каждой строки, что означает, что это должно происходить внутри while (reader.Read())

Изменяя QueryImpl<T>, вы можете добиться желаемого результата. Предполагая, что вы получаете результаты с:

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

Тогда начало блока try {} из QueryImpl<T> будет:

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

Это заставит метод работать только с полем дискриминатора, поэтому вы можете создать свой собственный QueryImpl<T>, если вам нужно, чтобы это нормально работало с другими запросами. Также я не могу гарантировать, что это будет работать в каждом случае, проверено только с двумя строками, по одной каждого типа, но это должно быть хорошей отправной точкой.

person embee    schedule 30.03.2015
comment
Потрясающие! Спасибо за это :) Даже если бы я использовал наследование TPT с отдельными таблицами, у вас все равно была бы проблема с запросом абстрактных типов. Я создал проблему на github, если вы тоже хотели разместить это там: github .com / StackExchange / dapper-dot-net / issues / 262. - person ajbeaven; 30.03.2015
comment
Да, у вас все еще будет проблема с абстрактными типами, и снижение производительности с помощью приведенного выше кода может быть незначительным, если у вас небольшой набор данных. - person embee; 31.03.2015

Я тоже хочу поделиться своим решением. Входы:

C#

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

SQL

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

В SQL у меня есть столбец Descriminator, который определяет тип C # для каждой строки «Equity» или «Bond». По сути, это стандартная реализация стратегии Table-Per-Hierarchy.

Я использовал синтаксис запросов Dapper без параметров

connection.Query(sql); 

чтобы получить объект dynamic, который Dapper видит как DapperRow. Хотя DapperRow является частным классом, он реализует IDictionary<string, object>. String - имя свойства, Object - значение свойства.

Функция Преобразовать строку IDictionary ‹, объект› в класс (строго типизированный):

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

И сопоставление между столбцом дескриминатора и классом C #:

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

Использование конвертера:

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;
}
person user3526723    schedule 09.06.2017
comment
+1 за это большое спасибо. По-видимому, это было бы довольно неэффективно при работе с большим набором данных, но все же хорошо иметь что-то здесь. - person ajbeaven; 09.06.2017

Создан универсальный метод расширения dapper для запроса иерархии классов для каждой таблицы. Возможно, будет кому-то полезно.

    public static async Task<IEnumerable<TValue>> QueryHierarchyAsync<TValue, TKey>(
        this IDbConnection connection,
        CommandDefinition command,
        string discriminator,
        Func<TKey, Type> typeProvider)
    {
        int discriminatorIndex = -1;
        var parsers = new Dictionary<TKey, Func<IDataReader, TValue>>();

        var result = new List<TValue>();

        using (var reader = await connection.ExecuteReaderAsync(command))
        {
            while (reader.Read())
            {
                if (discriminatorIndex < 0) discriminatorIndex = reader.GetOrdinal(discriminator);
                var objectValue = reader.GetValue(discriminatorIndex);
                if (!(objectValue is TKey value))
                    throw new Exception($"Discriminator value is not assignable to '{typeof(TKey).Name}'");

                if (!parsers.TryGetValue(value, out var parser))
                {
                    var type = typeProvider(value);
                    if (type == null)
                        throw new Exception($"Type for discriminator value '{value}' was not found");

                    if (!typeof(TValue).IsAssignableFrom(type))
                        throw new Exception($"Type '{type.Name}' is not assignable from '{typeof(TValue).Name}'");

                    parser = reader.GetRowParser<TValue>(type);
                    parsers.Add(value, parser);
                }

                result.Add(parser(reader));
            }
        }

        return result;
    }
person Oleg Bevz    schedule 18.11.2020
comment
Можете ли вы добавить пример того, как вызвать метод, в частности, как выглядит typeProvider. - person WillC; 14.02.2021

Для аналогичной проблемы в EFCore - Как автоматически сопоставить TPH Производные классы в EF Core? я придумал этот метод расширения, который получает производные подклассы (обычно абстрактного) класса.

 public static Type[] GetDerivedClasses(this Type type, string[] ignoreTypeNames = null) 
{
   ignoreTypeNames = ignoreTypeNames ?? new string[0];

   return Assembly.GetAssembly(type)
                  .GetTypes()
                  .Where
                   (
                      t => t.IsSubclassOf(type) &&
                      (!ignoreTypeNames?.Any(t.Name.Contains) ?? false)
                   )
                  .OrderBy(o => o.Name)
                  .ToArray();
}

Имея список подтипов, производных от базового типа, вы можете создать словарь всех синтаксических анализаторов подтипов для базового класса. Вот метод расширения, который возвращает типизированные записи для любого подтипа без необходимости вручную отображать их. Для моей системы правил это было гораздо более простое решение.

public static List<T> MapSubClassesOf<T>(this IDataReader reader, string discriminator = "Discriminator")
{
    var list = new List<T>();
    var derivedTypes = typeof(T).GetDerivedTypes();
    var parsers = derivedTypes.ToDictionary(s => s.Name, s => reader.GetRowParser<T>(s));

    while (reader.Read())
    {
        string typeName = reader.GetString(reader.GetOrdinal(discriminator));

        if (!parsers.ContainsKey(typeName))
            throw new Exception($"Discriminator value '{typeName}' in the database table is not a valid subType of {typeof(T).Name}.");

            var subType = parsers[typeName](reader);

            list.Add(subType);
        }
        return list;
    }
}

Вот код для его вызова. Это вызовет исключение, указанное выше, если дискриминатор в таблице не существует в производных классах.

string sql = @"SELECT SymbolRuleId, SortOrder, RuleGroup,
                      Discriminator, Description
               FROM   SymbolRules";

using (var reader = GetConnection().ExecuteReader(sql) )
{
    return reader.MapSubClassesOf<SymbolRule>();
}
person WillC    schedule 14.02.2021