Entity Framework Code First Fluent Api: добавление индексов в столбцы
Я запускаю EF 4.2 CF и хочу создавать индексы в определенных столбцах в моих объектах POCO.
В качестве примера можно сказать, что у нас есть этот класс сотрудников:
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime HireDate { get; set; }
}
Мы часто проводим поиск сотрудников по их EmployeeCode, и, поскольку есть много сотрудников, было бы неплохо иметь индексированные по причинам производительности.
Можем ли мы сделать это с беглым api каким-то образом? или, возможно, аннотации данных?
Я знаю, что можно выполнить sql-команды примерно так:
context.Database.ExecuteSqlCommand("CREATE INDEX IX_NAME ON ...");
Мне очень хотелось бы избежать использования raw SQL.
Я знаю, что этого не существует, но ищет что-то в этих строках:
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
internal EmployeeConfiguration()
{
this.HasIndex(e => e.EmployeeCode)
.HasIndex(e => e.FirstName)
.HasIndex(e => e.LastName);
}
}
или, возможно, используя System.ComponentModel.DataAnnotations
, POCO может выглядеть так (опять же я знаю, что этого не существует):
public class Employee
{
public int EmployeeID { get; set; }
[Indexed]
public string EmployeeCode { get; set; }
[Indexed]
public string FirstName { get; set; }
[Indexed]
public string LastName { get; set; }
public DateTime HireDate { get; set; }
}
У кого-нибудь есть идеи о том, как это сделать, или если есть какие-либо планы по реализации способа сделать это, первый код?
ОБНОВЛЕНИЕ: Как упоминалось в ответ Робба, эта функция реализована в версии EF 6.1
Ответы
Ответ 1
После внесения изменений в EF 4.3 теперь вы можете добавлять индексы при изменении или создании таблицы. Вот отрывок из EF 4.3 пошагового перехода на основе кода из блога команды ADO.NET
namespace MigrationsCodeDemo.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddPostClass : DbMigration
{
public override void Up()
{
CreateTable(
"Posts",
c => new
{
PostId = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 200),
Content = c.String(),
BlogId = c.Int(nullable: false),
})
.PrimaryKey(t => t.PostId)
.ForeignKey("Blogs", t => t.BlogId, cascadeDelete: true)
.Index(t => t.BlogId)
.Index(p => p.Title, unique: true);
AddColumn("Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3));
}
public override void Down()
{
DropIndex("Posts", new[] { "BlogId" });
DropForeignKey("Posts", "BlogId", "Blogs");
DropColumn("Blogs", "Rating");
DropTable("Posts");
}
}
}
Это хороший сильно типизированный способ добавления индексов, который был тем, что я искал, когда я впервые разместил вопрос.
Ответ 2
Вы можете создать атрибут с индексированием (как вы сказали), который затем выбирается в пользовательском инициализаторе.
Я создал следующий атрибут:
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class IndexAttribute : Attribute
{
public IndexAttribute(bool isUnique = false, bool isClustered = false, SortOrder sortOrder = SortOrder.Ascending)
{
IsUnique = isUnique;
IsClustered = isClustered;
SortOrder = sortOrder == SortOrder.Unspecified ? SortOrder.Ascending : sortOrder;
}
public bool IsUnique { get; private set; }
public bool IsClustered { get; private set; }
public SortOrder SortOrder { get; private set; }
//public string Where { get; private set; }
}
Затем я создал пользовательский инициализатор, который получил список имен таблиц, созданных для сущностей в моем контексте. У меня есть два базовых класса, которые наследуют все мои объекты, поэтому для получения имен таблиц я сделал следующее:
var baseEF = typeof (BaseEFEntity);
var baseLink = typeof (BaseLinkTable);
var types =
AppDomain.CurrentDomain.GetAssemblies().ToList().SelectMany(s => s.GetTypes()).Where(
baseEF.IsAssignableFrom).Union(AppDomain.CurrentDomain.GetAssemblies().ToList().SelectMany(
s => s.GetTypes()).Where(
baseLink.IsAssignableFrom));
var sqlScript = context.ObjectContext.CreateDatabaseScript();
foreach (var type in types)
{
var table = (TableAttribute) type.GetCustomAttributes(typeof (TableAttribute), true).FirstOrDefault();
var tableName = (table != null ? table.Name : null) ?? Pluralizer.Pluralize(type.Name);
Затем я нашел все свойства для каждого объекта, которые имеют этот атрибут, а затем выполнил команду SQL для создания индекса для каждого свойства. Классно!
//Check that a table exists
if (sqlScript.ToLower().Contains(string.Format(CREATETABLELOOKUP, tableName.ToLower())))
{
//indexes
var indexAttrib = typeof (IndexAttribute);
properties = type.GetProperties().Where(prop => Attribute.IsDefined(prop, indexAttrib));
foreach (var property in properties)
{
var attributes = property.GetCustomAttributes(indexAttrib, true).ToList();
foreach (IndexAttribute index in attributes)
{
var indexName = string.Format(INDEXNAMEFORMAT, tableName, property.Name,
attributes.Count > 1
? UNDERSCORE + (attributes.IndexOf(index) + 1)
: string.Empty);
try
{
context.ObjectContext.ExecuteStoreCommand(
string.Format(INDEX_STRING, indexName,
tableName,
property.Name,
index.IsUnique ? UNIQUE : string.Empty,
index.IsClustered ? CLUSTERED : NONCLUSTERED,
index.SortOrder == SortOrder.Ascending ? ASC : DESC));
}
catch (Exception)
{
}
}
}
Я даже включил индексы класса (которые могли иметь несколько столбцов), уникальные ограничения и ограничения по умолчанию все одинаково. Также приятно, что если вы поместите эти атрибуты в унаследованный класс, индекс или ограничение будут применены ко всем классам (таблицам), которые наследуют его.
Кстати, помощник плюрализатора содержит следующее:
public static class Pluralizer
{
private static object _pluralizer;
private static MethodInfo _pluralizationMethod;
public static string Pluralize(string word)
{
CreatePluralizer();
return (string) _pluralizationMethod.Invoke(_pluralizer, new object[] {word});
}
public static void CreatePluralizer()
{
if (_pluralizer == null)
{
var aseembly = typeof (DbContext).Assembly;
var type =
aseembly.GetType(
"System.Data.Entity.ModelConfiguration.Design.PluralizationServices.EnglishPluralizationService");
_pluralizer = Activator.CreateInstance(type, true);
_pluralizationMethod = _pluralizer.GetType().GetMethod("Pluralize");
}
}
}
Ответ 3
Чтобы использовать замороженный ответ, вы можете вручную передать его в перенос.
Сначала перейдите в консоль диспетчера пакетов и создайте новую миграцию с помощью add-migration
, а затем дайте ей имя. Появится пустая миграция. Вставьте это в:
public override void Up()
{
CreateIndex("TableName", "ColumnName");
}
public override void Down()
{
DropIndex("TableName",new[] {"ColumnName"});
}
Обратите внимание: если вы используете поле строки, оно должно быть ограничено длиной 450 символов.
Ответ 4
Я также изучил это недавно и не нашел другого способа, поэтому я решил создать индексы при посеве базы данных:
public class MyDBInitializer : DropCreateDatabaseIfModelChanges<MyContext>
{
private MyContext _Context;
protected override void Seed(MyContext context)
{
base.Seed(context);
_Context = context;
// We create database indexes
CreateIndex("FieldName", typeof(ClassName));
context.SaveChanges();
}
private void CreateIndex(string field, Type table)
{
_Context.Database.ExecuteSqlCommand(String.Format("CREATE INDEX IX_{0} ON {1} ({0})", field, table.Name));
}
}
Ответ 5
Обратите внимание, что в Entity Framework 6.1 (в настоящее время в бета-версии) поддержка IndexAttribute будет аннотировать свойства индекса, которые автоматически приведут к (уникальному) индексу в ваших первых переходах кода.
Ответ 6
Для всех, кто использует Entity Framework 6.1+, вы можете сделать следующее с быстрым api:
modelBuilder
.Entity<Department>()
.Property(t => t.Name)
.HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));
Подробнее в документации.
Ответ 7
Ну, я нашел решение онлайн и адаптировал его, чтобы он соответствовал моим потребностям:
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class IndexAttribute : Attribute
{
public IndexAttribute(string name, bool unique = false)
{
this.Name = name;
this.IsUnique = unique;
}
public string Name { get; private set; }
public bool IsUnique { get; private set; }
}
public class IndexInitializer<T> : IDatabaseInitializer<T> where T : DbContext
{
private const string CreateIndexQueryTemplate = "CREATE {unique} INDEX {indexName} ON {tableName} ({columnName});";
public void InitializeDatabase(T context)
{
const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance;
Dictionary<IndexAttribute, List<string>> indexes = new Dictionary<IndexAttribute, List<string>>();
string query = string.Empty;
foreach (var dataSetProperty in typeof(T).GetProperties(PublicInstance).Where(p => p.PropertyType.Name == typeof(DbSet<>).Name))
{
var entityType = dataSetProperty.PropertyType.GetGenericArguments().Single();
TableAttribute[] tableAttributes = (TableAttribute[])entityType.GetCustomAttributes(typeof(TableAttribute), false);
indexes.Clear();
string tableName = tableAttributes.Length != 0 ? tableAttributes[0].Name : dataSetProperty.Name;
foreach (PropertyInfo property in entityType.GetProperties(PublicInstance))
{
IndexAttribute[] indexAttributes = (IndexAttribute[])property.GetCustomAttributes(typeof(IndexAttribute), false);
NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])property.GetCustomAttributes(typeof(NotMappedAttribute), false);
if (indexAttributes.Length > 0 && notMappedAttributes.Length == 0)
{
ColumnAttribute[] columnAttributes = (ColumnAttribute[])property.GetCustomAttributes(typeof(ColumnAttribute), false);
foreach (IndexAttribute indexAttribute in indexAttributes)
{
if (!indexes.ContainsKey(indexAttribute))
{
indexes.Add(indexAttribute, new List<string>());
}
if (property.PropertyType.IsValueType || property.PropertyType == typeof(string))
{
string columnName = columnAttributes.Length != 0 ? columnAttributes[0].Name : property.Name;
indexes[indexAttribute].Add(columnName);
}
else
{
indexes[indexAttribute].Add(property.PropertyType.Name + "_" + GetKeyName(property.PropertyType));
}
}
}
}
foreach (IndexAttribute indexAttribute in indexes.Keys)
{
query += CreateIndexQueryTemplate.Replace("{indexName}", indexAttribute.Name)
.Replace("{tableName}", tableName)
.Replace("{columnName}", string.Join(", ", indexes[indexAttribute].ToArray()))
.Replace("{unique}", indexAttribute.IsUnique ? "UNIQUE" : string.Empty);
}
}
if (context.Database.CreateIfNotExists())
{
context.Database.ExecuteSqlCommand(query);
}
}
private string GetKeyName(Type type)
{
PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo propertyInfo in propertyInfos)
{
if (propertyInfo.GetCustomAttribute(typeof(KeyAttribute), true) != null)
return propertyInfo.Name;
}
throw new Exception("No property was found with the attribute Key");
}
}
Затем перегрузите OnModelCreating в свой dbcontext
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer(new IndexInitializer<MyContext>());
base.OnModelCreating(modelBuilder);
}
Применить атрибут индекса к типу Entity, с помощью этого решения вы можете иметь несколько полей в одном и том же индексе, просто используя одно и то же имя и уникальный.
Ответ 8
Расширение ответа Tsuushin выше для поддержки нескольких столбцов и уникальных ограничений:
private void CreateIndex(RBPContext context, string field, string table, bool unique = false)
{
context.Database.ExecuteSqlCommand(String.Format("CREATE {0}NONCLUSTERED INDEX IX_{1}_{2} ON {1} ({3})",
unique ? "UNIQUE " : "",
table,
field.Replace(",","_"),
field));
}
Ответ 9
расширяющееся на Petoj
i изменил параметр CreateIndexQueryTemplate на
private const string CreateIndexQueryTemplate = "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '{indexName}') CREATE {unique} INDEX {indexName} ON {tableName} ({columnName});";
и удалил из OnModelCreating
следующее:
Database.SetInitializer(new IndexInitializer<MyContext>());
и добавил следующее в метод настройки посева
new IndexInitializer<MyContext>().InitializeDatabase(context);
таким образом атрибуты индекса запускаются каждый раз, когда вы делаете базу данных обновлений.
Ответ 10
Если вы хотите, чтобы эта функция была добавлена в EF, вы можете проголосовать за нее здесь http://entityframework.codeplex.com/workitem/57
Ответ 11
Расширение jwsadler аннотаций данных было хорошо для нас. Мы используем аннотации для влияния на обработку класса или свойства и Fluent API для глобальных изменений.
Наши аннотации охватывают индексы (уникальные, а не уникальные) плюс значения по умолчанию для getdate() и (1). Образец кода показывает, как мы применили его к нашей ситуации. Все наши классы наследуются от одного базового класса. Эта реализация делает много предположений, потому что у нас довольно простая модель. Мы используем Entity Framework 6.0.1. Было добавлено много комментариев.
using System;
using System.Linq;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
namespace YourNameSpace
{
public enum SqlOption
{
Active = 1,
GetDate = 2,
Index = 3,
Unique = 4,
}
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class SqlAttribute : Attribute
{
public SqlAttribute(SqlOption selectedOption = SqlOption.Index)
{
this.Option = selectedOption;
}
public SqlOption Option {get; set;}
}
// See enum above, usage examples: [Sql(SqlOption.Unique)] [Sql(SqlOption.Index)] [Sql(SqlOption.GetDate)]
public class SqlInitializer<T> : IDatabaseInitializer<T> where T : DbContext
{
// Create templates for the DDL we want generate
const string INDEX_TEMPLATE = "CREATE NONCLUSTERED INDEX IX_{columnName} ON [dbo].[{tableName}] ([{columnName}]);";
const string UNIQUE_TEMPLATE = "CREATE UNIQUE NONCLUSTERED INDEX UQ_{columnName} ON [dbo].[{tableName}] ([{columnName}]);";
const string GETDATE_TEMPLATE = "ALTER TABLE [dbo].[{tableName}] ADD DEFAULT (getdate()) FOR [{columnName}];";
const string ACTIVE_TEMPLATE = "ALTER TABLE [dbo].[{tableName}] ADD DEFAULT (1) FOR [{columnName}];";
// Called by Database.SetInitializer(new IndexInitializer< MyDBContext>()); in MyDBContext.cs
public void InitializeDatabase(T context)
{
// To be used for the SQL DDL that I generate
string sql = string.Empty;
// All of my classes are derived from my base class, Entity
var baseClass = typeof(Entity);
// Get a list of classes in my model derived from my base class
var modelClasses = AppDomain.CurrentDomain.GetAssemblies().ToList().
SelectMany(s => s.GetTypes()).Where(baseClass.IsAssignableFrom);
// For debugging only - examine the SQL DDL that Entity Framework is generating
// Manipulating this is discouraged.
var generatedDDSQL = ((IObjectContextAdapter)context).ObjectContext.CreateDatabaseScript();
// Define which Annotation Attribute we care about (this class!)
var annotationAttribute = typeof(SqlAttribute);
// Generate a list of concrete classes in my model derived from
// Entity class since we follow Table Per Concrete Class (TPC).
var concreteClasses = from modelClass in modelClasses
where !modelClass.IsAbstract
select modelClass;
// Iterate through my model concrete classes (will be mapped to tables)
foreach (var concreteClass in concreteClasses)
{
// Calculate the table name - could get the table name from list of DbContext properties
// to be more correct (but this is sufficient in my case)
var tableName = concreteClass.Name + "s";
// Get concrete class properties that have this annotation
var propertiesWithAnnotations = concreteClass.GetProperties().Where(prop => Attribute.IsDefined(prop, annotationAttribute));
foreach (var annotatedProperty in propertiesWithAnnotations)
{
var columnName = annotatedProperty.Name;
var annotationProperties = annotatedProperty.GetCustomAttributes(annotationAttribute, true).ToList();
foreach (SqlAttribute annotationProperty in annotationProperties)
{
// Generate the appropriate SQL DLL based on the attribute selected
switch (annotationProperty.Option)
{
case SqlOption.Active: // Default value of true plus an index (for my case)
sql += ACTIVE_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
sql += INDEX_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
case SqlOption.GetDate: // GetDate plus an index (for my case)
sql += GETDATE_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
sql += INDEX_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
case SqlOption.Index: // Default for empty annotations for example [Sql()]
sql += INDEX_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
case SqlOption.Unique:
sql += UNIQUE_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
} // switch
} // foreach annotationProperty
} // foreach annotatedProperty
} // foreach concreteClass
// Would have been better not to go through all the work of generating the SQL
// if we weren't going to use it, but putting it here makes it easier to follow.
if (context.Database.CreateIfNotExists())
context.Database.ExecuteSqlCommand(sql);
} // InitializeDatabase
} // SqlInitializer
} // Namespace
Здесь наш контекст:
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace YourNameSpace
{
public class MyDBContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Only including my concrete classes here as we're following Table Per Concrete Class (TPC)
public virtual DbSet<Attendance> Attendances { get; set; }
public virtual DbSet<Course> Courses { get; set; }
public virtual DbSet<Location> Locations { get; set; }
public virtual DbSet<PaymentMethod> PaymentMethods { get; set; }
public virtual DbSet<Purchase> Purchases { get; set; }
public virtual DbSet<Student> Students { get; set; }
public virtual DbSet<Teacher> Teachers { get; set; }
// Process the SQL Annotations
Database.SetInitializer(new SqlInitializer<MyDBContext>());
base.OnModelCreating(modelBuilder);
// Change all datetime columns to datetime2
modelBuilder.Properties<DateTime>().Configure(c => c.HasColumnType("datetime2"));
// Turn off cascading deletes
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}
}
}
Ответ 12
Чтобы продолжить работу над этими замечательными ответами, мы добавили следующий код, чтобы активировать атрибут Index из связанного с ним типа метаданных. Для полной информации, пожалуйста, см. Мой пост в блоге, но вкратце вот подробности.
Типы метаданных используются следующим образом:
[MetadataType(typeof(UserAccountAnnotations))]
public partial class UserAccount : IDomainEntity
{
[Key]
public int Id { get; set; } // Unique ID
sealed class UserAccountAnnotations
{
[Index("IX_UserName", unique: true)]
public string UserName { get; set; }
}
}
В этом примере тип метаданных является вложенным классом, но он не обязательно должен быть, он может быть любым типом. Сопоставление свойств выполняется только по имени, поэтому тип метаданных должен иметь свойство с тем же именем, и любые аннотации данных, применяемые к этому, должны затем применяться к связанному классу сущностей. Это не работает в исходном решении, потому что оно не проверяет соответствующий тип метаданных. Мы применили следующий вспомогательный метод:
/// <summary>
/// Gets the index attributes on the specified property and the same property on any associated metadata type.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>IEnumerable{IndexAttribute}.</returns>
IEnumerable<IndexAttribute> GetIndexAttributes(PropertyInfo property)
{
Type entityType = property.DeclaringType;
var indexAttributes = (IndexAttribute[])property.GetCustomAttributes(typeof(IndexAttribute), false);
var metadataAttribute =
entityType.GetCustomAttribute(typeof(MetadataTypeAttribute)) as MetadataTypeAttribute;
if (metadataAttribute == null)
return indexAttributes; // No metadata type
Type associatedMetadataType = metadataAttribute.MetadataClassType;
PropertyInfo associatedProperty = associatedMetadataType.GetProperty(property.Name);
if (associatedProperty == null)
return indexAttributes; // No metadata on the property
var associatedIndexAttributes =
(IndexAttribute[])associatedProperty.GetCustomAttributes(typeof(IndexAttribute), false);
return indexAttributes.Union(associatedIndexAttributes);
}
Ответ 13
Я обнаружил проблему с ответом @highace - миграция вниз использует неправильное переопределение для DropIndex. Вот что я сделал:
- Чтобы соответствовать ограничениям Sql Server на столбцах индекса (900 байт), я уменьшил размер пары полей в моей модели.
- Я добавил перенос с помощью Add-Migration "Добавить уникальные индексы"
- Я вручную добавил методы CreateIndex и DropIndex для переноса. Я использовал переопределение, которое принимает имя индекса для индекса с одним столбцом. Я использовал переопределение, которое принимает массив имен столбцов, где индекс охватывает более одного столбца
И вот код с примерами обоих переопределений каждого метода:
public partial class AddUniqueIndexes : DbMigration
{
public override void Up()
{
//Sql Server limits indexes to 900 bytes,
//so we need to ensure cumulative field sizes do not exceed this
//otherwise inserts and updates could be prevented
//http://www.sqlteam.com/article/included-columns-sql-server-2005
AlterColumn("dbo.Answers",
"Text",
c => c.String(nullable: false, maxLength: 400));
AlterColumn("dbo.ConstructionTypes",
"Name",
c => c.String(nullable: false, maxLength: 300));
//[IX_Text] is the name that Entity Framework would use by default
// even if it wasn't specified here
CreateIndex("dbo.Answers",
"Text",
unique: true,
name: "IX_Text");
//Default name is [IX_Name_OrganisationID]
CreateIndex("dbo.ConstructionTypes",
new string[] { "Name", "OrganisationID" },
unique: true);
}
public override void Down()
{
//Drop Indexes before altering fields
//(otherwise it will fail because of dependencies)
//Example of dropping an index based on its name
DropIndex("dbo.Answers", "IX_Text");
//Example of dropping an index based on the columns it targets
DropIndex("dbo.ConstructionTypes",
new string[] { "Name", "OrganisationID" });
AlterColumn("dbo.ConstructionTypes",
"Name",
c => c.String(nullable: false));
AlterColumn("dbo.Answers",
"Text",
c => c.String(nullable: false, maxLength: 500));
}
Ответ 14
Для EF7 вы можете использовать метод hasIndex()
.
Мы можем также установить кластерный и некластеризованный индекс.
По умолчанию первичный ключ будет кластеризован. Мы также можем изменить это поведение.
supplierItemEntity.HasKey(supplierItem => supplierItem.SupplierItemId).ForSqlServerIsClustered(false);
supplierItemEntity.HasIndex(s => new { s.ItemId }).ForSqlServerIsClustered(true);
![введите описание изображения здесь]()