Что значит имя 3: База данных для SPA или Code First на EntityFramework

ru-RU | создано: 18.06.2014 | опубликовано: 18.06.2014 | обновлено: 01.01.2018 | просмотров за всё время: 6007

В предыдущей статье из цикла “Что значит имя” было показано что из себя представляет DurandalJS и как с ним работать. В этой статье будем работать с EntityFramework: создадим базу данных по принципу Code First; создадим классы сущностей, настроим SQL-подключение.

Приступая к работе

Перед тем как начать использовать BreezeJS следует подготовить базу данных. Именно этим мы и займемся в этой статье. Но для начала имеет смысл показать какая база данных "завалялась" у меня в папке под названием “Может пригодиться”, чтобы вы могли четко представлять о чем идет речь.

Схема базы данных

У меня давно “пылилась” база данных с толкованием имен. Она настолько простая, что я приведу схему:

01

Таблица “Letters” (1) содержит в себе только толкования каждой буквы, которая встречается в имени. А в таблице “Names” (2) толкования имен. Под номером 3 обозначены таблицы, которые создал EntityFramework для поддержания функционала ASP.NET Identity для реализации аутентификации и авторизации на сайте. Об этом мы возможно поговорим в будущих статьях. Еще следует отметить, что таблицы между собой никак не связаны.

База данных у меня есть, но использовать ее по прямому назначению я не буду. Мы создадим новую базу данных по принципу “Code First”, то есть мы создадим классы, а базу данных за нас создаст EntityFramework. После этого я перенесу из моей архивной базы в новую базу только данные.

Хочу подчеркнуть, что приведенная на картинке БД - всего лишь только для примера. Мы будем создавать новую базу. А из этой я только скопирую данные, но не структуру.

Создаем БД по принципу “Code First”

Для начала нам нужны классы, потому что “Code” означает не что иное как “код”, а значит классы. Я уже давно пришел к тому, что всё должно быть на своём месте, вот и в этот раз не могу поступить иначе. Я привык размещать классы моделей в отдельном проекте одного решения (solution).

02

Создаем новый проект:

03

Во вновь созданном проекте я удалил сгенерированный по умолчанию Visual Studio 2013 класс Class1.

Модели для Code First

А теперь создаем первый класс под названием LetterInfo... Хотя нет, давайте познакомимся с некоторыми принципами ООП, и в частности, один из основополагающих – “Наследование”. Создадим класс, который будет являться базовым для двух последующих. Класс назовем ModelBase:

public class ModelBase
{
  public int Id { get; set; }

  public string Name { get; set; }

  public string  Content { get; set; }
}

Сделали мы это потому, что каждый из двух последующих классов: LetterInfo и NameInfo будут содержать такие же свойства:

public class LetterInfo : ModelBase
{

}

public class NameInfo: ModelBase
{
    
}

В классе NameInfo не хватает еще одного свойства (исходя из схемы примерной базы данных), которого нет в базовом классе, добавим его:

public class NameInfo: ModelBase
{
  public string Gender { get; set; }
}

Вот и готова архитектурная модель приложения.

Атрибуты, классы, свойства, модели

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

public class ModelBase
{
  /// <summary>
  /// Идентификтор
  /// </summary>
  [Key]
  [Display(Name = "Идентификтор")]
  public int Id { get; set; }

  public string Name { get; set; }

  public string  Content { get; set; }
}

Атрибут Key укажет EntityFramework, что данное поле является PrimaryKey (первичным ключом) для классов (таблиц), которые будут использовать базовый класс в качестве наследника.

Вообще, стоит сделать пояснение. На самом деле EntityFramework очень “умный” фреймворк. Если вы не установите атрибут Key у этого свойства, то EntityFramework всё равно отметить это поле как PrimaryKey, то есть сделает первичным ключом. Платформа EntityFramework полагается на то, что каждая сущность имеет значение ключа, которое используется для отслеживания сущностей. Одно из соглашений, на которое полагается Code First, заключается в том, каким образом определяется ключевое свойство в каждом классе Code First. Это соглашение выполняет поиск свойства Id или свойства, которое объединяет “имя класса” и Id, например LetterId. Свойство сопоставляется со столбцом первичного ключа в базе данных.

В нашем случае свойство Id будет в каждом из классов наследников. Я всё равно всегда ставлю этот атрибут, чаще по привычке. Следующим по листингу идет атрибут Display. Этот атрибут не столько для EntityFramework, сколько для форм ASP.NET MVC и для читаемости кода в дальнейшем. Использование в разметке Razor:

@Html.DisplayFor(x=>x.Name)

Форма отобразит значение свойства именно из атрибута DisplayAttribute. Я также ставлю его практически всегда по привычке, потому что в нем можно указать именование поля по-русски, хотя в нашем приложении этот атрибут бесполезен, потому что рендеринг формы производит не Razor, а DurandalJS средствами KnockoutJS.

