- PVSM.RU - https://www.pvsm.ru -

Создание пользовательских миграционных операций в Entity Framework 6

Миграции в Entity Framework (EF) представляют собой строго типизированный подход для выполнения распространенных операций, таких как создание, изменение и удаление таблиц, столбцов, индексов, и т.д. Однако реализация базовых операций достаточно ограничена и не поддерживает весь спектр параметров, которые поддерживает та или иная СУБД.

До EF 6, единственным способом обхода данного ограничения было использование операции Sql [1], которая позволяет выполнить произвольную команду SQL при выполнении миграции. В EF 6 также появилась возможность реализации пользовательских строго типизированных операций.

Создание собственных операций

Базовая реализация операции CreateIndex позволяет задать список колонок, по которым строится индекс, а также позволяет указать является ли индекс уникальным и/или кластерным. Однако, например, команда CREATE INDEX [2] Microsoft SQL Server поддерживает так же указание направлений сортировки, задание списка включенных колонок, параметров хранения и других дополнительных ограничений.

Рассмотрим реализацию данной расширенной операции миграции.

Для начала создадим класс ExtendedCreateIndexOperation, который наследуется от абстрактного класса MigrationOperation и переопределим свойство IsDestructiveChange, которое указывает, может ли наша операция привести к потере данных.

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

IndexColumnModel.cs

namespace CustomMigrations.Infrastructure.Migrations.Models
{
    using System;

    /// <summary>
    ///     Index column sort direction.
    /// </summary>
    internal enum SortDirection
    {
        Ascending,
        Descending
    }

    /// <summary>
    ///     Represents information about an index column.
    /// </summary>
    internal class IndexColumnModel
    {
        /// <summary>
        ///     Gets or sets the name of the column.
        /// </summary>
        /// <value>
        ///     The name of the column.
        /// </value>
        public string Name { get; set; }

        /// <summary>
        ///     Gets or sets the sort direction.
        /// </summary>
        /// <value>
        ///     The sort direction.
        /// </value>
        public SortDirection SortDirection { get; set; }
    }
}

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

В результате получим следующий класс:

ExtendedCreateIndexOperation.cs

namespace CustomMigrations.Infrastructure.Migrations.Operations
{
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Migrations.Model;
    using System.Linq;
    using Models;

    /// <summary>
    ///     Represents creating an extended database index.
    /// </summary>
    internal class ExtendedCreateIndexOperation : MigrationOperation
    {
        private readonly ICollection<IndexColumnModel> _columns = new List<IndexColumnModel>();
        private readonly ICollection<string> _includes = new List<string>();
        private string _name;
        private string _table;

        /// <summary>
        ///     Initializes a new instance of the <see cref="ExtendedCreateIndexOperation" /> class.
        /// </summary>
        /// <param name="anonymousArguments">
        ///     Use anonymous type syntax to specify arguments e.g. 'new { SampleArgument = "MyValue" }'.
        /// </param>
        public ExtendedCreateIndexOperation(object anonymousArguments = null)
            : base(anonymousArguments)
        {
        }

        /// <summary>
        ///     Gets the columns collection.
        /// </summary>
        /// <value>
        ///     The columns collection.
        /// </value>
        public ICollection<IndexColumnModel> Columns
        {
            get { return _columns; }
        }

        /// <summary>
        ///     Gets the non-key columns to be added to the leaf level of the nonclustered index.
        /// </summary>
        /// <value>
        ///     The the non-key columns to be added to the leaf level of the nonclustered index.
        /// </value>
        public ICollection<string> Includes
        {
            get { return _includes; }
        }

        /// <summary>
        ///     Gets or sets the name of the table.
        /// </summary>
        /// <value>
        ///     The name of the table.
        /// </value>
        /// <exception cref="System.ArgumentException">Table name is null or whitespace.;value</exception>
        public string Table
        {
            get { return _table; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new ArgumentException("Table name is null or whitespace.", "value");
                }
                _table = value;
            }
        }

