我使用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
。
这是正确的方法吗?是否有任何努力在其他地方支持这个?
答案 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>();
}