实体框架中多个列的唯一键约束


243

我正在使用Entity Framework 5.0 Code First;

public class Entity
 {
   [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public string EntityId { get; set;}
   public int FirstColumn  { get; set;}
   public int SecondColumn  { get; set;}
 }

我要让之间的组合FirstColumnSecondColumn独特。

例:

Id  FirstColumn  SecondColumn 
1       1              1       = OK
2       2              1       = OK
3       3              3       = OK
5       3              1       = THIS OK 
4       3              3       = GRRRRR! HERE ERROR

反正有这样做吗?

Answers:


369

使用Entity Framework 6.1,您现在可以执行以下操作:

[Index("IX_FirstAndSecond", 1, IsUnique = true)]
public int FirstColumn { get; set; }

[Index("IX_FirstAndSecond", 2, IsUnique = true)]
public int SecondColumn { get; set; }

属性中的第二个参数是您可以在索引中指定列顺序的位置。
详细信息:MSDN


12
这对于数据批注是正确的:),如果您想要使用流畅的API的答案,请参见stackoverflow.com/a/25779348/2362036下的
Niaher

8
但是我需要它为外键工作!你能帮助我吗?
feedc0de

2
@ 0xFEEDC0DE在下面看到我的答案,该地址涉及索引中外键的使用。
Kryptos

1
您可以在linq到sql中发布如何使用此索引吗?
Bluebaron

4
@JJS-我可以在其中一个属性是外键的情况下正常工作..碰巧您的键是varchar还是nvarchar?有对这些都可以被用作唯一的密钥长度..限制stackoverflow.com/questions/2863993/...
戴夫劳伦斯

129

我找到了解决问题的三种方法。

EntityFramework Core中的唯一索引:

第一种方法:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Entity>()
   .HasIndex(p => new {p.FirstColumn , p.SecondColumn}).IsUnique();
}

通过备用键使用EF Core创建唯一约束的第二种方法

例子

一栏:

modelBuilder.Entity<Blog>().HasAlternateKey(c => c.SecondColumn).HasName("IX_SingeColumn");

多列:

modelBuilder.Entity<Entity>().HasAlternateKey(c => new [] {c.FirstColumn, c.SecondColumn}).HasName("IX_MultipleColumns");

EF 6以下:


第一种方法:

dbContext.Database.ExecuteSqlCommand(string.Format(
                        @"CREATE UNIQUE INDEX LX_{0} ON {0} ({1})", 
                                 "Entitys", "FirstColumn, SecondColumn"));

这种方法非常快速且实用,但是主要问题是实体框架对这些更改一无所知!


第二种方法:
我在这篇文章中找到了它,但是我自己没有尝试过。

CreateIndex("Entitys", new string[2] { "FirstColumn", "SecondColumn" },
              true, "IX_Entitys");

这种方法的问题如下:它需要DbMigration,所以如果没有它怎么办?


第三种方法:
我认为这是最好的方法,但是需要一些时间来完成。我将向您展示其背后的想法:在此链接http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a中, 您可以找到用于唯一键数据注释的代码:

[UniqueKey] // Unique Key 
public int FirstColumn  { get; set;}
[UniqueKey] // Unique Key 
public int SecondColumn  { get; set;}

// The problem hier
1, 1  = OK 
1 ,2  = NO OK 1 IS UNIQUE

这种方法的问题;如何合并它们?我有一个扩展这种Microsoft实现的想法,例如:

[UniqueKey, 1] // Unique Key 
public int FirstColumn  { get; set;}
[UniqueKey ,1] // Unique Key 
public int SecondColumn  { get; set;}

稍后,如Microsoft示例中所述,在IDatabaseInitializer中,您可以根据给定的整数组合键。但是,必须注意一件事:如果unique属性的类型为string,则必须设置MaxLength。


1
(y)我觉得这个答案更好。另一件事,第三种方法不一定是最好的。(实际上,我喜欢第一个。)我个人更喜欢不将任何EF工件转移到我的实体类中。
Najeeb

