实体框架Code First Fluent Api:向列添加索引

66

我正在运行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 ...");

我非常想避免像那样使用原始的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; }
}

有没有人对如何做到这一点有什么想法,或者是否有计划实现这种方法?

更新:正如Robba的回答中提到的那样,这个功能在EF版本6.1中实现了。


请查看此处的解决方案:https://dev59.com/cWkv5IYBdhLWcg3waAJT#23055838 - juFo
15个回答

47
自从EF 4.3引入了迁移后,您现在可以在修改或创建表时添加索引。以下是ADO.NET团队博客中EF 4.3基于代码的迁移演示的摘录。请注意保留HTML标签。
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");
        }
    }
}

这是一种很好的强类型方式来添加索引,这正是我在第一次发布问题时在寻找的。


1
我想表达的意思是原问题提到了4.2 :) - jwsadler
7
这个解决方案并不是非常好。如果从头开始创建数据库,那么在迁移中添加的索引将会丢失。你真正想要的是为索引添加数据注释。 - Jez
@Jez 我完全同意,要么这样,要么通过EntityTypeConfigurations进行设置的方法。 - Jim Wolff
只有在重新生成迁移时才会出现问题,因为迁移工具不知道新增的索引。如果在生成的迁移中有索引,则从头开始重新创建数据库实际上是可以的。 - Jim Wolff
1
根据我的经验,这种情况不会发生。它不会运行任何迁移;它只是创建与数据模型的最新版本相对应的数据库,并在迁移历史记录表中插入一个初始创建的条目。 - Jez
显示剩余2条评论

31
你可以创建一个名为indexed的属性(如你所建议的),然后在自定义初始化方法中使用它。我创建了以下属性:
[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");
        }
    }
}

非常有趣,这正是我所思考的内容,使用数据注释或流畅的 API 来定义模型中的索引。我很快就要尝试为这种方法创建一些测试,做得好。 - Jim Wolff

20

在frozen的回答基础上,您可以自己编写代码将其放入迁移中。

首先,进入包管理器控制台,使用 add-migration 创建一个新的迁移,然后为其命名。一个空白的迁移将出现。将以下内容添加到其中:

    public override void Up()
    {
        CreateIndex("TableName", "ColumnName");
    }

    public override void Down()
    {
        DropIndex("TableName",new[] {"ColumnName"});
    }

需要注意的是,如果您正在使用字符串字段,则它的长度也需要限制在450个字符以内。


这正是我想要的,因为我需要在迁移中创建一个索引。谢谢! - kamranicus
3
这里使用的DropIndex方法是不正确的。第二个参数应该是“string [] columns”或“string name”。Down方法将尝试删除名为“ColumnName”的索引。您需要在Up中指定索引名称,或在Down中传入列名的数组。请参见http://msdn.microsoft.com/en-us/library/hh829733(v=vs.103).aspx。 - Colin
Sql Server 强制实施 900 字节的最大键大小。 https://msdn.microsoft.com/zh-cn/library/ms191241(v=sql.105).aspx。根据编码方式,您的 450 '字符' 可能会有所不同。 - JJS

17

我最近也研究过这个问题,并没有找到其他方法,所以在填充数据库时我选择了创建索引:

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));
    }    
}   

我正在将一个类型传递给CreateIndex()函数,以便在表名上实现编译时安全性。不幸的是,对于字段名来说这并不容易实现,因此我还没有实现它。 - CB-Dan
谢谢 - 这对我很有帮助。稍微扩展了CreateIndex方法以支持多列和唯一索引 - 请参见下面的答案: - Andy Butland

14
请注意,在Entity Framework 6.1(目前处于测试版)中,将支持使用IndexAttribute注释索引属性,这将自动在您的Code First迁移中生成(唯一)索引。

2
现在它可以了:EF 6.1:使用IndexAttribute创建索引 - Mrchief

11

对于使用 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");
    }
}

然后在你的dbcontext中重载OnModelCreating。
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        Database.SetInitializer(new IndexInitializer<MyContext>());
        base.OnModelCreating(modelBuilder);
    }

将索引属性应用于您的实体类型,使用此解决方案,您可以在同一索引中拥有多个字段,只需使用相同的名称和唯一标识即可。


非常有趣的解决方案,是否有任何方法可以扩展add-migrations功能和TypeConfigurations的流畅API,以便在使用迁移时创建索引? - Jim Wolff
1
@RudolfDvoracek,请将以下行更改为“const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;”,这样应该可以循环基类中的属性。 - Peter
@Petoj。我有一个名为DeviceContact的基类,其中包含一个名为Id的属性,它是主键。派生类SMTPMail有一个额外的属性SMTPAddress。尽管我更改了PublicInstance常量的值,但迭代属性仅通过ID属性而不是已经装饰的SMTPAddress。 - Rudolf Dvoracek
有趣的技巧,但它无法获取在MetaDataType上定义的属性。原文章在哪里? - Tim Long
注意!!!这种技术是可行的,但它会阻止您的数据库执行自动更新(如果您更改了模式)。解决方案是从您的Seed方法显式调用IndexInitializer.InitializeDatabase,并更改InitializeDatabase中的最后一个if语句,以便它始终运行(因为它只会从Seed中调用)。 - OSH
显示剩余5条评论

3

3

关于Petoj的进一步阐述

我修改了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);

这样做可以让索引属性在每次执行update-database时都运行。

为什么不改成删除并重新创建,这样您就可以更改包含的字段,并在每次更新时进行更新。 - Peter

3
jwsadler的数据注释扩展对我们非常适用。我们使用注释来影响类或属性的处理,使用流畅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's 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's properties
                // to be more correct (but this is sufficient in my case)
                var tableName = concreteClass.Name + "s";

                // Get concrete class's 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>();
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接