EF Code First: отдельное создание объектов в БД с наследованием TFT

Пример использования

Пользователь зарегистрировался на сайте (добавление записи в таблицу Users). Затем переходит на электронную почту и подтверждает регистрацию. Ссылка подтверждения перенаправляет на страницу с формой редактирования человека, пользователь заполняет форму и отправляет ее (добавляя запись в таблицу «Люди»).

Техническое описание

У меня есть простое наследование с двумя объектами Table-Per-Type, определенное в модели EF.

Объекты

public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

public class Person : User
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
}

База данных

public class MainDataContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Person> Persons { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        var configs = modelBuilder.Configurations;
        configs.Add(new UserConfiguration());
        configs.Add(new PersonConfiguration());
    }
}

Конфигурации

internal class UserConfiguration : EntityTypeConfiguration<User>
{
    internal UserConfiguration()
    {
        ToTable("Users");

        HasKey(x => x.Id);

        Property(x => x.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        Property(x => x.Name)
            .IsRequired()
            .HasMaxLength(128);
        Property(x => x.Password)
            .IsRequired()
            .HasMaxLength(128);
        Property(x => x.Email)
            .IsRequired()
            .HasMaxLength(128);
    }
}
internal class PersonConfiguration : EntityTypeConfiguration<Person>
{
    internal PersonConfiguration()
    {
        ToTable("Persons");

        Property(x => x.FirstName)
            .IsRequired()
            .HasMaxLength(16);
        Property(x => x.MiddleName)
            .IsOptional()
            .HasMaxLength(16);
        Property(x => x.LastName)
            .IsRequired()
            .HasMaxLength(16);
    }
}

Необходимо последовательно создавать связанные (FK) объекты в DbSets, примерно так:

    var db = new MainDataContext();
    var user = new User
    {
        Name = "someUser",
        Password = "somePassword",
        Email = "someEmail"
    };
    db.Users.Add(user);
    db.SaveChanges();
    // After some time in other some place
    var person = new Person
    {
        Id = user.Id,
        FirstName = "someFirstName",
        LastName = "someLastName"
    };
    db.Persons.Add(person);
    db.SaveChanges();

После последнего сохранения изменений была обнаружена ошибка DbEntityValidationError: «Проверка не удалась для одного или нескольких объектов. Дополнительные сведения см. в свойстве EntityValidationErrors». Есть 3 ошибки проверки для обязательных полей пользователя.

Как отдельно добавить записи в DbSet, связанные FK (наследованием)?

ОБНОВЛЕНИЕ

Я нашел одно решение. Но для этого необходимо удалить старого пользователя. Я хотел бы сделать это без удаления, потому что, если таблица Users будет связана с другими таблицами, удаление будет невозможно.

    var db = new MainDataContext();
    var user = new User
    {
        Name = "someUser",
        Password = "somePassword",
        Email = "someEmail"
    };
    db.Users.Add(user);
    db.SaveChanges();
    // After some time in other some place
    var person = new Person
    {
        Id = user.Id,
        Name = user.Name,
        Password = user.Password,
        Email = user.Email,
        FirstName = "someFirstName",
        LastName = "someLastName"
    };
    db.Users.Remove(user);
    db.Persons.Add(person);
    db.SaveChanges();

Но возникла другая проблема - генерируется новый Guid для объекта person.

Как решить проблему, не удаляя старую запись пользователя?


person ebashmakov    schedule 14.02.2012    source источник
comment
Поскольку Person наследуется от User, вы должны указать имя, пароль и адрес электронной почты. Как выглядит база данных? Есть ли в таблице Person все поля из таблицы User или только FK для пользовательской таблицы. Если это позднее, ваши Сущности ошибаются.   -  person cadrell0    schedule 14.02.2012
comment
@cadrell0, как я уже говорил ранее, я использую способ таблицы для каждого типа (просто FK для пользовательской таблицы) для сопоставления таблиц с иерархиями объектов. Вот изображение диаграммы БД.   -  person ebashmakov    schedule 14.02.2012
comment
Тогда почему Person наследуется от User?   -  person cadrell0    schedule 14.02.2012
comment
Потому что это необходимо по логике моего приложения. Вы предлагаете сделать наоборот? Не беда, проблема не в этом. Нужно отдельно добавлять записи (объекты) в таблицы (DbSets), связанные внешним ключом (наследование).   -  person ebashmakov    schedule 15.02.2012
comment
Ваши объекты должны соответствовать формату вашей базы данных. Если ваше приложение должно представлять данные по-другому, создайте классы предметной области (DTO).   -  person jrummell    schedule 16.02.2012
comment
Проверка не вызывает здесь проблемы. Вы можете отключить проверку, но тогда база данных выдаст ошибку, поскольку вы настроили свойства пароля, имени и электронной почты в соответствии с требованиями. Это означает, что база данных не допустит пустых значений. Поскольку вы не устанавливаете соответствующие свойства, вы пытаетесь эффективно сохранить значения NULL в столбцах, не допускающих значения NULL. Ваше приложение позволяет этим свойствам быть нулевыми? Если да, то база данных, вероятно, тоже должна. Если нет, то вы никогда не должны допускать, чтобы они были нулевыми в вашем приложении. Если ничего из вышеперечисленного не соответствует действительности, возможно, вам придется переосмыслить свою архитектуру.   -  person Pawel    schedule 16.02.2012
comment
@jrummell и Павел, извините, но вы опять не понимаете проблемы. Я попытаюсь объяснить еще больше. Например, приложение требует от пользователя сначала зарегистрироваться (т.е. указать имя пользователя, пароль и адрес электронной почты), а затем, после подтверждения регистрации, заполнить свои личные данные (таблица «Люди»).   -  person ebashmakov    schedule 16.02.2012
comment
Что произойдет, если вы просто создадите Person со всеми установленными полями и вызовете savechanges?   -  person alun    schedule 16.02.2012
comment
@alun все в порядке. В БД добавлен новый человек. Но как я уже говорил мне нужно сначала добавить пользователя, а потом дополнить его по человеку.   -  person ebashmakov    schedule 16.02.2012


Ответы (2)


После включения обновления варианта использования:

Здесь не следует использовать наследование. Вы должны использовать композицию и отношение один к одному между сущностями:

public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public virtual UserInfo Information{get;set;}
}

