Как добавить индекс в несколько столбцов с сортировкой по ASC/DESC с использованием Fluent API?

У меня есть приложение MVC ASP.NET, использующее Entity Framework 6 - Code First.

Используя API Fluent, как добавить индекс в несколько столбцов с сортировкой ASC/DESC, которая отличается для каждого столбца?

Я видел много примеров, используя несколько столбцов, но не способ установить порядок сортировки столбцов в индексе.

Table
-----
Id
Type
DateFor
DateCreated
Value

Мне нужен индекс в следующих столбцах: Type (ASC), DateFor (Desc), DateCreated (Desc).

Ответ 1

Краткий ответ: Entity Framework 6 не позволяет использовать несколько индексов с разными видами.

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

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

Итак, давайте предположим, что мне нужно индексировать свойства следующим образом:

  • Тип (ASC)
  • DateFor (Desc)
  • DateCreated (Описание изделия)

Затем я назову свой индекс, за которым следует разделитель (:) и порядок сортировки. Это будет выглядеть так:

var indexName = "IX_Table:ASC,DESC,DESC";

Индекс с несколькими полями будет выглядеть так:

this.Property(t => t.Type)
    .HasColumnAnnotation(
        IndexAnnotation.AnnotationName,
        new IndexAnnotation(new[]
            {
                new IndexAttribute(indexName) { Order = 1 }
            }
        )
    );

this.Property(t => t.DateFor)
    .HasColumnAnnotation(
        IndexAnnotation.AnnotationName,
        new IndexAnnotation(new[]
            {
                new IndexAttribute(indexName) { Order = 2 }
            }
        )
    );

this.Property(t => t.DateCreated)
    .HasColumnAnnotation(
        IndexAnnotation.AnnotationName,
        new IndexAnnotation(new[]
            {
                new IndexAttribute(indexName) { Order = 3 }
            }
        )
    );

Теперь мы должны создать собственный класс генерации SQL, чтобы сгенерировать правильный код SQL для анализа нашего "подправленного" имени индекса:

public class CustomSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    protected override void Generate(CreateIndexOperation createIndexOperation)
    {
        using (var writer = Writer())
        {
            writer.Write("CREATE ");

            if (createIndexOperation.IsUnique)
            {
                writer.Write("UNIQUE ");
            }

            if (createIndexOperation.IsClustered)
            {
                writer.Write("CLUSTERED ");
            }
            else
            {
                writer.Write("NONCLUSTERED ");
            }

            string name = createIndexOperation.Name;
            string[] sorts = {};
            if (createIndexOperation.Name.Contains(":"))
            {
                var parts = createIndexOperation.Name.Split(':');

                if (parts.Length >= 1)
                {
                    name = parts[0];
                }
                if (parts.Length >= 2)
                {
                    sorts = parts[1].Split(',');
                }
            }

            writer.Write("INDEX ");
            writer.Write(Quote(name));
            writer.Write(" ON ");
            writer.Write(Name(createIndexOperation.Table));
            writer.Write("(");

            // Add the columns to the index with their respective sort order
            string fields = "";
            if (sorts.Length == 0 || sorts.Length == createIndexOperation.Columns.Count)
            {
                for (int i=0 ; i<createIndexOperation.Columns.Count ; i++)
                {
                    string sort = "ASC";
                    if (sorts.Length == 0)
                    {
                        // Do nothing
                    }
                    else if (sorts[i] != "ASC" && sorts[i] != "DESC")
                    {
                        throw new Exception(string.Format("Expected sort for {0} is 'ASC' or 'DESC. Received: {1}", name, sorts[i]));
                    }
                    else 
                    { 
                        sort = sorts[i];  
                    }

                    fields = fields + Quote(createIndexOperation.Columns[i]) + " " + sort + ",";
                }
                fields = fields.Substring(0, fields.Length - 1);
            }
            else
            {
                throw new Exception(string.Format("The sort (ASC/DEC) count is not equal to the number of fields in your Index ({0}).", name));
            }

            writer.Write(fields);

            writer.Write(")");
            Statement(writer);
        }
    }
}

Наконец, вы должны указать Entity Framework использовать новый метод, сгенерированный кодом, вместо метода по умолчанию, отредактировав файл Configuration.cs:

internal sealed class MyConfiguration : DbMigrationsConfiguration<MyContext>
{

    /// <summary>
    /// Constructor
    /// </summary>
    public MyConfiguration()
    {
        // Other stuff here...

        // Index/Unique custom generation (Ascending and Descending)
        SetSqlGenerator("System.Data.SqlClient", new CustomSqlServerMigrationSqlGenerator());
    }
}

Это. Возможно, это не самое чистое решение, но если вы генерируете свои сущности на лету (как я), вы сэкономите много времени и не забудете запустить свой сырой SQL.

Смотрите код здесь

Большое спасибо Роуэн Миллер и всем статьям в его блоге. Этот ответ был вдохновлен: Настройка провайдера первых миграций кода.

Ответ 2

Мне очень нравится @Maxime ответ, но он довольно сложный, но я попытаюсь изучить этот материал.

Мое решение немного проще, и оно просто работает, поэтому добавив его здесь, в случае, если это будет полезно для кого-то.

Мне удалось это сделать, вручную изменив Migrations и добавив правильный код. При проверке модели EF проверяет только, существует ли индекс, а не порядок.

    public override void Up()
    {
        DropIndex("dbo.MonitoringItemHistory", "IX_ItemDate");
        Sql("ALTER TABLE dbo.MonitoringItemHistory DROP CONSTRAINT [PK_dbo.MonitoringItemHistory]");
        CreateIndex("dbo.MonitoringItemHistory", new[] { "MonitoringItemId", "ChangeTime" }, clustered: true, name: "IX_ItemDate");
        Sql("ALTER TABLE dbo.MonitoringItemHistory ADD CONSTRAINT [PK_dbo.MonitoringItemHistory] PRIMARY KEY NONCLUSTERED (Id)");
    }

    public override void Down()
    {
        Sql("ALTER TABLE dbo.MonitoringItemHistory DROP CONSTRAINT [PK_dbo.MonitoringItemHistory]");
        DropIndex("dbo.MonitoringItemHistory", "IX_ItemDate");
        Sql("ALTER TABLE dbo.MonitoringItemHistory ADD CONSTRAINT [PK_dbo.MonitoringItemHistory] PRIMARY KEY CLUSTERED (Id)");
        CreateIndex("dbo.MonitoringItemHistory", new[] { "MonitoringItemId", "ChangeTime" }, name: "IX_ItemDate");
    }

и в моем коде Entity DB:

    [Index("IX_ItemDate", 1, IsClustered = true)]
    public int MonitoringItemId { get; set;}
    [Index("IX_ItemDate", 2, IsClustered = true)]
    public DateTimeOffset ChangeTime { get; set; }

Ответ 3

Вы можете сделать это вручную, редактируя Миграции следующим образом:

public override void Up()
{
    Sql("CREATE NONCLUSTERED INDEX [IX_Index_name] ON [dbo].[TableName] ([ColumnName1] Asc,[ColumnName2] Desc,[ColumnName3] Desc)");
}

public override void Down()
{
     Sql("DROP INDEX [dbo].[TableName].[IX_Index_name]");
}