Множественные сущности и таблицы

101

Множественные сущности для одной таблицы

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

Подход с использованием нескольких классов модели для одной таблицы скорее всего понадобится, когда вы работаете с уже существующей базой данных (т.е. используете Code-Second). Хотя никто вас не ограничивает использовать множественные сущности при работе с Code-First, когда на их основе будет создаваться одна таблица в базе данных.

Допустим у нас имеется следующая модель:

public class Customer
{
        public int CustomerId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public int Age { get; set; }

        public Photo Photo { get; set; }
}

[Table("Photo")]
public class Photo
{
        [Key, ForeignKey("PhotoOf")]
        public int CustomerId { get; set; }

        [Column(TypeName = "image")]
        public byte[] Image { get; set; }
        public string Caption { get; set; }

        public Customer PhotoOf { get; set; }
}

Здесь данные фотографии покупателя были вынесены в отдельную таблицу Photo. После запуска этого примера, данные о фотографии пользователя будут храниться в отдельной таблице. Вполне возможно вы захотите хранить эти данные в таблице Customers, но в коде иметь возможность отдельно извлекать данные о фотографии, имея дополнительный сущностный класс. Чтобы реализовать модель множественных сущностей в Code-First, нужно следовать следующим простым правилам:

Наши классы Customer и Photo реализуют эти правила, поэтому мы можем настроить их для использования в одной таблице базы данных. Для этого можно использовать атрибут Table в модели для обоих классов, передав ему имя одной таблицы:

[Table("Customer")]
public class Customer
{
    // ...
    
    [Required]
    public Photo Photo { get; set; }
}

[Table("Customer")]
public class Photo
{
    // ...
}

Обратите внимание, что в классе Customer мы указываем атрибут Required для навигационного свойства, ссылающегося на таблицу Photo. Благодаря этому мы указываем Entity Framework на создание связи один-к-одному, иначе, была бы создана связь ноль-или-один-к-одному, что не соответствовало бы первому условию. Если вы теперь запустите приложение, то Entity Framework воссоздаст базу данных, т.к. модель данных изменилась, и использует одну таблицу Customer для двух сущностных типов:

Создание одной таблицы для двух сущностей

Подобная модель похожа на использование сложных типов, которые мы описывали ранее. Но огромной разницей при этом является то, что если вы захотите вставить новые данные для столбцов сложного типа вам нужно работать со всей таблицей, а множественные сущности позволяют работать только с частью таблицы – для того, чтобы вставить фотографию нужно просто знать идентификатор покупателя. Очевидно, что данный подход значительно увеличивает производительность при работе с большими таблицами.

Если вы используете запрос наподобие следующего:

context.Customers.ToList();

то Entity Framework выберет данные покупателя без данных о фотографии, несмотря на то, что эти данные хранятся в одной таблице базы данных. Это говорит о том, что в логически в коде Entity Framework фактически работает с двумя таблицами. Чтобы извлечь также данные о фотографии нужно использовать метод Include():

context.Customers.Include(c => c.Photo).ToList();

Вы можете извлечь также данные фотографии отдельно от данных пользователя (что невозможно при использовании сложных типов).

Альтернативным способом настройки множественных сущностей является использование Fluent API. Как описывалось в предыдущей статье, для явного указания имени таблицы в Fluent API используется метод ToTable():

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().ToTable("Customer");
    modelBuilder.Entity<Photo>().ToTable("Customer");
}

Несколько таблиц для одной сущности

Теперь давайте рассмотрим обратный сценарий, когда нам требуется отобразить несколько таблиц из базы данных на один сущностный класс модели. Такой подход обычно используется при работе с Code-Second, когда нам необходимо описать несколько таблиц, связанных по смыслу, в одном классе. В контексте нашего примера это означает, что если в базе данных имеется две таблицы Customers и Photo, то их можно отобразить на один сущностный класс Customer, содержащий данные как заказчика, так и его фотографии.

Для реализации такой модели необходимо указать Code-First наборы свойств в классе, которые должны отображаться на соответствующие таблицы. Очевидно, что сделать это с помощью аннотаций данных нельзя, т.к. аннотации в виде атрибутов нельзя указывать к подмножествам свойств в модели. Fluent API имеет специальный метод Map(), который позволяет построить список нужных свойств и указать имя таблицы.

Давайте создадим модель с классом Customer, который содержит информацию как о пользователе, так и о его фотографии:

public class Customer
{
        public int CustomerId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public int Age { get; set; }

        [Column(TypeName = "image")]
        public byte[] Image { get; set; }
        public string Caption { get; set; }
}

В следующем примере показана настройка конфигурации множественных таблиц с использованием Fluent API:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
        .Map(m =>
        {
            // Настройка первой таблицы
            m.Properties(c => new { c.Age, c.FirstName, c.LastName, c.Email });
            m.ToTable("Customer");
        })
        .Map(m =>
        {
            // Настройка второй таблицы
            m.Properties(c => new { c.Image, c.Caption });
            m.ToTable("Photo");
        });
}

Метод Map() вызывается на объекте конфигурации сущностного класса, который возвращается вызовом типизированного метода DbModelBuilder.Entity(). Этому методу передается делегат Action типизированный классом EntityMappingConfiguration, в котором имеется метод Properties(), где указывается набор свойств класса модели, который должен быть вставлен в таблицу. Во втором вызове метода Map() описывается структура таблицы Photo. Порядок вызовов методов Map() важен, т.к. при первом вызове определяется главная таблица, а далее определяются зависимые от нее таблицы. Убедиться в этом можно, взглянув на рисунок, где показана структура созданных таблиц Customer и Photo:

Создание двух таблиц из одного класса модели

Таблица Photo здесь является зависимой, т.к. она содержит внешний ключ. Между таблицами создается отношение один-к-одному. Если вы покопаетесь в свойствах таблицы Photo то увидите, что она не поддерживает каскадного удаления данных. Но Entity Framework знает, что если вы удалите покупателя из таблицы Customer, он должен будет построить команду DELETE, которая охватит обе таблицы.

Пройди тесты
Лучший чат для C# программистов