Следующее свойство – “Name”. Но в нашем случае, это свойство использует разные настройки атрибутов для каждого из наследников. Например, в классе LetterInfo данное свойство имеет название (DisplayAttribute) “Буква”, в классе NameInfo – “Имя”.

Создадим класс метаданных прямо в классах наследниках и переопределим метаданные базового класса в них. Сначала разберемся с классом LetterInfo:

[MetadataType(typeof(LetterInfoMetadata))]
public class LetterInfo : ModelBase {

  internal sealed class LetterInfoMetadata
  {
    /// <summary>
    /// Буква
    /// </summary>
    [Display(Name = "Буква")]
    [Required(ErrorMessage = "Буква - обязательное поле")]
    [StringLength(50, ErrorMessage = "Буква не может быть длиннее 50 символов")]
    public string Name { get; set; }

    /// <summary>
    /// Описание
    /// </summary>
    [Display(Name = "Описание")]
    [Required(ErrorMessage = "Описание - обязательное поле")]
    [StringLength(1000)]
    public string Content { get; set; }
  }
}

Для начала посмотрите на строку 4, где определяется класс метаданных LetterInfoMetadata. Обратите внимание, что название свойств совпадает с названием свойств в базовом классе ModelBase. Атрибут Display мы уже описали, следующий  - Required (строка 10 ). Этот атрибут указывает EntityFramework пометить свойство как обязательное, то есть запись в таблицу не попадет пока не будут заполнены все обязательные поля. Далее атрибут StringLength (строка 11), который устанавливает ограничение на длину строки в таблице SQL. Для свойства Content настроим свои атрибуты.

Следующий класс NameInfo:

[MetadataType(typeof(NameInfoMetadata))]
public class NameInfo : ModelBase {

  internal sealed class NameInfoMetadata {

    /// <summary>
    /// Имя
    /// </summary>
    [Display(Name = "Имя")]
    [Required(ErrorMessage = "Имя - обязательное поле")]
    [StringLength(50, ErrorMessage = "Имя не может быть длиннее 50 символов")]
    public string Name { get; set; }

    /// <summary>
    /// Описание
    /// </summary>
    [Display(Name = "Описание")]
    [Required(ErrorMessage = "Описание - обязательное поле")]
    public string Content { get; set; }
  }

  /// <summary>
  /// Пол (М/Ж)
  /// </summary>
  [Display(Name = "Пол (М/Ж)")]
  [Required(ErrorMessage = "Пол (М/Ж) - обязательное поле")]
  [StringLength(1, ErrorMessage = "Пол (М/Ж) не может быть длиннее 1 символов")]
  public string Gender { get; set; }
}

Опустим комментарии, потому как класс NameInfo подобен предыдущему за исключением, одного свойства Gender, которое реализует разделение по половому признаку для имен. Таким образом, структура классов (моделей) готова.

Лирическое отступление: вы можете возразить, к чему такая сложность при формировании сущностей. Не буду отрицать, это не самый простой способ реализации. Как альтернативное решение, гораздо проще было бы создать два класса, и в каждом реализовать свои собственные свойства с разными атрибутами. Тезисы в защиту проделанной работы:

  • Использование принципов и правил ООП (Объектно-ориентированного программирования) хороший тон для любого программиста;
  • При дальнейшем развитии проекта изменение базовых свойств можно осуществлять централизовано;
  • В любом случае, программировать надо правильно и развивать правильные привычки.

Концепция “Правильно и Хорошо” (© Calabonga) подводит к выводу: приведенный способ – “Правильно”, предложенная альтернатива – “Хорошо”

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

Например, для класса LetterInfo я установил такой атрибут:

[Table("Letters")]
[MetadataType(typeof(LetterInfoMetadata))]
public class LetterInfo : ModelBase {

  // много букв
  // опущено для краткости
}

И, следовательно, таблица, которую создаст для меня EntityFramework, в которой будут храниться названия букв, будет носить название “Letters”, а не “LetterInfo”, если бы я не поставил этот атрибут.

Проделаем то же самое для второй сущности:

[Table("Names")]
[MetadataType(typeof(NameInfoMetadata))]
public class NameInfo : ModelBase {

  // убрано для краткости
   // названия свойств
}

С такими атрибутами EntityFramework создаст для меня две таблицы с именами Letters и Names. Пришло время познакомиться с EntityFramework поближе.

DbContext или знакомство с EntityFramework

Первым делом снова создадим новый проект в нашем решении (solution), чтобы проект с данными был обособлен от Web-реализации клиента. Я назвал новый проект “InfoNames.Data”. И я снова удалил файл Class1, который Visual Studio 2013 любезно для меня сгенерировала. Теперь в моем решении (solution) три проекта:

05

