9

我正在使用 Table Per Hierarchy 数据库继承,其中所有派生类型的列都在一个表中。每个派生表都使用包含派生类名称的字符串鉴别器字段进行标识:

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

4 回答 4

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>如果您需要此方法与其他查询正常工作,您可能需要创建自己的方法。此外,我不能保证这在每种情况下都有效,只测试了两行,每种类型一个 - 但这应该是一个很好的起点。

于 2015-03-30T18:21:55.077 回答
4

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

C#

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

SQL

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

在 SQL 中,我有一个描述器列,它为每一行“Equity”或“Bond”确定 C# 类型。基本上,这是 Table-Per-Hierarchy 策略的标准实现。

我使用了 Dapper 的无参数查询语法

connection.Query(sql); 

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

函数将 IDictionary<string, object> 转换为类 (强类型):

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;
}
于 2017-06-09T12:08:44.247 回答
2

创建通用 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;
    }
于 2020-11-18T08:20:58.427 回答
0

对于 EFCore 中的类似问题 -如何在 EF Core 中自动映射 TPH 派生类?,我想出了这个扩展方法,它获取一个(通常是抽象的)类的派生子类。

 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>();
}
于 2021-02-14T19:08:07.693 回答