Опрос абстрактных моделей в dapper
Я использую наследование базы данных Table Per Hierarchy, где столбцы для всех производных типов находятся в одной таблице. Каждая производная таблица идентифицируется с использованием строкового поля 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
,
Это правильный подход? Были ли какие-либо попытки поддержать это в другом месте?
4 ответа
Я тоже хочу поделиться своим решением. Входы:
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.
Я использовал синтаксис Query без параметров
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;
}
И Mapper между дескрипторным столбцом и классом 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;
}
Вы можете сделать это, но это будет менее эффективно, чем использование поведения 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>
если вам нужно это нормально работать с другими запросами. Также я не могу гарантировать, что это будет работать в каждом случае, только протестировано с двумя строками, по одному для каждого типа - но это должно быть хорошей отправной точкой.
Создан универсальный метод расширения 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;
}
Для аналогичной проблемы в 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;
}
}