1
可能的话,第二种方法应该是:CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2})?(请参阅 BOL
AK

2
愚蠢的问题:为什么要用“ IX_”开头所有名字?
Bastien Vandamme

1
@BastienVandamme这是一个很好的问题。EF的自动生成索引以IX_开头。默认情况下,这似乎是EF索引中的约定,索引名称将为IX_ {属性名称}。
巴萨姆·阿鲁吉利

1
是的,应该如此。感谢您的Fluent API实现。严重缺乏相关文档
JSON

75

如果您使用的是Code-First,则可以实现自定义扩展HasUniqueIndexAnnotation

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Infrastructure.Annotations;
using System.Data.Entity.ModelConfiguration.Configuration;

internal static class TypeConfigurationExtensions
{
    public static PrimitivePropertyConfiguration HasUniqueIndexAnnotation(
        this PrimitivePropertyConfiguration property, 
        string indexName,
        int columnOrder)
    {
        var indexAttribute = new IndexAttribute(indexName, columnOrder) { IsUnique = true };
        var indexAnnotation = new IndexAnnotation(indexAttribute);

        return property.HasColumnAnnotation(IndexAnnotation.AnnotationName, indexAnnotation);
    }
}

然后像这样使用它:

this.Property(t => t.Email)
    .HasColumnName("Email")
    .HasMaxLength(250)
    .IsRequired()
    .HasUniqueIndexAnnotation("UQ_User_EmailPerApplication", 0);

this.Property(t => t.ApplicationId)
    .HasColumnName("ApplicationId")
    .HasUniqueIndexAnnotation("UQ_User_EmailPerApplication", 1);

这将导致此迁移:

public override void Up()
{
    CreateIndex("dbo.User", new[] { "Email", "ApplicationId" }, unique: true, name: "UQ_User_EmailPerApplication");
}

public override void Down()
{
    DropIndex("dbo.User", "UQ_User_EmailPerApplication");
}

最终以以下形式出现在数据库中:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)

3
但这不是索引约束!
罗曼·波克罗夫斯基

3
在您的第二个代码块(this.Property(t => t.Email))中,包含类的是什么?(即:什么是this
JoeBrockhaus

2
虚拟机 EntityTypeConfiguration<T>
JoeBrockhaus

5
@RomanPokrovskij:唯一索引和唯一约束之间的差异似乎与如何在SQL Server中维护其记录有关。有关详细信息,请参见technet.microsoft.com/zh-cn/library/aa224827%28v=sql.80%29.aspx
Mass Dot Net

1
@niaher我感谢您很好的扩展方法
Mohsen Afshin

18

您需要定义一个组合键。

带有数据注释的看起来像这样:

public class Entity
 {
   public string EntityId { get; set;}
   [Key]
   [Column(Order=0)]
   public int FirstColumn  { get; set;}
   [Key]
   [Column(Order=1)]
   public int SecondColumn  { get; set;}
 }

您还可以在通过指定以下内容覆盖OnModelCreating时使用modelBuilder进行此操作:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });

2
但是,不是键,我只是希望它们作为唯一键,键应该是ID吗?我已经更新了申请表,感谢您的帮助!
巴萨姆·阿鲁吉利

16

niaher的回答指出,在编写本文时,要使用fluent API,您需要自定义扩展名可能是正确的。您现在可以(EF core 2.1)如下使用fluent API:

modelBuilder.Entity<ClassName>()
            .HasIndex(a => new { a.Column1, a.Column2}).IsUnique();

9

完成@chuck答案以使用带有外键的复合索引

您需要定义一个将保留外键值的属性。然后,您可以在索引定义中使用此属性。

例如,我们有一个有员工的公司,只有我们对任何员工的(姓名,公司)都有唯一的约束:

class Company
{
    public Guid Id { get; set; }
}

class Employee
{
    public Guid Id { get; set; }
    [Required]
    public String Name { get; set; }
    public Company Company  { get; set; }
    [Required]
    public Guid CompanyId { get; set; }
}

现在是Employee类的映射:

