Использование внешнего ключа (FK) в качестве дискриминатора для иерархии таблиц (TPH)

#sql-server #entity-framework-4.1

#sql-сервер #entity-framework-4.1

Вопрос:

[Вопрос:] Можно ли использовать FK в качестве дискриминатора в EF и какие обходные пути придумали люди?

Сценарий

Объекты EF

 public class List
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<ListItem> Items { get; set; }
}

public abstract class ListItem
{
    public int Id { get; set; }
    public List List { get; set; }
    public string Text { get; set; }
}
 

База данных

Существующая база данных, которая не используется исключительно EF (т. Е. Не может быть изменена), имеет следующие поля:

 List
    Id         int not null      (identity)
    Name       varchar

ListItem
    Id         int not null      (identity)
    ListId     int not null      (FK to List.Id)
    Text       varchar
 

Желаемый результат

Я хочу, чтобы идентификатор списка был дискриминатором для ListItem. т.е. Для каждой записи в списке реализован отдельный класс, происходящий от ListItem.

Например, для списка [Id:1]

 public class PersonListItem : ListItem
{
    public int PersonId { get; set; }
    public Person Person { get; set; }
}

public class ListItemConfiguration : EntityTypeConfiguration<ListItem>
{
    Map<PersonListItem>(m => m.Requires("ListId").HasValue(1));
}
 

В приведенном выше сценарии сохранение изменений приводит к исключению SQL, поскольку EF пытается вставить в List_Id при создании нового экземпляра ListItem. List_Id не является полем, и я не могу сопоставить ListID как свойство в ListItem, поскольку его нельзя было бы использовать в качестве дискриминатора.

Мое решение до сих пор…

Этот вопрос и ответ объясняет, почему они приняли решение не разрешать FK в качестве дискриминатора.

Мой обходной путь до сих пор состоит в том, чтобы добавить в базу данных другое поле для использования в качестве дискриминатора, которому затем присваивается то же значение, что и ListID, с помощью триггера вставки (для обработки вставок, отличных от EF), а затем добавить навигационное свойство ListID в объект ListItem.

У кого-нибудь есть какие-либо предложения / альтернативы?

Ответ №1:

Наиболее простое решение заключается во введении перечисления, которое используется в качестве дискриминатора для представления типа. В упомянутом вами сообщении четко описывается, что FK не может быть использован, поэтому вам нужно преобразовать его во что-то, что может быть применено. Это требует некоторого обслуживания, но поскольку вы также должны знать типы, которые являются производными от вашего listitem, я не вижу в этом серьезной проблемы. Другим вариантом, который у вас есть, было бы отказаться от TPH и использовать TablePerType. Таким образом, вам не нужно вводить перечисление, но вы создадите таблицу для каждого производного элемента списка.

Комментарии:

1. Спасибо за ваш вклад, Карло. Мне следовало бы упомянуть, что я обнаружил, что TPT не страдает от этой проблемы, но на самом деле это не вариант в моем сценарии. Не могли бы вы подробнее рассказать о подходе Enum? Как это позволяет мне избежать добавления другого поля в БД в качестве дискриминатора? Другая проблема заключается в том, что это не эксклюзивная таблица и EF, устаревшие приложения все еще записывают в нее (отсюда и триггерный подход).

Ответ №2:

Предполагая, что сначала код EF 4, существует несколько (очень?) Надуманный способ, который требует использования представлений для создания виртуального столбца дискриминатора, который совпадает с ListID.

Вы должны иметь возможность добавлять представление в БД и триггеры «вместо» для этого представления, чтобы имитировать новый столбец. Во всем этом вам нужно обойти некоторые проблемы, с которыми EF сталкивается при таком подходе.

 CREATE view [dbo].[vListItem] as 
select Id, ListId,[Text]
,cast(ListId as varchar(10)) as Discriminator  
from xListItem;
GO

Note: need the cast to varchar <= int causes errors on EF (MaxLength facet)
 

Примером триггера для вставки может быть:

 CREATE TRIGGER trg_vListItem_Insert
ON [vListItem]
INSTEAD OF INSERT
AS
Begin
    -- set nocount on
    Insert into xListItem (ListId,[Text]) 
    Select i.ListId, i.[Text]
    from Inserted i

    -- Need this so Ef knows a row has been inserted
    select Id from xListItem where @@ROWCOUNT > 0 and Id = scope_identity()
End

Note: Delete and Update would be similar.
 

Тогда вы можете иметь в своей модели:

 public class vListItem
{
    [Key]
    public int Id { get; set; }

    public string Text { get; set; }
    [ForeignKey("ListId")]
    public xList aList { get; set; }
    public int ListId { get; set; }
}

public class vPerson : vListItem
{
}
public class vThing : vListItem
{
}
 

В вашем DbContext вы можете иметь:

     public DbSet<vListItem> vitems { get; set; }
    public DbSet<vPerson> vpersons { get; set; }
    public DbSet<vThing> vthings { get; set; }
 

И при создании модели вы определяете:

 modelBuilder.Entity<vListItem>()
    .Map<vPerson>(m => m.Requires("Discriminator").HasValue("1"))
    .Map<vThing>(m => m.Requires("Discriminator").HasValue("2"))
    ;
 

Вот и все. Некоторые тесты:

     [TestMethod]
    public void TestMethodPersonInsert()
    {
        Entities db = new Entities();
        xList xl = db.lists.SingleOrDefault(x => x.Id == 1);
        vPerson vp = new vPerson();
        vp.Text = "p4";
        vp.aList = xl;
        db.vpersons.Add(vp);
        db.SaveChanges();
    }

    [TestMethod]
    public void TestMethodThingInsert()
    {
        Entities db = new Entities();
        xList xl = db.lists.SingleOrDefault(x => x.Id == 2);
        vThing vt = new vThing();
        vt.Text = "t0";
        vt.aList = xl;
        db.vthings.Add(vt);
        db.SaveChanges();
    }
 

Ответ №3:

Я бы использовал несколько сопоставлений. Но суть проблемы заключается в том, чтобы вообще не иметь такого типа отношений и пересмотреть свой дизайн.