#c# #linq #entity-framework-core #ef-core-3.1
#c# #linq #entity-framework-core #ef-core-3.1
Вопрос:
У меня есть проект EF Core code first и я создал следующую таблицу
[Table("Activity")]
public class Activity : BaseModel
{
[Key]
public int Id { &et; set; }
public int ActivityId { &et; set; }
public TransactionType Type { &et; set; }
public virtual ActivityItem Item { &et; set; }
}
ActivityItem
это абстрактный класс, который не сопоставляется со своей собственной таблицей, но у меня есть три отдельные модели, которые все наследуются от него.
//Not its own table in the database
public abstract class ActivityItem
{}
[Table("ClassA")]
public class ClassA : ActivityItem
{
public int Id {&et; set;}
}
[Table("ClassB")]
public class ClassB : ActivityItem
{
public int Id {&et; set;}
}
[Table("ClassC")]
public class ClassC : ActivityItem
{
public int Id {&et; set;}
}
В данном конкретном случае я явно избегаю схемы Таблица-иерархия, потому что мне нужно иметь ClassA
, ClassB
и ClassC
поскольку в базе данных есть собственные уникальные таблицы.
Всякий раз, когда я запрашиваю запись из моей Activity
таблицы, я использую Type
свойство, чтобы указать, является ли Item
либо экземпляром ClassA
, ClassB
либо ClassC
.
Есть ли способ, которым я все еще могу использовать Include
метод при формировании своих запросов для заполнения Item
свойства навигации?
Текущее решение
var activity = _context.Activity.Where(...).FirstOrDefault();
if (activity.Type == "ClassA")
activity.Item = _context.ClassA.First(p =&&t; p.Id == activity.ActivityId);
else if (activity.Type == "ClassB")
activity.Item = _context.ClassB.First(p =&&t; p.Id == activity.ActivityId);
else if (activity.Type == "ClassC")
activity.Item = _context.ClassC.First(p =&&t; p.Id == activity.ActivityId);
Желаемое решение
var activity = _context.Activity.Include(p =&&t; p.Item).Where(...).FirstOrDefault();
Я знаю, что ядро EF будет записывать запросы Join к связанной таблице при использовании Include
метода, но в моем случае связанной таблицы нет, поскольку ActivityItem
она не представлена в базе данных. Есть ли способ явно указать, к какой таблице присоединяться на основе моего пользовательского Type
поля, не прибегая к схеме Таблица-иерархия?
Комментарии:
1. Если вы хотите получить данные в одном запросе, вам придется выполнять левые соединения с каждой таблицей, что в некотором роде противоречит цели наличия одной
ActivityId
сType
. Вместо этого вы могли бы иметь по одному FK для каждой таблицы, которая имеет значение null, с соответствующими свойствами навигации, и тогда вы смогли бы включить их все, и вам пришлось бы определить тип, по которому один из них не равен null. В принципе, реляционная база данных не может точно моделировать наследование.2. В качестве альтернативы вы можете рассмотреть решение, в котором у вас есть одна таблица действий, и она будет содержать информацию, которая определяет любые различия между каждым действием.
Ответ №1:
Итак, у вас есть таблица действий и предикат, который фильтрует ноль или более действий из этой таблицы. Вас интересует только первое действие, которое проходит фильтр. Может вообще отсутствовать действие, которое могло бы пройти фильтр, отсюда и OrDefault.
Это извлеченное действие содержит (строку?) тип свойства. Значение этого свойства показывает, следует ли вам искать в таблице ClassA / ClassB / ClassC.
Выбранное действие также имеет свойство ActivityId. Вам нужен первый (или используемый по умолчанию) ActivityItem (ClassA / ClassB / ClassC), который имеет идентификатор, равный этому ActivityId.
Из-за вашего дизайна это свойство ActivityId
не является надлежащим внешним ключом к тому, ActivityItem
к которому это Activity
относится. На самом деле ваш внешний ключ представляет собой комбинацию [Type, ActivityId]
.
Итак, что вам нужно, это метод, который объединяет все ваши ClassA / ClassB / ClassC
в одну последовательность, помня Type
. После этого вы можете присоединиться [Type, Id]
.
Если вам нужно сделать это только один раз, вы можете использовать следующее. Если вам нужно сделать это для решения нескольких проблем, рассмотрите возможность создания методов расширения.
var activityItems = dbContext.ClassAItems.Cast<ActivityItem&&t;()
.Select(classAItem =&&t; new
{
Type = "ClassA",
Data = classAItem,
})
.Concat(dbContext.ClassBItems.Cast<ActivityItem&&t;()
.Select(classBItem =&&t; new
{
Type = "ClassB",
Data = classBItem,
}))
.Concat(dbContext.ClassCItems.Cast<ActivityItem&&t;()
.Select(classCItem =&&t; new
{
Type = "ClassC",
Data = classCItem,
}));
Теперь вы можете просто выполнить внутреннее объединение:
// join the filtered activities with the activityitems:
var result = dbContext.Activities.Where(...)
.Join(activityItems,
activity =&&t; new // from each activity take [type, activityId]
{
Type = activity.Type
Id = activity.ActivityId,
}
activityItem =&&t; new // from each ActivityItems take [Type, Id]
{
Type = activityItem.Type,
Id = activityItem.Data.Id,
}
// when these keys match, use the Activity and the matchin& ActivityItem to make one new
// well, in this case, you are only interested in the Data of the ActivityItem
(activity, activityItem) =&&t; activityItem.Data)
// From the joined items you only want the first:
.FirstOrDefault();