EF4 TPT с абстрактными классами - определение отношений - PullRequest
2 голосов
/ 21 ноября 2010

Я ходил по кругу с этим и, похоже, не в состоянии гуглить правильные ответы - даже после нескольких часов, потраченных на это, так что вы - мое последнее средство!

Настройка

В моем веб-приложении я хотел бы разрешить пользователям использовать различные механизмы аутентификации для доступа к своим учетным записям. В дополнение к обычному пользователю / паролю, я хотел бы позволить им использовать OpenId от Google, OpenId от Yahoo или даже Facebook. Это кажется довольно простым для отображения на классы: абстрактный Account с несколькими <something>Account классами, наследующими некоторые базовые свойства от Account. Я начал со следующих двух классов:

public abstract class Account
{
    public int Id { get; set; }
    public int OwnerId { get; set; }

    public virtual Person Owner { get; set; }
}

public class OpenIdAccount : Account
{
    public string Identifier { get; set; }
}

Будучи немного перфекционистом и много занимаясь db dev в своей повседневной работе, я решил, что таблица для каждого типа (TPT) будет наиболее желательным вариантом. Поскольку EF4 по умолчанию использует TPH, на стороне DbContext я определил следующее:

public class MySampleDb : DbContext
{
    public DbSet<Person> People { get; set; }
    public DbSet<Account> Accounts { get; set; }
    public DbSet<OpenIdAccount> OpenIdAccounts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Account>().MapHierarchy(a => new
        {
            a.Id, 
            a.OwnerId
        }).ToTable("Account");

        modelBuilder.Entity<OpenIdAccount>().MapHierarchy(oid => new
        {
            oid.Id,
            oid.Identifier
        }).ToTable("OpenIdAccount");
    }
}

Теперь я хотел начать с нескольких простых тестов и подумал, что заполнение базы данных при каждом запуске - это хороший способ иметь согласованные данные для работы:

protected override void Seed(MySampleDb context)
{
    Person johns = new Person
    {
        Id = 1,
        Nickname = "John Skeet"
    };

    OpenIdAccount google = new OpenIdAccount
    {
        Id = 2,
        OwnerId = 1,
        Identifier = "https://www.google.com/accounts/o8/id?id=AItOawnmUz4e6QIn9mgd98WMAbnzC25sji5lpSM"
    };

    context.People.Add(johns);
    context.Accounts.Add(google);
    context.SaveChanges();
}

(Не уверен, почему, но DbContext, похоже, никогда не пытался заполнить мою базу данных вышеуказанными данными, пока я явно не назвал .SaveChanges() ... какие-нибудь идеи?)

Проблемы

One

В базе данных EF не определял никаких отношений между Account и OpenIdAccount. Это был первый предупредительный знак, что-то было не так; несомненно, OpenIdAccount должен иметь Id, определенный как PK и FK, указывающий на Account.Id?

Два

Я получаю следующее UpdateException, когда .net пытается выполнить .SaveChanges():

Значение, разделяемое между сущностями или ассоциациями генерируется в нескольких местах. Проверь это отображение не разделяет EntityKey на несколько Сгенерированные магазином столбцы.

Следующие шаги

Сегодня мой первый день с EF4 и первой разработкой кода. Потратив много часов, читая о EF4 + code-first + custom mapping, я пришел к точке, где я навсегда застрял и мне нужен удар в правильном направлении, прежде чем я смогу начать снова :-)

Так что я надеюсь, что вы, ребята, сможете понять вышесказанное и объяснить какую-то глупую ошибку / недоразумение с моей стороны!

1 Ответ

4 голосов
/ 22 ноября 2010

Не беспокойтесь, вы находитесь в правильном месте :) Давайте разберемся с вашими вопросами:

(Не знаю почему, но DbContext, похоже, никогда не пытался заполнить мою базу данных указанными выше данными, пока яявно называется .SaveChanges () ... есть идеи?)

Именно так оно и было задумано.В методе Seed вам нужно будет вызвать SaveChanges после добавления новых объектов в их соответствующие наборы данных DbSets.Так что ты там хорош.

В базе данных EF не определяла никаких отношений между Account и OpenIdAccount.

Ваша реализация наследования имеет вид Таблица для конкретного типа или наследования TPC и NOT TPT, и это происходит из-за того, что вы сделали свой класс Account равным аннотация и то, что вы видите с точки зрения отсутствия отношения один к одному между Account и OpenIdAccount, является точным поведением EF по умолчанию, когда дело доходит до отображения TPC .Если вы удалите абстрактное ключевое слово из класса Account, у вас будет TPT , и ваш код будет работать нормально.

Значит ли это, что вы должны отказаться от своего TPC и превратить его в TPT?Ну, это, конечно, одно решение, но вам не нужно идти на него, если вы все еще хотите сохранить свой TPC, поскольку абсолютно возможно иметь TPC с Code First, и нам просто нужно внести небольшие изменения в вашу модель, чтобы сделатьэто работает.

Решение:

TPC означает «Создать полностью отдельную таблицу для каждого неабстрактного типа в моей иерархии».Обратите внимание, что поскольку между двумя таблицами нет внешнего ключа, нам нужно позаботиться о предоставлении уникальных ключей, поэтому мы должны отключить идентификацию в свойстве первичного ключа.Для этого есть 2 способа:

1.Используя DataAnnotations из System.ComponentModel.DataAnnotations:

public abstract class Account 
{
    [StoreGenerated(StoreGeneratedPattern.None)]
    public int Id { get; set; }
    public int OwnerId { get; set; }
    public virtual Person Owner { get; set; }
}


2.Используя FluentAPI:

modelBuilder.Entity<Account>().Property(a => a.Id)
    .StoreGeneratedPattern = System.Data.Metadata.Edm.StoreGeneratedPattern.None;

После того, как вы отключите его одним из вышеперечисленных способов, вы увидите, что полученное вами исключение исчезает, и модель начинает работать.

Также, чтобы сделать его действительно TPC, вы должны отобразить все в каждой таблице, потому что в TPC есть таблица для каждого класса, и у каждой из этих таблиц есть столбец для каждого свойства этого типа :

modelBuilder.Entity<Account>().MapHierarchy(a => new {
    a.Id,
    a.OwnerId,
})
.ToTable("Accounts");

modelBuilder.Entity<OpenIdAccount>().MapHierarchy(o => new {
    o.Id,
    o.OwnerId,
    o.Identifier
})
.ToTable("OpenIdAccounts");


С учетом всего сказанного, я думаю, что TPC не предназначен для использования в этом сценарии, и вы должны использовать TPT.Для более подробного обсуждения этой темы вы можете прочитать этот отличный пост: Алекс Джеймс :
Как выбрать стратегию наследования

...