Restricciones clave únicas para múltiples columnas en Entity Framework

244

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

Quiero hacer la combinación entre FirstColumny SecondColumncomo única.

Ejemplo:

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

¿Hay alguna manera de hacerlo?

Bassam Alugili
fuente

Respuestas:

370

Con Entity Framework 6.1, ahora puede hacer esto:

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

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

El segundo parámetro en el atributo es donde puede especificar el orden de las columnas en el índice.
Más información: MSDN

Charlie
fuente
12
Esto es correcto para las anotaciones de datos :), si desea la respuesta para usar la API fluida, vea la respuesta de Niaher a continuación stackoverflow.com/a/25779348/2362036
tekiegirl
8
¡Pero lo necesito trabajando para claves foráneas! ¿Me puedes ayudar?
feedc0de
2
@ 0xFEEDC0DE vea mi respuesta a continuación que aborda el uso de claves foráneas en los índices.
Kryptos
1
¿Puedes publicar cómo usar este índice con linq en sql?
Bluebaron
44
@JJS - Lo hice funcionar donde una de las propiedades era una clave externa ... por casualidad, ¿es su clave un varchar o nvarchar? Hay un límite de longitud que puede usarse como una clave única. Stackoverflow.com/questions/2863993/…
Dave Lawrence
129

Encontré tres formas de resolver el problema.

Índices únicos en EntityFramework Core:

Primer enfoque:

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

El segundo enfoque para crear restricciones únicas con EF Core mediante el uso de claves alternativas.

Ejemplos

Una columna:

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

Múltiples columnas:

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

EF 6 y abajo:


Primer enfoque:

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

¡Este enfoque es muy rápido y útil, pero el problema principal es que Entity Framework no sabe nada sobre esos cambios!


Segundo enfoque:
lo encontré en esta publicación pero no lo intenté solo.

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

El problema de este enfoque es el siguiente: necesita DbMigration, ¿qué debe hacer si no lo tiene?


Tercer enfoque:
creo que este es el mejor, pero requiere algo de tiempo para hacerlo. Simplemente le mostraré la idea detrás de esto: en este enlace http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a puede encontrar el código para una anotación de datos clave ú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

El problema para este enfoque; ¿Cómo puedo combinarlos? Tengo una idea para extender esta implementación de Microsoft, por ejemplo:

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

Más adelante en el IDatabaseInitializer como se describe en el ejemplo de Microsoft, puede combinar las claves de acuerdo con el entero dado. Sin embargo, hay que tener en cuenta una cosa: si la propiedad única es de tipo cadena, debe establecer MaxLength.

Bassam Alugili
fuente
1
(y) Encuentro esta respuesta mejor. Otra cosa, el tercer enfoque puede no ser necesariamente el mejor. (Me gusta el primero en realidad). Personalmente prefiero no tener ningún artefacto EF transferido a mis clases de entidad.
Najeeb
1
Posiblemente, el segundo enfoque debería ser CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2}):? (ver BOL )
AK
2
Pregunta estúpida: ¿Por qué comienzas tu nombre con "IX_"?
Bastien Vandamme
1
@BastienVandamme es una buena pregunta. el índice de generación automática por EF comienza con IX_. Parece ser una convención en el índice EF por defecto, el nombre del índice será IX_ {nombre de propiedad}.
Bassam Alugili
1
Sí debería ser. Gracias por la implementación de Fluent API. Hay una grave falta de documentación sobre esto
JSON
75

Si usa Code-First , puede implementar una extensión 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);
    }
}

Luego úsalo así:

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

Lo que dará como resultado esta migración:

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

Y finalmente terminan en la base de datos como:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
fuente
3
¡Pero eso es índice, no restricción!
Roman Pokrovskij
3
En su segundo bloque de código ( this.Property(t => t.Email)), ¿qué es esa clase que contiene? (es decir, qué es this)
JoeBrockhaus
2
nvm. EntityTypeConfiguration<T>
JoeBrockhaus
55
@RomanPokrovskij: La diferencia entre un índice único y una restricción única parece ser una cuestión de cómo se mantienen sus registros en SQL Server. Consulte technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx para obtener más detalles.
Mass Dot Net
1
@niaher Aprecio tu buen método de extensión
Mohsen Afshin
18

Necesita definir una clave compuesta.

Con anotaciones de datos se ve así:

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

También puede hacer esto con modelBuilder al anular OnModelCreating especificando:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Admir Tuzović
fuente
2
Pero no son claves, solo las quiero como Unique, ¿la clave debería ser la Id? He actualizado la pregunta gracias por la ayuda!
Bassam Alugili
16