internal class UserConfiguration : EntityTypeConfiguration<User>
{
    internal UserConfiguration()
    {
        ToTable("Users");

        HasKey(x => x.Id);

        Property(x => x.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        Property(x => x.Name)
            .IsRequired()
            .HasMaxLength(128);
        Property(x => x.Password)
            .IsRequired()
            .HasMaxLength(128);
        Property(x => x.Email)
            .IsRequired()
            .HasMaxLength(128);        
    }
}

public class UserInfo
{
    [ForeignKey("InfosUser")]
    [Key]
    public UserId {get;set;}
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public virtual User InfosUser {get;set;}
}

internal class UserInfoConfiguration : EntityTypeConfiguration<UserInfo>
{
    internal UserInfoConfiguration()
    {
        ToTable("Persons");

        Property(x => x.FirstName)
            .IsRequired()
            .HasMaxLength(16);
        Property(x => x.MiddleName)
            .IsOptional()
            .HasMaxLength(16);
        Property(x => x.LastName)
            .IsRequired()
            .HasMaxLength(16);
        HasRequired(ui=>ui.InfosUser).WithOptional(u=>u.Information)
    }
}

и используйте его:

    var db = new MainDataContext();
    var user = new User
    {
        Name = "someUser",
        Password = "somePassword",
        Email = "someEmail"
    };
    db.Users.Add(user);
    db.SaveChanges();
    // After some time in other some place
    var user = db.GetPersonFromDb(Id);// Get early saved person from db
    var information = new UserInfo
    {
         FirstName = "someFirstName",
         LastName = "someLastName",
    }
    user.Information = information;
    db.SaveChanges();
person Kirill Bestemyanov    schedule 04.12.2012
comment
Извините, решение не подходит, потому что я хотел бы, чтобы столбцы FirstName и LastName были обязательными в таблице Persons. - person ebashmakov; 04.12.2012
comment
Не могли бы вы описать свой вариант использования? Почему вам нужно сохранить половину данных, а затем добавить некоторые другие данные? Это невозможно сделать в ef, что вы хотите (кроме обходного пути с удалением предыдущей строки), но я уверен, что для вашего варианта использования есть другой способ. - person Kirill Bestemyanov; 04.12.2012
comment
Спасибо за ваш ответ, честно говоря, я уже использовал такой подход. Но я хочу использовать наследование TFT, и мой вопрос об этом. - person ebashmakov; 04.12.2012
comment
Вы не можете изменить тип объекта (восходящий или нисходящий) в Entity Framework. Это ограничение EF. - person Kirill Bestemyanov; 04.12.2012
comment
Это действительно означает, что тип меняется? Я просто хочу добавить объект Person (после добавления объекта User) с использованием сгенерированного идентификатора пользователя, но с сохранением наследования. - person ebashmakov; 05.12.2012
comment
С точки зрения EF Person и User — это два разных типа. Вы не можете изменить (или преобразовать) с пользователя на человека или наоборот. Если вы хотите работать с UserTable и PersonTable, как с разными таблицами, вы не должны использовать наследование между классами User и Person. - person Kirill Bestemyanov; 05.12.2012
comment
Кирилл, похоже, ты прав. Но я хочу быть уверен. Как насчет состояния сущности, присоединения, отсоединения и т. д.? Есть ли способ удалить сущность пользователя и создать человека с тем же идентификатором? - person ebashmakov; 05.12.2012
comment
Теоретически можно удалить .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); и установите идентификатор. Он будет сохранен. Но в этом случае вы должны установить значение по умолчанию в базе данных или создать идентификатор для каждой новой вставленной сущности. - person Kirill Bestemyanov; 05.12.2012

Проблема, похоже, в том, что созданный объект не является объектом Person. Если вы вставите значение User.id в таблицу Person, вы сможете найти пользователя как человека и обновить его.

        Guid id;
        using (var db = new MainDataContext())
        {
            var user = new User
            {
                Name = "someUser",
                Password = "somePassword",
                Email = "someEmail"
            };
            db.Users.Add(user);
            db.SaveChanges();
            id = user.Id;
        }

        // After some time in other some place
        //pause here and insert the id GUID into the Person table

        using (var db = new MainDataContext())
        {
            var person = db.Persons.Find(id);
            person.FirstName = "someFirstName";
            person.LastName = "someLastName";
            db.SaveChanges();
        }

Добавление идентификатора пользователя в таблицу Person за пределами EF обеспечивает желаемое повышение. Вы можете «повысить» объект от пользователя до человека с помощью прямого обновления базы данных.

person qujck    schedule 07.12.2012