Restrições de chaves exclusivas para várias colunas no Entity Framework

Estou usando o 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;} } 

Eu quero fazer a combinação entre FirstColumn e SecondColumn como única.

Exemplo:

 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 

Existe alguma maneira de fazer isso?

    Com o Entity Framework 6.1, você pode fazer isso agora:

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

    O segundo parâmetro no atributo é onde você pode especificar a ordem das colunas no índice.
    Mais informações: MSDN

    Se você estiver usando o Code-First , poderá implementar uma extensão personalizada 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); } } 

    Então use assim:

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

    O que resultará nessa migration:

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

    E, eventualmente, acabar no database como:

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

    Eu encontrei três maneiras de resolver o problema.

    Índices exclusivos no EntityFramework Core:

    Primeira abordagem:

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

    A segunda abordagem para criar restrições únicas com o EF Core usando Chaves Alternativas.

    Exemplos

    Uma coluna:

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

    Múltiplas colunas:

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

    EF 6 e abaixo:


    Primeira abordagem:

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

    Essa abordagem é muito rápida e útil, mas o principal problema é que o Entity Framework não sabe nada sobre essas mudanças!


    Segunda abordagem:
    Eu encontrei neste post, mas eu não tentei por mim mesmo.

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

    O problema dessa abordagem é o seguinte: Ele precisa do DbMigration, então o que você faz se não tiver?


    Terceira abordagem:
    Eu acho que este é o melhor, mas requer algum tempo para fazê-lo. Eu mostrarei a você a idéia por trás disso: neste link http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a você pode encontrar o código para uma anotação de dados de chave única:

     [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 

    O problema dessa abordagem; Como posso combiná-los? Eu tenho uma idéia para estender essa implementação da Microsoft, por exemplo:

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

    Posteriormente no IDatabaseInitializer, conforme descrito no exemplo da Microsoft, você pode combinar as chaves de acordo com o inteiro dado. No entanto, uma coisa deve ser notada: Se a propriedade exclusiva for do tipo string, você deverá definir o MaxLength.

    Você precisa definir uma chave composta.

    Com as annotations de dados, é assim:

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

    Você também pode fazer isso com modelBuilder ao sobrescrever OnModelCreating especificando:

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

    Completando resposta @chuck por usar índices compostos com foreign keys .

    Você precisa definir uma propriedade que manterá o valor da chave estrangeira. Você pode então usar essa propriedade dentro da definição do índice.

    Por exemplo, temos companhia com funcionários e só temos uma restrição exclusiva em (nome, empresa) para qualquer funcionário:

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

    Agora o mapeamento da class Employee:

     class EmployeeMap : EntityTypeConfiguration { 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); } } 

    Observe que também usei a extensão @niaher para uma anotação de índice exclusiva.

    Eu suponho que você sempre quer EntityId para ser a chave primária, então substituí-lo por uma chave composta não é uma opção (se apenas porque as chaves compostas são muito mais complicadas de trabalhar e porque não é muito sensato ter chaves primárias que também têm significado na lógica de negócios).

    O mínimo que você deve fazer é criar uma chave exclusiva nos dois campos do database e verificar especificamente exceções de violação de chave exclusivas ao salvar as alterações.

    Além disso, você pode (deve) verificar valores exclusivos antes de salvar as alterações. A melhor maneira de fazer isso é por meio de uma consulta Any() , porque minimiza a quantidade de dados transferidos:

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

    Cuidado, este cheque nunca é suficiente. Há sempre alguma latência entre a verificação e a confirmação real, portanto, você sempre precisará da manipulação exclusiva de restrição + exceção.