Возможно ли присоединиться к несвязанной таблице в запросе EF Core Include

#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();