Подтвердить что ты не робот

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

[Q:] Возможно ли использовать 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.

Например, для List [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, так как не мог бы использоваться в качестве дискриминатора.

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

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

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

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

4b9b3361

Ответ 1

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

Ответ 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; }

И, OnModelCreating вы определяете:

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

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