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

我使用实体框架5.0代码第一;

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

有办法吗?

232390 次浏览

您需要定义一个复合键。

使用数据注释,它看起来像这样:

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

你也可以在覆盖onmodelcreation时通过指定modelBuilder来做到这一点:

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

我假设你总是希望EntityId是主键,所以用复合键代替它是不可取的(如果只是因为复合键与而且一起工作要复杂得多,因为主键在业务逻辑中也有意义是不明智的)。

您至少应该在数据库中的两个字段上创建一个唯一键,并在保存更改时专门检查是否存在违反唯一键的异常。

此外,您可以(应该)在保存更改之前检查惟一值。最好的方法是使用Any()查询,因为它可以最小化传输的数据量:

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

请注意,单凭这张支票是远远不够的。在检查和实际提交之间总是有一些延迟,所以你总是需要唯一的约束+异常处理。

我找到了三种方法来解决这个问题。

EntityFramework核心中唯一的索引:

第一种方法:

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

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

例子

一列:

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及以下:


第一种方法: < br >

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

这种方法非常快速和有用,但主要的问题是实体框架不知道任何关于这些变化!


< p > 第二种方法: < br >

.我在这篇文章中找到了它,但我没有自己尝试
CreateIndex("Entitys", new string[2] { "FirstColumn", "SecondColumn" },
true, "IX_Entitys");

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


< p > 第三种方法: < br > 我认为这是最好的一个,但它需要一些时间来做。我将向你们展示它背后的思想: 在这个链接中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

这种方法的问题;我如何将它们结合起来? 我有一个想法来扩展这个微软实现,例如:

[UniqueKey, 1] // Unique Key
public int FirstColumn  { get; set;}
[UniqueKey ,1] // Unique Key
public int SecondColumn  { get; set;}
稍后在微软示例中描述的IDatabaseInitializer中,您可以根据给定的整数组合键。 有一件事必须注意:如果唯一属性的类型是字符串,那么你必须设置MaxLength.

使用实体框架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

如果你使用代码优先,你可以实现一个自定义扩展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
)

完成使用综合指数外键的@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扩展名。

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

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

最近添加了一个组合键的唯一性2列使用'chuck'推荐的方法,谢谢@chuck。只有这条路在我看来比较干净:

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

在@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; }
}

对于那些在寻找2021年解决方案的人来说,公认答案的工作版本现在应该是这样的:

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

因此,注释应该存在于模型上,而不是单独的列上。还要注意nameof()语法。

这个答案来自于官方文档:https://learn.microsoft.com/en-us/ef/core/modeling/indexes?tabs=data-annotations

我想补充我的答案,因为提供的解决方案对我没有帮助。在我的例子中,其中一列是外键引用。

旧的模型:

public class Matrix
{
public int ID { get; set; }


public MachineData MachineData { get; set; }


public MachineVariant MachineVariant { get; set; }
}

注意,MachineVariant是一个enum, MachineData是一个引用。

尝试使用@Bassam Alugili提供的解决方案:

modelBuilder.Entity<Matrix>()
.HasIndex(sm => new { sm.MachineData, sm.DoughVariant }).IsUnique(true);

没有工作。所以我为machineData外键添加了一个ID列,如下所示:

public class Matrix
{
public int ID { get; set; }


public MachineData MachineData { get; set; }


[ForeignKey("MachineData")]
public int MachineDataID { get; set; }


public MachineVariant MachineVariant { get; set; }
}

并将模型构建器代码更改为:

modelBuilder.Entity<Matrix>()
.HasIndex(sm => new { sm.MachineDataID, sm.DoughVariant }).IsUnique(true);

哪一种方法得到了想要的解决方案

你应该把Index属性放在实体类的顶部,并在string[]中定义多个键。

[Index("FirstColumn", "SecondColumn", IsUnique = true, Name = "My_Unique_Index")]
public class Entity


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