Получить только базовый класс из Entity Framework
Если у меня есть три класса в инфраструктуре сущности.
class Base {}
class Left : Base {}
class Right : Base {}
и я вызываю DBContext.Bases.ToList();
Это возвращает все экземпляры Base
, полностью введенные в их ассоциированные унаследованные типы, как заметили некоторые люди, производительность EF на больших структурах наследования невелика, если не сказать больше. Мой фактический запрос в моем проекте длится 600 строк, просто для возврата одного объекта и требуется 2 секунды для генерации.
Они запрашивают выполнение намного быстрее, если вы скажете, какой тип будет возвращаться, поскольку он не должен объединяться по всей структуре. например.
DBContext.Bases.OfType<Left>.ToList();
or
DBContext.Bases.OfType<Right>.ToList();
Однако теперь я хочу ТОЛЬКО вернуть базовый класс. Несчастье делает
DBContext.Bases.OfType<Base>.ToList();
делает то же самое, что и DBContext.Bases.ToList();
Он получает структуру наследования WHOLE... Есть ли способ (без создания нового типа в EF) ТОЛЬКО, возвращающий класс Base при просмотре базовой коллекции?
Извините, что я не могу войти в мою реальную учетную запись...
Возможно, я не понял, я хочу вернуть все объекты (включая Base, Left и Right), но я хочу, чтобы возвращался Base-класс, даже если в базе данных они являются действительными классами Left и Right.
OFTYPE было хорошим предложением, но оно отфильтровывает все мои объекты, потому что ни один из них не является базовым типом. Но я хочу вернуть только значения типа Base в объекте Base type.
Любые идеи?
Ответы
Ответ 1
GetType()
не понимается в Entity Framework, но ключевое слово is
работает. Таким образом, вы можете создать выражение и применить его к вашему запросу. Здесь код должен работать для EF5 +, чтобы добавить метод расширения, который вы можете назвать следующим: query.OfOnlyType<Base, SubTypeWithDescendants>()
. (Или с теми же двумя аргументами типа, если вам нужно, моя иерархия сложнее, чем это)
public static IQueryable<ReturnType> OfOnlyType<ReturnType, QueryType>
(this IQueryable<QueryType> query)
where ReturnType : QueryType {
// Look just for immediate subclasses as that will be enough to remove
// any generations below
var subTypes = typeof(ReturnType).Assembly.GetTypes()
.Where(t => t.IsSubclassOf(typeof(ReturnType)));
if (subTypes.Count() == 0) { return query.OfType<ReturnType>(); }
// Start with a parameter of the type of the query
var parameter = Expression.Parameter(typeof(ReturnType));
// Build up an expression excluding all the sub-types
Expression removeAllSubTypes = null;
foreach (var subType in subTypes) {
// For each sub-type, add a clause to make sure that the parameter is
// not of this type
var removeThisSubType = Expression.Not(Expression
.TypeIs(parameter, subType));
// Merge with the previous expressions
if (removeAllSubTypes == null) {
removeAllSubTypes = removeThisSubType;
} else {
removeAllSubTypes = Expression
.AndAlso(removeAllSubTypes, removeThisSubType);
}
}
// Convert to a lambda (actually pass the parameter in)
var removeAllSubTypesLambda = Expression
.Lambda(removeAllSubTypes, parameter);
// Filter the query
return query
.OfType<ReturnType>()
.Where(removeAllSubTypesLambda as Expression<Func<ReturnType, bool>>);
}
Я тестировал его только на EF6.1 с кодовой первой моделью. Это сильно зависит от подсказка Алекс Джеймса 35.
Ответ 2
Предполагая, что вы можете использовать LINQ, можете ли вы использовать что-то по строкам следующего быстрого и грязного примера?:
var result = from item in DBContext.Bases.ToList()
where (!item.GetType().IsSubclassOf(typeof(Base)))
select item;
Ответ 3
Чтобы ответить на вопрос о том, что ни один из вышеперечисленных ответов, по-видимому, не заботится (т.е. мы только фильтруем возвращенные столбцы только столбцами базового типа, но не отфильтровываем строки, которые имеют информацию о производном типе), там это довольно простой способ сделать это с анонимными типами. См. здесь для другого вопроса о stackoverflow, касающегося специфики.
Идея состоит в том, чтобы сделать что-то вроде этого:
db.BaseTypes.Select(o => new { Prop1 = o.Prop1, Prop2 = o.Prop2, ....})
.AsEnumerable()
.Select(a => new BaseType() { Prop1 = a.Prop1, Prop2 = a.Prop2, ...});
Linq-to-Entities вернет список анонимных объектов, а .AsEnumerable()
вернет вас к Linq-to-Objects и позволит вам вызвать new BaseType()
с помощью списка инициализатора объекта.
У этого есть неудачный недостаток того, чтобы быть специфичным для типов. Кто-то здесь, в офисе, хочет написать общий текст, поэтому я скоро вернусь и отредактирую этот ответ с полностью общей версией этого.
EDIT (проверено, но не в производстве EntityFramework):
Благодаря этому ответу для кода SelectDynamic.
public static class QueryableExtensions {
/// <summary>
/// Constructs a query that only selects the columns that are actually in the type <typeparamref name="T"/> as public properties.
///
/// Useful for inherited types when you only want the base type information.
/// </summary>
/// <remarks>
/// This function materializes the query. You'll want to call the where clauses BEFORE this call (since it is an optimization).
/// </remarks>
/// <typeparam name="T">Entity type.</typeparam>
/// <param name="query">Source query.</param>
/// <returns>An IEnumerable of items of type <typeparamref name="T"/>.</returns>
public static IEnumerable<T> FilterColumnsByType<T>(this IQueryable<T> query) where T : new() {
Type type = typeof(T);
List<string> selectedProps = type.GetProperties().Select(p => p.Name).ToList();
Tuple<IQueryable, Type> anonObjectTypePair = query.SelectDynamicAndType(selectedProps);
IQueryable anonObjects = anonObjectTypePair.Item1;
Type anonType = anonObjectTypePair.Item2;
return anonObjects.Cast<object>().AsEnumerable().Select(ob => {
var ret = new T();
selectedProps.ForEach(p =>
type.GetProperty(p).SetValue(ret, anonType.GetField(p).GetValue(ob)));
return ret;
});
}
/// <summary>
/// Constructs a query that selects only the <paramref name="propNames"/> given and returns an <see cref="IQueryable"/> of dynamic objects with only the selected fields.
///
/// Also returns the type information of the dynamic objects.
/// </summary>
/// <param name="source">Source query.</param>
/// <param name="propNames">The list of properties names to select.</param>
/// <returns>A query of anonymous types defined by the supplied <paramref name="propNames"/> and the actual <see cref="Type"/> used to construct anonymous type.</returns>
public static Tuple<IQueryable, Type> SelectDynamicAndType(this IQueryable source, IEnumerable<string> propNames) {
Dictionary<string, PropertyInfo> sourceProperties = propNames.ToDictionary(name => name, name => source.ElementType.GetProperty(name));
Type dynamicType = LinqRuntimeTypeBuilder.GetDynamicType(sourceProperties.Values);
ParameterExpression sourceItem = Expression.Parameter(source.ElementType, "t");
IEnumerable<MemberBinding> bindings = dynamicType.GetFields().Select(p => Expression.Bind(p, Expression.Property(sourceItem, sourceProperties[p.Name]))).OfType<MemberBinding>();
Expression selector = Expression.Lambda(Expression.MemberInit(
Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), sourceItem);
return Tuple.Create(source.Provider.CreateQuery(Expression.Call(typeof(Queryable), "Select", new Type[] { source.ElementType, dynamicType },
Expression.Constant(source), selector)), dynamicType);
}
/// <summary>
/// Constructs a query that selects only the <paramref name="propNames"/> given and returns an <see cref="IQueryable{dynamic}"/> of dynamic objects with only the selected fields.
/// </summary>
/// <param name="source">Source query.</param>
/// <param name="propNames">The list of properties names to select.</param>
/// <returns>A query of anonymous types defined by the supplied <paramref name="propNames"/>.</returns>
public static IQueryable<dynamic> SelectDynamic(this IQueryable source, IEnumerable<string> propNames) {
return source.SelectDynamicAndType(propNames).Item1.Cast<dynamic>();
}
static class LinqRuntimeTypeBuilder {
private static AssemblyName assemblyName = new AssemblyName() { Name = "DynamicLinqTypes" };
private static ModuleBuilder moduleBuilder = null;
private static Dictionary<string, Type> builtTypes = new Dictionary<string, Type>();
static LinqRuntimeTypeBuilder() {
moduleBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run).DefineDynamicModule(assemblyName.Name);
}
private static string GetTypeKey(Dictionary<string, Type> fields) {
string key = string.Empty;
foreach (var field in fields.OrderBy(kvp => kvp.Key).ThenBy(kvp => kvp.Value.Name))
key += field.Key + ";" + field.Value.Name + ";";
return key;
}
private static Type GetDynamicType(Dictionary<string, Type> fields) {
if (null == fields)
throw new ArgumentNullException("fields");
if (0 == fields.Count)
throw new ArgumentOutOfRangeException("fields", "fields must have at least 1 field definition");
try {
Monitor.Enter(builtTypes);
string className = GetTypeKey(fields);
if (builtTypes.ContainsKey(className))
return builtTypes[className];
TypeBuilder typeBuilder = moduleBuilder.DefineType(className, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);
foreach (var field in fields)
typeBuilder.DefineField(field.Key, field.Value, FieldAttributes.Public);
builtTypes[className] = typeBuilder.CreateType();
return builtTypes[className];
} catch (Exception ex) {
//log.Error(ex);
Console.WriteLine(ex);
} finally {
Monitor.Exit(builtTypes);
}
return null;
}
public static Type GetDynamicType(IEnumerable<PropertyInfo> fields) {
return GetDynamicType(fields.ToDictionary(f => f.Name, f => f.PropertyType));
}
}
}
Ответ 4
В настоящее время я использую следующее расширение LINQ, предполагая, что подклассы расположены в одной и той же сборке.
public static class MyLinqExtensions
{
public static IQueryable<T> OfTypeOnly<T>(this IQueryable<T> query)
{
Type type = typeof (T);
IEnumerable<Type> derivedTypes = Assembly
.GetAssembly(type)
.GetTypes()
.Where(t => t.IsSubclassOf(type));
return query.ExceptTypes(derivedTypes.ToArray());
}
public static IQueryable<T> ExceptTypes<T>(this IQueryable<T> query, params Type[] excludedTypes)
{
if (excludedTypes == null)
return query;
return excludedTypes.Aggregate(query,
(current, excludedType) => current.Where(entity => entity.GetType() != excludedType));
}
}
Использование:
var bases = DBContext.Bases.OfTypeOnly<Base>();
Ответ 5
Вы можете использовать DbSet.SqlQuery
:
DBContext.Bases.SqlQuery("select * from BaseTable").AsNoTracking().ToList();
Имейте в виду, что не используя .AsNoTracking()
рано или поздно вы попадете в горячую воду (если есть производные типы, уже загруженные в контекст, вы сразу получите уникальные ключевые нарушения/исключения).
Ответ 6
Не уверен в различиях в производительности, но я могу представить, что это будет быстрее, чем загрузка всех строк (когда много строк находится в БД):
List<int> ids = DBContext.Rights.Select(x => x.Id).ToList();
ids.AddRange(DBContext.Lefts.Select(x => x.Id).ToList());
var bases = DBContext.Bases.Where(x => !ids.Contains(x.Id)).ToList();