Создание базы данных

91

Ранее мы уже видели, как Entity Framework создает базу данных из сущностной модели с помощью Code-First, но при этом не описывали этот момент более подробно. С помощью Code-First можно настроить инициализацию базы данных. Например, в статье “Использование Code-First” мы использовали следующие настройку:

Database.SetInitializer(
        new DropCreateDatabaseIfModelChanges<SampleContext>());

Этот код говорит Code-First, что если модель данных изменилась, то нужно удалить и воссоздать базу данных с новой моделью.

Процесс инициализации базы данных состоит из двух этапов:

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

Запуск инициализации базы данных вручную

Есть ситуации, когда вы, возможно, захотите контролировать момент создания базы данных, а не использовать автоматическую инициализацию Code-First. Для явного вызова процесса создания базы данных используется метод DbContext.Database.Initialize(), которому передается логический параметр. Если этот логический параметр равен false, то инициализация запустится только в том случае, если она уже не была вызвана ранее. Если этот логический параметр равен true, инициализация запустится в любом случае. Стоит запомнить, что этот метод следует вызвать до создания объекта контекста, его, например, можно указать в конструкторе класса контекста.

Возникает вопрос, когда может пригодиться ручная инициализация? С помощью ручной инициализации вы можете обработать любые ошибки, которые возникают во время создания модели и базы данных, в одном месте. Еще одной причиной является увеличение производительности, при отражении большой и сложной модели данных.

Давайте рассмотрим пример использования ручной инициализации. Мы будем использовать старую модель, с двумя классами Customer и Order, которую использовали ранее. Следующий код можно добавить в обработчик загрузки веб-формы нашего приложения ASP.NET:

protected void Page_Load(object sender, EventArgs e)
{
    try
    {
        SampleContext context = new SampleContext();

        // Запустить инициализацию базы данных в этой точке
        context.Database.Initialize(false);
    }
    catch (Exception ex)
    {
        // Если при создании БД возникла ошибка, 
        // отобразим ее в окне отладчика
        Debug.WriteLine("Инициализация не выполнена. Ошибка: ");
        Debug.WriteLine(ex.Message);
    }
}

Давайте теперь создадим ошибку в нашей модели. Для этого первичному ключу CustomerId класса Customer явно зададим тип NVARCHAR в базе данных с помощью атрибута Column. Это вызовет ошибку инициализации модели, т.к. поле имеет тип int:

public class Customer
{
    [Column(TypeName="nvarchar")]
    public int CustomerId { get; set; }
    
    // ...
}

В результате запуска приложения с такой моделью, в окне отладки Output вы увидите следующее сообщение с ошибкой:

Обработка ошибок при инициализации базы данных

Как вы видите, с помощью ручной инициализации мы можем обработать ошибки, возникающие при отображении модели на базу данных. В данном случае мы просто вывели сообщение в консоль, но в реальном приложении вы можете предпринять какие-нибудь действия и попробовать повторно инициализировать базу данных. Удалите атрибут Column у свойства CustomerId в классе модели, чтобы тестировать последующие примеры.

С помощью метода Initialize() можно также отключить автоматическую инициализацию базы данных. Для этого вы можете передать ему значение null, при этом все еще можно будет запустить инициализацию вручную.

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

С помощью метода Database.SetInitializer() можно управлять поведением Code-First для создания базы данных при изменении модели. Этот метод принимает экземпляр интерфейса IDatabaseInitializer<TContext>. В Entity Framework есть три класса, реализующих этот интерфейс и обеспечивающих возможность выбора поведения Code-First при инициализации базы данных:

CreateDatabaseIfNotExists

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

DropCreateDatabaseIfModelChanges

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

DropCreateDatabaseAlways

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

Помимо этих стандартных типов инициализации вы можете создать произвольный тип инициализации, реализовав интерфейс IDatabaseInitializer. В этом интерфейсе определен один обобщенный метод InitializeDatabase() принимающий объект контекста. Примером пользовательского механизма инициализации может послужить класс DontDropDbJustCreateTablesIfModelChanged, который находится в расширении EF CodeFirst пакета NuGet. С помощью этого инициализатора вы можете не беспокоится об удалении базы данных, т.к. он затрагивает только таблицы, которые изменились в модели.