        /// <summary>
        ///     Gets or sets the name of the index.
        /// </summary>
        /// <value>
        ///     The name of the index.
        /// </value>
        public string Name
        {
            get
            {
                return _name ??
                       IndexOperation.BuildDefaultName(
                           _columns.Where(c => c != null && !string.IsNullOrWhiteSpace(c.Name)).Select(c => c.Name));
            }
            set { _name = value; }
        }

        /// <summary>
        ///     Gets or sets a value indicating if this is a unique index.
        /// </summary>
        /// <value>
        ///     The value indicating if this is a unique index.
        /// </value>
        public bool IsUnique { get; set; }

        /// <summary>
        ///     Gets or sets whether this is a clustered index.
        /// </summary>
        /// <value>
        ///     Whether this is a clustered index.
        /// </value>
        public bool IsClustered { get; set; }

        /// <summary>
        ///     Gets or sets the WHERE option (<filter_predicate>).
        /// </summary>
        /// <value>
        ///     The WHERE option.
        /// </value>
        public string Where { get; set; }

        /// <summary>
        ///     Gets or sets the WITH option (<relational_index_option> [ ,...n ]).
        /// </summary>
        /// <value>
        ///     The WITH option.
        /// </value>
        public string With { get; set; }

        /// <summary>
        ///     Gets or sets the ON option (partition_scheme_name ( column_name ) | filegroup_name | default).
        /// </summary>
        /// <value>
        ///     The ON option.
        /// </value>
        public string On { get; set; }

