В процессе разработки проекта появилась следующая задача – добавить новое свойство в один из классов Модели. При этом он уже был связан с таблицей при помощи Entity Framework Code First. На первый взгляд, казалось, что все будет просто: изменить код и дать команду на изменение схемы базы данных. Но на практике оказалось все чуть-чуть сложнее из-за типа нового свойства - DateTime.
Моделируем ситуацию
Для примера создадим консольное приложение EFMigrationsDateTimeDemo. С помощью NuGet установим библиотеку Entity Framework, выполнив команду:
PM> install-package EntityFramework
Теперь добавим Модель UserProfile:
namespace EFMigrationsDateTimeDemo
{
using System;
public class UserProfile
{
public int Id { get; set; }
public string Name { get; set; }
}
}
Кроме того, потребуется класс контекста для работы с Entity Framework:
namespace EFMigrationsDateTimeDemo
{
using System.Data.Entity;
public class AppDbContext : DbContext
{
public DbSet<UserProfile> UserProfiles { get; set; }
}
}
Затем разместим код работы с базой данных в методе Main:
namespace EFMigrationsDateTimeDemo
{
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
WorkWithDb();
Console.WriteLine("Press any key ...");
Console.ReadKey(true);
}
static void WorkWithDb()
{
using (var context = new AppDbContext()) {
context.UserProfiles.Add(new UserProfile() {
Name = "John Doe"
});
context.SaveChanges();
var userProfiles = context.UserProfiles.AsEnumerable();
foreach (var user in userProfiles) {
Console.WriteLine("{0} - {1}", user.Id, user.Name);
}
}
}
}
}
Запустим приложение в первый раз для того, чтобы создать исходную базу данных. Обратите внимание, что используется соединение по умолчанию. При необходимости, можно задать параметры в App.Config.
Последним шагом в подготовке будет активация механизма автоматической миграции следующей командой в консоли NuGet:
PM> Enable-Migrations -EnableAutomaticMigrations
В результате будет создан файл Migrations\Configuration.cs.
Проблема
Давайте добавим свойство типа DateTime в UserProfile:
namespace EFMigrationsDateTimeDemo
{
using System;
public class UserProfile
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime LastVisit { get; set; }
}
}
Может возникнуть вопрос – почему не использовать Nullable<DateTime>? Здесь тип определяется бизнес-логикой задачи (дата обязательно должна быть указана). При этом, в дальнейшем, значения будут получены после импорта данных из других источников.
Теперь попробуем изменить схему базы данных командой Update-Database. И получим следующее сообщение об ошибке:
PM> Update-Database -Verbose
.........
The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.
The statement has been terminated.
Как показало небольшое исследование, причин ошибки здесь две:
- Несоответствие диапазонов значений свойства типа DateTime и колонки типа datetime в таблице.
- Возможны отличия при передаче формата текстового представления даты, если различаю��ся региональные настройки компьютера и таблицы в базе данных.
Первый вариант решения
В Entity Framework для создания SQL команд миграции используется класс MigrationSqlGenerator. Таким образом, необходимо предоставить его реализацию, которая преобразует DateTime с учетом диапазона и игнорируя текущие региональные настройки.
Задача может показаться сложной. Однако на практике все достаточно легко. В зависимости от того, какая база данных используется, необходимо в качестве базового класса взять:
- SqlServerMigrationSqlGenerator – для SQL Server / SQL Server Express / LocalDb.
- SqlCeMigrationSqlGenerator – для SQL Server Compact.
После этого остается только переопределить метод Generate(DateTime defaultValue).
Свойство типа DateTime будет представлено в базе данных полем datetime, которое может принимать значения от 1 января 1753 до 31 декабря 9999 года. Учтём это при формировании текстового представления даты. Кроме того, жестко определим его формат и используем CultureInfo.InvariantCulture, чтобы исключить влияние региональных настроек.
namespace EFMigrationsDateTimeDemo.Migrations
{
using System;
using System.Data.Entity.Migrations.Sql;
using System.Globalization;
internal class SqlServerMigrationSqlGeneratorFixed : SqlServerMigrationSqlGenerator
{
protected override string Generate(DateTime defaultValue)
{
var value = defaultValue.Ticks != 0 ?
defaultValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) :
"1753-01-01 00:00:00";
return string.Format("'{0}'", value);
}
}
}
Остается только указать Entity Framework использовать исправленную версию SQL генератора. Для этого в конструкторе класса Configuration добавим следующую строку:
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.SetSqlGenerator("System.Data.SqlClient", new SqlServerMigrationSqlGeneratorFixed());
}
Обратите внимание, что здесь указано имя провайдера для SQL Server. Для SQL Server Compact необходимо указать System.Data.SqlServerCe.4.0.
Давайте теперь повторим попытку модифицировать схему базы данных:
PM> Update-Database -Verbose
Using NuGet project 'EFMigrationsDateTimeDemo'.
Using StartUp project 'EFMigrationsDateTimeDemo'.
Target database is: 'EFMigrationsDateTimeDemo.AppDbContext'
(DataSource: .\SQLEXPRESS, Provider: System.Data.SqlClient, Origin: Convention).
No pending explicit migrations.
Applying automatic migration: 201205020748408_AutomaticMigration.
ALTER TABLE [UserProfiles] ADD [LastVisit] [datetime] NOT NULL DEFAULT '1753-01-01 00:00:00'
[Inserting migration history record]
Миграция прошла успешно.
Второй вариант решения
При работе с Microsoft SQL Server, в качестве альтернативы datetime, можно использовать datetime2. Он может принимать значения от 1 января 0001 года до 31 декабря 9999, что совпадает с диапазоном DateTime. Задать данный тип для соответствующего свойства Модели можно с помощью FluentAPI:
namespace EFMigrationsDateTimeDemo
{
using System.Data.Entity;
public class AppDbContext : DbContext
{
public DbSet<UserProfile> UserProfiles { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<UserProfile>().
Property(p => p.LastVisit)
.HasColumnType("datetime2")
.HasPrecision(0)
.IsRequired();
}
}
}
В этом случае, создание SQL генератора может потребоваться только для определения формата текстового представления даты (что не всегда необходимо):
namespace EFMigrationsDateTimeDemo.Migrations
{
using System;
using System.Data.Entity.Migrations.Sql;
using System.Globalization;
internal class SqlServerMigrationSqlGeneratorFixed : SqlServerMigrationSqlGenerator
{
protected override string Generate(DateTime defaultValue)
{
return string.Format("'{0}'",
defaultValue.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
}
}
}
Небольшой совет - автозапуск миграции при изменении Модели
При запуске проекта можно автоматически выполнять миграцию базы данных, если до этого была изменена модель. Для этого достаточно в начале работы приложения (метод Main() или Application_Start() для ASP.NET/ASP.NET MVC) добавить следующий код:
static void Main(string[] args)
{
Database.SetInitializer(
new MigrateDatabaseToLatestVersion<AppDbContext, Migrations.Configuration>());
WorkWithDb();
Console.WriteLine("Press any key ...");
Console.ReadKey(true);
}