Первым делом надо установить nuget-пакет, который позволит начать работу с EntityFramework. Выберите проект InfoNames.Data правой кнопкой выберите пункт Manage Nuget Packages. В менеджере пакетов выберите слева хранилище “Microsoft and NET”, EntityFramework-пакет будет на первом месте (в силу частоты загрузок), если его нет на первом месте, введите EntityFramework в поиске над колонкой информации справа.

06

Нажмите кнопу Install.

Следующим этапом надо добавить ссылку на проект InfoNames.Models. Выберите снова проект InfoNames.Data правой кнопкой вызовите меню и найдите пункт Add –> Refferences:

07

Отметьте проект галкой и нажмите Ok. Хорошо. Теперь готово всё, чтобы начать создавать контекст БД, для этого создаем новый класс ApplicationDbContext в проекте InfoNames.Data:

public class ApplicationDbContext : DbContext {

    public IDbSet<LetterInfo> Letters { get; set; }
    public IDbSet<NameInfo> Names { get; set; }

}

Класс унаследуем от специального базового класса EntityFramework, который называется DbContext. Надо бы переопределить строку подключения базы данных, иначе в противном случае, EntityFramework создаст LocalDb базу в папке App_Data проекта InfoNames.Web. У меня установлен SQL server 2012, и я хочу чтобы база данных была создана в нем, а не в папке App_Data. Добавим перегрузку конструктора:

public class ApplicationDbContext : DbContext {

  public ApplicationDbContext()
    : base("DefaultConnection") {
  }

  public IDbSet<LetterInfo> Letters { get; set; }
  public IDbSet<NameInfo> Names { get; set; }

}

В конструкторе базового класса мы указали название строки подключения “DefaultConnection”. Давайте теперь строку подключения к SQL server добавим в web.config проекта InfoNames.Web.

<connectionStrings>
  <add name="DefaultConnection" 
      connectionString="Data Source=(local);Initial Catalog=infonamesdb;Integrated Security=True" 
      providerName="System.Data.SqlClient" />
</connectionStrings>

Обратите внимание на параметр “Initial Catalog”, он указывает название для базы данных, которая будет создана в SQL сервере. В моем случае будет создана база “infonamesdb”. Теперь в проект InfoNames.Web надо добавить ссылку на проект InfoNames.Data:

08

Выберите проект, отметив его галкой и нажмите Ok. Хорошо, поехали дальше.

Команда Enable-Migrations

Давайте снова вернемся к проекту InfoNames.Data, надо включить миграции (migrations). Миграции для базы данных позволят вносить изменения в структуру модели данных приложения “автоматически” при вызове команды Update-Database. Другими словами, если вы что-то упустили при разработки модели, например, забыли какой-нибудь атрибут или пропустили какое-то свойство, то при добавлении его в модель, потребуется выполнить команду Update-Database чтобы обновления “пришли” на SQL server. EntityFramework проделает основную работу за вас, если будут включены миграции, в противном случае, придется полностью удалять базу и генерировать ее заново. А если у вас уже есть данные?..

Внимание: Не все команды по изменению схемы базы данных могут автоматически применить полученные изменения, некоторые изменения придется обрабатывать “вручную” написав команды (migrations) собственноручно. Но скорее всего, особенно в простых структурах БД за вас всё сделает автомиграции.

В Package Manager Console выполните команду:

PM> Enable-Migrations -ProjectName InfoNames.Data
Checking if the context targets an existing database...
Code First Migrations enabled for project InfoNames.Data.
PM> 

В проекте InfoNames.Data появилась папка Migrations с файлом Configuration.cs

namespace InfoNames.Data.Migrations
{
  using System.Data.Entity.Migrations;

  internal sealed class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(ApplicationDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }
}

Миграции мы подключили, теперь давайте настроим их работу на “автомат”, для этого установите свойства в строке 9 значение true и добавьте еще одно:

public Configuration() {
  AutomaticMigrationsEnabled = true;
  AutomaticMigrationDataLossAllowed = true;
}

Вот теперь окончательно всё настроено. Можно перейти в Package Manager Console и выполнить команду Update-Database:

PM> Update-Database
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
No pending explicit migrations.
Applying automatic migration: 201406280303101_AutomaticMigration.
Running Seed method.
PM> 

Ура! выполнено без ошибок. В строке 3 указывается, что явных миграций не найдено (те что пишутся “вручную”), а строке 4 обнаружены обновления в структуру данных (естественно, она же только что создана) и автомиграция применена.В строке 5 уведомляется, что запушен метод Seed. Результатом выполнения команды явилась новая база данных в списке SQL-сервера:

созданая база данных EntityFramework

 

Если развернуть и посмотреть структуру таблиц:

таблицы созданные EntityFramework

То, что и требовалось получить по результатам статьи. Далее я экспортирую данные из моей старой базы данных в новую (это останется “за кадром”). А в следующей статье начнем работу с BreezeJS.

Заключение

В качестве заключения хотелось бы напомнить, что можно экспериментировать, скачав текущую версию проекта с Github.