Установить инициализатор, используемый по умолчанию, можно также в конфигурационном файле приложения, как показано в примере ниже:

<entityFramework>
    <contexts>
      <context type="CodeFirst.SampleContext, CodeFirst" disableDatabaseInitialization="false">
        <databaseInitializer 
           type="System.Data.Entity.DropCreateDatabaseIfModelChanges`1[[CodeFirst.SampleContext, CodeFirst]], EntityFramework" />
      </context>
    </contexts>
    ...
</entityFramework>

Для этого используется раздел contexts настроек Entity Framework. При структуре этого приложения эти настройки нужно указывать в файле Web.config веб-приложения, а не в проекте, где мы создавали модель. Обратите внимание, что в этом примере показано использование атрибута disableDatabaseInitialization. Если вы установите его в true, то сможете отключить автоматическую инициализацию базы данных в приложении, как мы делали это с использованием метода Initialize(null). В атрибуте type узла context указывается полное имя класса контекста, а также имя сборки, где содержится этот класс. В атрибуте type узла databaseInitializer указывается полный тип инициализатора, обратите внимание на синтаксис этой инструкции.

Создание базы данных с некоторыми данными по умолчанию

Ранее в этой статье вы видели как и когда создается база данных в Code-First и как этим процессом можно управлять. При этом создаваемая база данных всегда пуста, хотя возможны ситуации, когда вы захотите вставить некоторые начальные данные при создании базы. Например, вы можете добавить некоторые справочные таблицы, которые содержат статичную информацию - список стран, валют, список областей и населенных пунктов Российской Федерации и т.п.

Для этого вы можете использовать свой инициализатор данных, унаследованный от одного из стандартных инициализаторов и переопределить его метод Seed(). Допустим вам нужно добавить таблицу, со всеми крупными населенными пунктами России, чтобы можно было привязать адрес проживания покупателя в модели. Для этого давайте определим следующую модель:

public class Customer
{
     // ...

    // Город проживания покупателя
    public City Location { get; set; }

    [ForeignKey("Location")]
    public int LocationId { get; set; }
}

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

В таблице City мы будем хранить список городов. Чтобы заполнить эту таблицу автоматически при инициализации базы данных, можно использовать следующий код:

using System.Collections.Generic;
using System.Data.Entity;

namespace CodeFirst
{
    public class SampleInitializer 
        : DropCreateDatabaseIfModelChanges<SampleContext>
    {
        // В этом методе можно заполнить таблицу по умолчанию
        protected override void Seed(SampleContext context)
        {
            List<City> cities = new List<City>
            {
                new City { Name = "Москва" },
                new City { Name = "Санкт-Петербург" },
                new City { Name = "Казань" }
                // ...
            };

            foreach (City city in cities)
                context.Cities.Add(city);

            context.SaveChanges();
            base.Seed(context);
        }
    }

    public class SampleContext : DbContext
    {
        public SampleContext() : base("MyShop")
        {
            // Установить новый инициализатор
            Database.SetInitializer<SampleContext>(new SampleInitializer());
        }
        
        public DbSet<Customer> Customers { get; set; }
        public DbSet<City> Cities { get; set; }
    }
}

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

Добавление настроек базы данных при ее инициализации

Помимо добавления данных по умолчанию, в базе данных можно определить и другие настройки, которые будут применены при инициализации. Например, вы можете создать индекс для столбца FirstName таблицы Customers, чтобы ускорить поиск по имени покупателя в этой таблице. Чтобы определить различные настройки, вы можете использовать метод DbContext.Database.ExecuteSqlCommand(), в который передается произвольная SQL-команда для настройки различных аспектов базы данных. Этот метод вызывается также, в переопределенном методе Seed() объекта инициализации. В следующем примере показано, как задать индекс для столбца CustomerId:

protected override void Seed(SampleContext context)
{
    context.Database.ExecuteSqlCommand
         ("CREATE INDEX Index_Customer_Name ON Customers (CustomerId) INCLUDE (FirstName)");

    base.Seed(context);
}

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

Добавленный индекс в таблицу, после создания базы данных
Пройди тесты
Лучший чат для C# программистов