[Из песочницы] Создание пользовательских миграционных операций в Entity Framework 6
Миграции в Entity Framework (EF) представляют собой строго типизированный подход для выполнения распространенных операций, таких как создание, изменение и удаление таблиц, столбцов, индексов, и т.д. Однако реализация базовых операций достаточно ограничена и не поддерживает весь спектр параметров, которые поддерживает та или иная СУБД.До EF 6, единственным способом обхода данного ограничения было использование операции Sql, которая позволяет выполнить произвольную команду SQL при выполнении миграции. В EF 6 также появилась возможность реализации пользовательских строго типизированных операций.
Создание собственных операцийБазовая реализация операции CreateIndex позволяет задать список колонок, по которым строится индекс, а также позволяет указать является ли индекс уникальным и/или кластерным. Однако, например, команда CREATE INDEX Microsoft SQL Server поддерживает так же указание направлений сортировки, задание списка включенных колонок, параметров хранения и других дополнительных ограничений.Рассмотрим реализацию данной расширенной операции миграции.
Для начала создадим класс ExtendedCreateIndexOperation, который наследуется от абстрактного класса MigrationOperation и переопределим свойство IsDestructiveChange, которое указывает, может ли наша операция привести к потере данных.
Начнем с возможности поддержки указания направлений сортировки по колонкам. Для этого создадим вспомогательный класс IndexColumnModel, который будет в себе содержать информацию о названии колонки и направление сортировки по ней.
IndexColumnModel.cs namespace CustomMigrations.Infrastructure.Migrations.Models { using System;
///
///
///
В результате получим следующий класс:
ExtendedCreateIndexOperation.cs namespace CustomMigrations.Infrastructure.Migrations.Operations { using System; using System.Collections.Generic; using System.Data.Entity.Migrations.Model; using System.Linq; using Models;
///
///
///
///
///
///
///
///
///
///
///
///
public override bool IsDestructiveChange { get { return false; } } } } Подключение операций Хорошо, операция создана, но необходимо добавить возможность ее использования.Для этого добавим метод расширения CreateIndex, который будет создавать экземпляр класса ExtendedCreateIndexOperation, передавать ему необходимые значения параметров и добавлять его в список операций текущей миграции.
DbMigrationExtensions.cs namespace CustomMigrations.Infrastructure.Extensions { using System; using System.Collections.Generic; using System.Data.Entity.Migrations; using System.Data.Entity.Migrations.Infrastructure; using System.Linq; using Migrations.Models; using Migrations.Operations;
internal static class DbMigrationExtensions
{
///
if (string.IsNullOrWhiteSpace (table)) { throw new ArgumentException («Table name is null or whitespace.», «table»); }
if (columns == null) { throw new ArgumentNullException («columns»); }
if (! columns.Any ()) { throw new ArgumentException («Columns collection is empty.», «columns»); }
var createIndexOperation = new ExtendedCreateIndexOperation (anonymousArguments) { Table = table, IsUnique = unique, Name = name, IsClustered = clustered, Where = where, With = with, On = on, FileStreamOn = fileStreamOn };
foreach (IndexColumnModel column in columns) { createIndexOperation.Columns.Add (column); }
if (includes!= null) { foreach (string column in includes) { createIndexOperation.Includes.Add (column); } }
((IDbMigration) migration).AddOperation (createIndexOperation); } } } Генерация SQL кода Для того чтобы научить EF преобразовывать пользовательские операции в SQL код, необходимо расширить возможности имеющегося базового генератора.Для этого необходимо создать наследника от класса SqlServerMigrationSqlGeneratorи и переопределить метод Generate (MigrationOperation). Данный метод вызывается только при обработке операций, которые неизвестны базовому генератору SQL.
Опишем необходимые преобразования из нашей операции в соответствующее выражение на языке SQL.
CustomSqlServerMigrationSqlGenerator.cs namespace CustomMigrations.Infrastructure.Migrations { using System; using System.Collections.Generic; using System.Data.Entity.Migrations.Model; using System.Data.Entity.Migrations.Utilities; using System.Data.Entity.SqlServer; using System.Linq; using Models; using Operations;
///
///
var createIndexOperation = migrationOperation as ExtendedCreateIndexOperation; if (createIndexOperation == null) { return; }
Generate (createIndexOperation); }
///
using (IndentedTextWriter writer = Writer ()) { writer.Write («CREATE »);
if (createIndexOperation.IsUnique) { writer.Write («UNIQUE »); }
if (createIndexOperation.IsClustered) { writer.Write («CLUSTERED »); }
writer.Write («INDEX »); writer.WriteLine (Quote (createIndexOperation.Name)); writer.Indent++; writer.Write («ON »); writer.Write (Name (createIndexOperation.Table));
writer.Write (»(»); writer.Write (string.Join (»,», createIndexOperation.Columns.Where (c => c!= null && ! string.IsNullOrWhiteSpace (c.Name)) .Select (c => Quote (c.Name) + » » + SortDirectionDescriptionMap[c.SortDirection]))); writer.Write (»)»);
// Skip the INCLUDE part for clustered indexes. if (! createIndexOperation.IsClustered && createIndexOperation.Includes.Count > 0) { writer.WriteLine (); writer.Write («INCLUDE (»); writer.Write (string.Join (»,», createIndexOperation.Includes.Where (c =>! string.IsNullOrWhiteSpace©).Select (Quote))); writer.Write (»)»); }
if (! string.IsNullOrWhiteSpace (createIndexOperation.Where)) { writer.WriteLine (); writer.Write («WHERE »); writer.Write (createIndexOperation.Where); }
if (! string.IsNullOrWhiteSpace (createIndexOperation.With)) { writer.WriteLine (); writer.Write («WITH (»); writer.Write (createIndexOperation.With); writer.Write (»)»); }
if (! string.IsNullOrWhiteSpace (createIndexOperation.On)) { writer.WriteLine (); writer.Write («ON »); writer.Write (createIndexOperation.On); }
if (! string.IsNullOrWhiteSpace (createIndexOperation.FileStreamOn)) { writer.WriteLine (); writer.Write («FILESTREAM_ON »); writer.Write (createIndexOperation.On); }
Statement (writer); } } } } Как видите, создание операций позволяет также добавить в них дополнительную логику проверок (в данном случае пропускается генерация кода для включенных колонок, в случае, если индекс является кластерным).
Итак, все необходимые приготовления завершены, и теперь необходимо подключить наш генератор.
Его можно зарегистрировать в соответствующих настройках миграций:
Configuration.cs namespace CustomMigrations.Migrations { using System.Data.Entity; using System.Data.Entity.Migrations; using System.Data.Entity.SqlServer;
internal sealed class Configuration: DbMigrationsConfiguration
CustomDbConfiguration.cs namespace CustomMigrations.Migrations { using System.Data.Entity; using System.Data.Entity.SqlServer;
public class CustomDbConfiguration: DbConfiguration { public CustomDbConfiguration () { SetMigrationSqlGenerator (SqlProviderServices.ProviderInvariantName, () => new CustomSqlServerMigrationSqlGenerator ()); } } } Небольшие улучшения Для того чтобы удобнее было добавлять колонки без явного создания экземпляров класса IndexColumnModel добавим несколько методов расширения для строк, а также оператор неявного преобразования типов в сам класс.StringExtensions.cs namespace CustomMigrations.Infrastructure.Extensions { using System; using Migrations.Models;
internal static class StringExtensions
{
///
return new IndexColumnModel {Name = value, SortDirection = SortDirection.Ascending}; }
///
return new IndexColumnModel {Name = value, SortDirection = SortDirection.Descending}; } } } IndexColumnModel.cs namespace CustomMigrations.Infrastructure.Migrations.Models { using System;
///
///
///
///
…
Sql («CREATE NONCLUSTERED INDEX [IX_Column1_Column2_Column3] ON [dbo].[TestTable]» + »(» + » [Column1] DESC,» + » [Column2] ASC,» + » [Column3] ASC» + »)» + » INCLUDE (» + » [Column4],» + » [Column5]» + »)» + » WHERE [Column6] = 'Some filter'» + » WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]»);
… Стало:
…
this.CreateIndex («dbo.TestTable», columns: new[] {«Column1».Descending (), «Column2».Ascending (), «Column3»}, includes: new[] {«Column4», «Column5»}, where:»[Column6] = 'Some filter'», with: «SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF», on:»[PRIMARY]» );
… Результат работы операции можно оценить, выполнив команду Update-Database -Script в консоли Package Manager Console.
…
CREATE INDEX [IX_Column1_Column2_Column3] ON [dbo].[TestTable]([Column1] DESC, [Column2] ASC, [Column3] ASC) INCLUDE ([Column4], [Column5]) WHERE [Column6] = 'Some filter' WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]
… Полный исходный код доступен здесь.