在精巧的时候查询抽象模型

时间:2015-03-26 01:11:28

标签: c# inheritance dapper table-per-hierarchy

我使用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 个答案:

答案 0 :(得分: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>。此外,我不能保证这会在每种情况下都有效,只测试两行,每种类型一个 - 但这应该是一个很好的起点。

答案 1 :(得分:3)

我也希望分享我的解决方案。输入:

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#类型&#34; Equity&#34;或者&#34;邦德&#34;。 Basicaly,这是一个标准的实现,如果每层次表策略。

我使用了Dapper的无承载查询语法

connection.Query(sql); 

获取Dapper视为DapperRow的dynamic对象。尽管DapperRow是一个私有类,但它实现了IDictionary<string, object>. String - 属性的名称,Object - 属性值。

功能Convert IDictionary<string, object> to class(强类型):

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

在descriminator列和C#类之间使用Mapper:

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

答案 2 :(得分:1)

创建了通用的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;
    }

答案 3 :(得分:0)

对于 EFCore 中的类似问题 - How to Automatically Map TPH Derived Classes in 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;
    }
}

这是调用它的代码。如果派生类中不存在表中的Discriminator,则会抛出上述异常。

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

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