        /// <summary>
        ///     Gets or sets the FILESTREAM_ON option (filestream_filegroup_name | partition_scheme_name | "NULL").
        /// </summary>
        /// <value>
        ///     The FILESTREAM_ON option.
        /// </value>
        public string FileStreamOn { get; set; }

        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
    {
        /// <summary>
        ///     Adds an operation to create an table index.
        /// </summary>
        /// <param name="migration">The database migration instance.</param>
        /// <param name="table">
        ///     The name of the table to create the index on. Schema name is optional, if no schema is specified
        ///     then dbo is assumed.
        /// </param>
        /// <param name="columns">The name of the columns to create the index on.</param>
        /// <param name="includes">The includes.</param>
        /// <param name="unique">
        ///     A value indicating if this is a unique index. If no value is supplied a non-unique index will be
        ///     created.
        /// </param>
        /// <param name="name">
        ///     The name to use for the index in the database. If no value is supplied a unique name will be
        ///     generated.
        /// </param>
        /// <param name="clustered">A value indicating whether or not this is a clustered index.</param>
        /// <param name="where">The WHERE option (<filter_predicate>).</param>
        /// <param name="with">The WITH option (<relational_index_option> [ ,...n ]).</param>
        /// <param name="on">The ON option (partition_scheme_name ( column_name ) | filegroup_name | default).</param>
        /// <param name="fileStreamOn">The FILESTREAM_ON option (filestream_filegroup_name | partition_scheme_name | "NULL").</param>
        /// <param name="anonymousArguments">
        ///     Additional arguments that may be processed by providers. Use anonymous type syntax to
        ///     specify arguments e.g. 'new { SampleArgument = "MyValue" }'.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        ///     migration
        ///     or
        ///     columns
        /// </exception>
        /// <exception cref="System.ArgumentException">
        ///     Table name is null or whitespace.;table
        ///     or
        ///     Columns collection is empty.;columns
        /// </exception>
        public static void CreateIndex(this DbMigration migration,
            string table,
            ICollection<IndexColumnModel> columns,
            ICollection<string> includes = null,
            bool unique = false,
            string name = null,
            bool clustered = false,
            string where = null,
            string with = null,
            string on = null,
            string fileStreamOn = null,
            object anonymousArguments = null)
        {
            if (migration == null)
            {
                throw new ArgumentNullException("migration");
            }

            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;

    /// <summary>
    ///     Custom provider to convert provider agnostic migration operations into SQL commands
    ///     that can be run against a Microsoft SQL Server database.
    /// </summary>
    internal sealed class CustomSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
    {
        private static readonly IDictionary<SortDirection, string> SortDirectionDescriptionMap = new Dictionary
            <SortDirection, string>
        {
            {SortDirection.Ascending, "ASC"},
            {SortDirection.Descending, "DESC"}
        };

        /// <summary>
        ///     Generates SQL for a <see cref="T:System.Data.Entity.Migrations.Model.MigrationOperation" />.
        ///     Allows derived providers to handle additional operation types.
        ///     Generated SQL should be added using the Statement method.
        /// </summary>
        /// <param name="migrationOperation">The operation to produce SQL for.</param>
        /// <exception cref="System.ArgumentNullException">migrationOperation</exception>
        protected override void Generate(MigrationOperation migrationOperation)
        {
            if (migrationOperation == null)
            {
                throw new ArgumentNullException("migrationOperation");
            }

            var createIndexOperation = migrationOperation as ExtendedCreateIndexOperation;
            if (createIndexOperation == null)
            {
                return;
            }

            Generate(createIndexOperation);
        }

        /// <summary>
        ///     Generates SQL for a
        ///     <see cref="T:CustomMigrations.Infrastructure.Migrations.Operations.ExtendedCreateIndexOperation" />.
        /// </summary>
        /// <param name="createIndexOperation">The operation to produce SQL for.</param>
        /// <exception cref="System.ArgumentNullException">createIndexOperation</exception>
        private void Generate(ExtendedCreateIndexOperation createIndexOperation)
        {
            if (createIndexOperation == null)
            {
                throw new ArgumentNullException("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(c)).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<DbContext>
    {
        public Configuration()
        {
            SetSqlGenerator(SqlProviderServices.ProviderInvariantName, new CustomSqlServerMigrationSqlGenerator());
        }
    }
}

Или в настройках DbConfigration приложения:

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
    {
        /// <summary>
        ///     Creates the index column model with the ascending sort direction.
        /// </summary>
        /// <param name="value">The column name.</param>
        /// <returns>The index column model.</returns>
        /// <exception cref="System.ArgumentNullException">value</exception>
        public static IndexColumnModel Ascending(this string value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            return new IndexColumnModel {Name = value, SortDirection = SortDirection.Ascending};
        }

        /// <summary>
        ///     Creates the index column model with the descending sort direction.
        /// </summary>
        /// <param name="value">The column name.</param>
        /// <returns>The index column model.</returns>
        /// <exception cref="System.ArgumentNullException">value</exception>
        public static IndexColumnModel Descending(this string value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            return new IndexColumnModel {Name = value, SortDirection = SortDirection.Descending};
        }
    }
}

IndexColumnModel.cs

namespace CustomMigrations.Infrastructure.Migrations.Models
{
    using System;

    /// <summary>
    ///     Index column sort direction.
    /// </summary>
    internal enum SortDirection
    {
        Ascending,
        Descending
    }

    /// <summary>
    ///     Represents information about an index column.
    /// </summary>
    internal class IndexColumnModel
    {
        /// <summary>
        ///     Gets or sets the name of the column.
        /// </summary>
        /// <value>
        ///     The name of the column.
        /// </value>
        public string Name { get; set; }

        /// <summary>
        ///     Gets or sets the sort direction.
        /// </summary>
        /// <value>
        ///     The sort direction.
        /// </value>
        public SortDirection SortDirection { get; set; }

        /// <summary>
        ///     Performs an implicit conversion from <see cref="System.String" /> to <see cref="IndexColumnModel" />.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>
        ///     The result of the conversion.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">value</exception>
        public static implicit operator IndexColumnModel(string value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            return new IndexColumnModel {Name = value, SortDirection = SortDirection.Ascending};
        }
    }
}

Проверка результатов работы

Для иллюстрации результатов работы сравним создание индекса с параметрами по старинке и с использованием нашей операции.

Было:

...

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]

...

Полный исходный код доступен здесь [3].

Автор: sndr

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/net/67412

Ссылки в тексте:

[1] Sql: http://msdn.microsoft.com/ru-ru/library/system.data.entity.migrations.dbmigration.sql(v=vs.113).aspx

[2] CREATE INDEX: http://msdn.microsoft.com/ru-ru/library/ms188783.aspx

[3] здесь: https://bitbucket.org/izhogoliev/efcustommigrations

[4] Источник: http://habrahabr.ru/post/233207/