class EmployeeMap : EntityTypeConfiguration<Employee>
{
    public EmployeeMap ()
    {
        ToTable("Employee");

        Property(p => p.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

        Property(p => p.Name)
            .HasUniqueIndexAnnotation("UK_Employee_Name_Company", 0);
        Property(p => p.CompanyId )
            .HasUniqueIndexAnnotation("UK_Employee_Name_Company", 1);
        HasRequired(p => p.Company)
            .WithMany()
            .HasForeignKey(p => p.CompanyId)
            .WillCascadeOnDelete(false);
    }
}

请注意,我还将@niaher扩展用于唯一索引注释。


1
在此示例中,您同时拥有Company和CompanyId。这意味着呼叫者可以更改一个,但不能更改另一个,并且可以拥有一个带有错误数据的实体。
LosManos

1
@LosManos您在说哪个来电者?该类表示数据库中的数据。通过查询更改值将确保一致性。根据客户端应用程序的功能,您可能需要实施检查,但这不是OP的范围。
Kryptos

4

在@chuck接受的答案中,有一条评论说它不适用于FK。

它对我有用,EF6 .Net4.7.2的情况

public class OnCallDay
{
     public int Id { get; set; }
    //[Key]
    [Index("IX_OnCallDateEmployee", 1, IsUnique = true)]
    public DateTime Date { get; set; }
    [ForeignKey("Employee")]
    [Index("IX_OnCallDateEmployee", 2, IsUnique = true)]
    public string EmployeeId { get; set; }
    public virtual ApplicationUser Employee{ get; set; }
}

很久。可以说它已经工作很长时间了!感谢您的更新,请在@chuck答案中添加评论。我认为Chuck早已不使用SO。
巴萨姆·阿鲁吉利

此处的EmployeeID属性是否需要一个属性来限制其长度以便对其建立索引?否则用VARCHAR(MAX)创建,它不能有索引?将[StringLength(255)]属性添加到EmployeeID
Lord Darth Vader

EmployeeID是一个GUID。许多教程建议将GUID映射为字符串而不是guid,但我不知道为什么
dalios

3

我假设您一直想EntityId成为主键,所以用复合键代替它不是一个选择(如果仅仅是因为复合键的使用要复杂得多,并且因为拥有同样有意义的主键也不太明智在业务逻辑中)。

最少要做的是在数据库的两个字段上创建唯一键,并在保存更改时专门检查唯一键冲突异常。

另外,您可以(应该)在保存更改之前检查唯一值。最好的方法是通过Any()查询,因为它可以最大程度地减少传输的数据量:

if (context.Entities.Any(e => e.FirstColumn == value1 
                           && e.SecondColumn == value2))
{
    // deal with duplicate values here.
}

注意仅凭此检查是远远不够的。在检查和实际提交之间总是存在一些延迟,因此您将始终需要唯一的约束+异常处理。


3
感谢@GertArnold给出的答案,但我不想在业务层上验证其唯一性,这是数据库工作,应在数据库中完成!
巴萨姆·阿鲁吉利

2
确定,然后坚持唯一索引。但是无论如何,您都必须处理业务层中的索引冲突。
Gert Arnold 2013年

1
从外部接收到此类异常时,我将捕获并报告错误并中断进程或关闭应用程序。
巴萨姆·阿鲁吉利

3
是的,...我需要回应吗?记住,我对您的应用程序一无所知,我无法告诉您处理这些异常的最佳方法是什么,只能告诉您必须处理它们。
Gert Arnold

2
警惕EF的数据库唯一约束。如果执行此操作,然后结束更新,该更新会将作为唯一键一部分的列之一的值进行翻转,则除非添加整个事务层,否则实体frameowkr将在保存时失败。例如:Page对象具有Elements的子级集合。每个元素都有SortOrder。您希望PageID和SortOrder的组合是唯一的。在前端,用户触发器元素的排序顺序为1和2。Entity Framework将无法通过一次尝试更新排序顺序的保存b / c失败。
EGP 2014年

1

最近使用“查克”推荐的方法添加了一个具有两列唯一性的复合键,谢谢@查克。只有这种方法对我来说看起来更干净:

public int groupId {get; set;}

[Index("IX_ClientGrouping", 1, IsUnique = true)]
public int ClientId { get; set; }

[Index("IX_ClientGrouping", 2, IsUnique = true)]
public int GroupName { get; set; }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.