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

243

Estou usando o código do Entity Framework 5.0 primeiro;

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 FirstColumne SecondColumncomo ú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?

Bassam Alugili
fonte

Respostas:

368

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

[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

charlie
fonte
12
Isso é correto para anotações de dados :), se você quer a resposta para usar a API fluente ver a resposta de Niaher abaixo stackoverflow.com/a/25779348/2362036
tekiegirl
8
Mas eu preciso dele trabalhando para chaves estrangeiras! Pode me ajudar?
feedc0de
2
@ 0xFEEDC0DE veja minha resposta abaixo que aborda o uso de chaves estrangeiras em índices.
Kryptos
1
Você pode postar como usar esse índice com linq to sql?
Bluebaron
4
@JJS - Eu consegui trabalhar onde uma das propriedades era uma chave estrangeira ... por acaso sua chave é um varchar ou nvarchar? Há um limite para o comprimento que pode ser usado como uma chave exclusiva. Stackoverflow.com/questions/2863993/…
Dave Lawrence
129

Encontrei três maneiras de resolver o problema.

Índices exclusivos no EntityFramework Core:

Primeira abordagem:

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

A segunda abordagem para criar restrições exclusivas com o EF Core usando chaves alternativas.

Exemplos

Uma coluna:

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

Várias colunas:

modelBuilder.Entity<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 alterações!


Segunda abordagem:
Encontrei-a neste post, mas não tentei sozinha.

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 o possui?


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

[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 para esta abordagem; Como posso combiná-los? Eu tenho uma idéia para estender esta 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 número inteiro especificado. Porém, uma coisa deve ser observada: se a propriedade exclusiva for do tipo string, você deverá definir o MaxLength.

Bassam Alugili
fonte
1
(y) Acho melhor esta resposta. Outra coisa, a terceira abordagem pode não ser necessariamente a melhor. (Na verdade, eu gosto do primeiro.) Pessoalmente, prefiro não ter nenhum artefato EF transferido para minhas classes de entidade.
Najeb #
1
Possivelmente, a segunda abordagem deve ser CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2}):? (ver BOL )
AK
2
Pergunta estúpida: Por que você começa todo o seu nome com "IX_"?
Bastien Vandamme
1
@BastienVandamme é uma boa pergunta. o índice de geração automática da EF começa com IX_. Parece ser uma convenção no índice EF, por padrão, o nome do índice será IX_ {nome da propriedade}.
Bassam Alugili
1
Sim deveria ser. Obrigado pela implementação da API Fluent. Existe uma grave falta de documentação sobre isso
JSON
75

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

Em seguida, use-o 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 migração:

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, acabam no banco de dados como:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
fonte
3
Mas isso é índice, não restrição!
Roman Pokrovskij
3
No seu segundo bloco de código ( this.Property(t => t.Email)), o que é que contém classe? (isto é: o que é this)
JoeBrockhaus
2
NVM. EntityTypeConfiguration<T>
JoeBrockhaus
5
@RomanPokrovskij: A diferença entre um índice exclusivo e uma restrição exclusiva parece ser uma questão de como os registros dele são mantidos no SQL Server. Consulte technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx para obter detalhes.
Mass Dot Net
1
@niaher Eu aprecio sua agradável método de extensão
Mohsen Afshin
18

Você precisa definir uma chave composta.

Com anotações de dados, fica 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 o modelBuilder ao substituir OnModelCreating especificando:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Admir Tuzović
fonte
2
Mas as chaves não são, eu só quero que elas sejam únicas. A chave deve ser o ID? Atualizei a pergunta, obrigado pela ajuda!
Bassam Alugili
16

A resposta de niaher afirmando que, para usar a API fluente, você precisa de uma extensão personalizada pode estar correta no momento da redação. Agora você pode (EF core 2.1) usar a API fluente da seguinte maneira:

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

Concluindo a resposta @chuck para usar índices compostos com chaves estrangeiras .

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

Por exemplo, temos empresa com funcionários e somente temos uma restrição exclusiva (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 classe 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);
    }
}

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

Kryptos
fonte
1
Neste exemplo, você tem Company e CompanyId. Isso significa que o chamador pode alterar um, mas não o outro, e ter uma entidade com dados incorretos.
21717 LosManos
1
@LosManos De quem você está falando? A classe representa dados em um banco de dados. Alterar o valor por meio de consultas garantirá consistência. Dependendo do que o aplicativo cliente pode fazer, pode ser necessário implementar verificações, mas esse não é o escopo do OP.
Kryptos
4

Na resposta aceita por @chuck, há um comentário dizendo que não funcionará no caso do FK.

funcionou para mim, caso do 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; }
}
dalios
fonte
Muito tempo. digamos que tenha sido trabalho há muito tempo! obrigado pela atualização, adicione um comentário à resposta @chuck. Eu acho que Chuck é antes de muito tempo não usa SO.
Bassam Alugili
A propriedade EmployeeID Here precisa de um atributo para limitar seu comprimento para que seja indexado? Senão, é criado com VARCHAR (MAX), que não pode ter um índice? Add Attribute [StringLength (255)] to EmployeeID
Lord Darth Vader
CódigoDoFuncionário é um GUID. Muitos tutoriais sugerem mapear o GUID para string em vez de guid, não sei porquê
dalios
3

Suponho que você sempre queira EntityIdser a chave primária, portanto, substituí-la por uma chave composta não é uma opção (apenas porque as chaves compostas são muito mais complicadas de se trabalhar e porque não é muito sensato ter chaves primárias que também tenham significado na lógica de negócios).

O mínimo que você deve fazer é criar uma chave exclusiva nos dois campos do banco de dados e verificar especificamente se há 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 é através de uma Any()consulta, porque minimiza a quantidade de dados transferidos:

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

Cuidado que esse cheque sozinho nunca é suficiente. Sempre há alguma latência entre a verificação e a confirmação real, portanto, você sempre precisará da restrição exclusiva + tratamento de exceções.

Gert Arnold
fonte
3
Obrigado @GertArnold pela resposta, mas não quero validar a exclusividade na camada de negócios. Esse é um trabalho de banco de dados e isso deve ser feito no banco de dados!
Bassam Alugili
2
OK, atenha-se ao índice exclusivo então. Mas você terá que lidar com violações de índice na camada de negócios de qualquer maneira.
Gert Arnold
1
do lado de fora, quando eu receber esse tipo de exceção, capturarei e, talvez, relate o erro e quebre o processo ou encerre o aplicativo.
Bassam Alugili
3
Sim ... preciso responder a isso? Lembre-se de que não sei nada sobre sua aplicação, não posso lhe dizer qual é a melhor maneira de lidar com essas exceções, apenas que você precisa lidar com elas.
precisa saber é o seguinte
2
Cuidado com as restrições exclusivas do banco de dados com o EF. Se você fizer isso e acabar tendo uma atualização que inverta os valores de uma das colunas que faz parte da chave exclusiva, a entidade frameowkr falhará no salvamento, a menos que você adicione uma camada de transação inteira. Por exemplo: O objeto Página tem uma coleção filho de Elementos. Cada elemento possui SortOrder. Você deseja que a combinação de PageID e SortOrder seja única. No front end, os flip-flops dos usuários ordenam os elementos com as ordens de classificação 1 e 2. O Entity Framework falhará ao salvar porque está tentando atualizar as ordens de classificação uma de cada vez.
EGP
1

Recentemente, adicionamos uma chave composta com a exclusividade de 2 colunas usando a abordagem recomendada pelo 'chuck', obrigado @chuck. Só isso abordado me pareceu mais limpo:

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; }
Shoeb Hasan
fonte