La respuesta de niaher que indica que para usar la API fluida que necesita una extensión personalizada puede haber sido correcta al momento de la escritura. Ahora puede (EF core 2.1) usar la API fluida de la siguiente manera:

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

Completar la respuesta @chuck para usar índices compuestos con claves foráneas .

Debe definir una propiedad que contendrá el valor de la clave externa. Luego puede usar esta propiedad dentro de la definición del índice.

Por ejemplo, tenemos compañía con empleados y solo tenemos una restricción única en (nombre, compañía) para cualquier empleado:

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

Ahora el mapeo de la clase Empleado:

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

Tenga en cuenta que también usé la extensión @niaher para anotaciones de índice únicas.

Kryptos
fuente
1
En este ejemplo, tiene Company y CompanyId. Esto significa que la persona que llama puede cambiar uno pero no el otro y tener una entidad con datos incorrectos.
LosManos
1
@LosManos ¿De quién estás hablando? La clase representa datos en una base de datos. Cambiar el valor mediante consultas garantizará la coherencia. Dependiendo de lo que pueda hacer la aplicación cliente, es posible que necesite implementar controles, pero ese no es el alcance del OP.
Kryptos
4

En la respuesta aceptada por @chuck, hay un comentario que dice que no funcionará en el caso de FK.

funcionó para mí, caso de 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
fuente
Largo tiempo. ¡Digamos que ha sido trabajo antes de mucho tiempo! gracias por la actualización, por favor agregue un comentario a la respuesta @chuck. Creo que Chuck hace mucho tiempo que no usa SO.
Bassam Alugili
¿La propiedad EmployeeID Here necesita un atributo para limitar su longitud para que se indexe? De lo contrario, se creó con VARCHAR (MAX) que no puede tener un índice. Agregue el atributo [StringLength (255)] a EmployeeID
Lord Darth Vader
EmployeeID es un GUID. Muchos tutoriales sugieren asignar el GUID a una cadena en lugar de guid, no sé por qué
dalios
3

Supongo que siempre quiere EntityIdser la clave principal, por lo que reemplazarla por una clave compuesta no es una opción (solo porque las claves compuestas son mucho más complicadas de trabajar y porque no es muy sensato tener claves primarias que también tengan significado en la lógica de negocios).

Lo menos que debe hacer es crear una clave única en ambos campos en la base de datos y verificar específicamente las excepciones de violación de clave única al guardar los cambios.

Además, podría (debería) verificar valores únicos antes de guardar los cambios. La mejor manera de hacerlo es mediante una Any()consulta, ya que minimiza la cantidad de datos transferidos:

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

Tenga en cuenta que este control solo nunca es suficiente. Siempre hay algo de latencia entre la verificación y la confirmación real, por lo que siempre necesitará la restricción única + manejo de excepciones.

Gert Arnold
fuente
3
Gracias @GertArnold por la respuesta, pero no quiero validar la unicidad en la capa empresarial, este es un trabajo de base de datos y esto se hará en la base de datos.
Bassam Alugili
2
OK, entonces adhiérete al índice único. Pero tendrá que lidiar con violaciones de índice en la capa empresarial de todos modos.
Gert Arnold
1
Desde afuera, cuando reciba este tipo de excepción, lo detectaré y luego tal vez informe el error y rompa el proceso o cierre la aplicación.
Bassam Alugili
3
Sí, ¿tengo que responder a eso? Recuerda que no sé nada de tu aplicación, no puedo decirte cuál es la mejor manera de lidiar con estas excepciones, solo que tienes que lidiar con ellas.
Gert Arnold
2
Tenga cuidado con las restricciones únicas de DB con EF. Si hace esto y luego termina teniendo una actualización que cambia los valores de una de las columnas que es parte de la clave única, la entidad frameowkr fallará en el guardado a menos que agregue una capa de transacción completa. Por ejemplo: el objeto de página tiene una colección secundaria de elementos. Cada elemento tiene SortOrder. Desea que la combinación de PageID y SortOrder sea única. En el extremo frontal, el orden de los elementos de los flip flops del usuario con los ordenadores 1 y 2. Entity Framework fallará al guardar b / c que está tratando de actualizar los ordenadores uno por uno.
EGP
1

Recientemente se agregó una clave compuesta con la singularidad de 2 columnas utilizando el enfoque que recomienda 'chuck', gracias @chuck. Solo este enfoque me pareció más limpio:

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
fuente