Vincoli chiave univoci per più colonne in Entity Framework


244

Sto usando prima Entity Framework 5.0 Code;

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

Voglio rendere la combinazione tra FirstColumne SecondColumnunica.

Esempio:

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

Esiste un modo per farlo?

Risposte:


370

Con Entity Framework 6.1, ora puoi farlo:

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

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

Il secondo parametro nell'attributo è dove è possibile specificare l'ordine delle colonne nell'indice.
Ulteriori informazioni: MSDN


12
Questo è corretto per le annotazioni di dati :), se si desidera che la risposta per utilizzando l'API fluente vedere la risposta di Niaher sotto stackoverflow.com/a/25779348/2362036
tekiegirl

8
Ma ho bisogno che funzioni per le chiavi esterne! Mi potete aiutare?
feedc0de,

2
@ 0xFEEDC0DE vedi sotto la mia risposta che affronta l'uso di chiavi esterne negli indici.
Kryptos,

1
Puoi pubblicare come utilizzare questo indice con linq a sql?
Bluebaron il

4
@JJS - L'ho fatto funzionare dove una delle proprietà era una chiave esterna .. per caso la tua chiave è varchar o nvarchar? Esiste un limite alla lunghezza che può essere utilizzata come chiave univoca .. stackoverflow.com/questions/2863993/…
Dave Lawrence,

129

Ho trovato tre modi per risolvere il problema.

Indici univoci in EntityFramework Core:

Primo approccio:

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

Il secondo approccio per creare vincoli univoci con EF Core utilizzando i tasti alternativi.

Esempi

Una colonna:

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

Colonne multiple:

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

EF 6 e inferiore:


Primo approccio:

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

Questo approccio è molto rapido e utile, ma il problema principale è che Entity Framework non sa nulla di questi cambiamenti!


Secondo approccio: l'
ho trovato in questo post ma non ho provato da solo.

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

Il problema di questo approccio è il seguente: ha bisogno di DbMigration, quindi cosa fai se non ce l'hai?


Terzo approccio:
penso che questo sia il migliore, ma richiede del tempo per farlo. Ti mostrerò solo l'idea alla base: in questo link http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a puoi trovare il codice per l'annotazione dei dati chiave univoci:

[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

Il problema per questo approccio; Come posso combinarli? Ho un'idea per estendere questa implementazione Microsoft ad esempio:

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

Più avanti in IDatabaseInitializer come descritto nell'esempio Microsoft è possibile combinare le chiavi in ​​base al numero intero indicato. È necessario notare una cosa: se la proprietà unica è di tipo stringa, è necessario impostare MaxLength.


1
(y) Trovo migliore questa risposta. Un'altra cosa, il terzo approccio potrebbe non essere necessariamente il migliore. (In realtà mi piace il primo.) Personalmente preferisco non avere artefatti EF riportati nelle mie classi di entità.
Najeeb,

1
Forse, il secondo approccio dovrebbe essere CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2}):? (vedi BOL )
AK

2
Domanda stupida: perché così inizi tutto il tuo nome con "IX_"?
Bastien Vandamme,

1
@BastienVandamme è una buona domanda. l'indice di generazione automatica di EF inizia con IX_. Sembra essere una convenzione nell'indice EF per impostazione predefinita, il nome dell'indice sarà IX_ {nome proprietà}.
Bassam Alugili,

1
Sì, dovrebbe essere. Grazie per l'implementazione fluida dell'API. C'è una grave mancanza di documentazione su questo
JSON

75

Se si utilizza Code-First , è possibile implementare un'estensione personalizzata 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);
    }
}

Quindi usalo così:

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

Ciò comporterà questa migrazione:

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 alla fine finiscono nel database come:

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

3
Ma questo è indice non vincolo!
Roman Pokrovskij,

3
Nel tuo secondo blocco di codice ( this.Property(t => t.Email)), che cos'è quella classe contenente? (vale a dire: cos'è this)
JoeBrockhaus,

2
NVM. EntityTypeConfiguration<T>
JoeBrockhaus,

5
@RomanPokrovskij: la differenza tra un indice univoco e un vincolo univoco sembra dipendere da come vengono conservati i record di esso in SQL Server. Vedere technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx per i dettagli.
Mass Dot Net

1
@niaher apprezzo il tuo bel metodo di estensione
Mohsen Afshin

18

Devi definire una chiave composita.

Con le annotazioni dei dati è simile al seguente:

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

Puoi anche farlo con modelBuilder quando esegui l'override di OnModelCreating specificando:

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

2
Ma non sono le chiavi, le voglio solo come Uniche, la chiave dovrebbe essere l'ID? Ho aggiornato la domanda grazie per l'aiuto!
Bassam Alugili,

16

La risposta di Niaher affermando che per usare l'API fluente è necessaria un'estensione personalizzata potrebbe essere stata corretta al momento della scrittura. Ora puoi (EF core 2.1) utilizzare l'API fluente come segue:

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

9

Completamento della risposta @chuck per l'utilizzo di indici compositi con chiavi esterne .

È necessario definire una proprietà che conterrà il valore della chiave esterna. È quindi possibile utilizzare questa proprietà all'interno della definizione dell'indice.

Ad esempio, abbiamo compagnia con dipendenti e solo noi abbiamo un vincolo univoco su (nome, azienda) per qualsiasi dipendente:

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

Ora la mappatura della 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);
    }
}

Nota che ho anche usato l'estensione @niaher per l'annotazione univoca dell'indice.


1
In questo esempio hai sia Company che CompanyId. Ciò significa che il chiamante può cambiare uno ma non l'altro e avere un'entità con dati errati.
LosManos

1
@LosManos Di quale chiamante stai parlando? La classe rappresenta i dati in un database. La modifica del valore tramite query garantirà coerenza. A seconda di ciò che può fare l'applicazione client, potrebbe essere necessario implementare controlli, ma non rientra nell'ambito dell'OP.
Kryptos,

4

Nella risposta accettata da @chuck, c'è un commento che dice che non funzionerà nel caso di FK.

ha funzionato per me, caso di 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; }
}

A lungo. diciamo che è stato un lavoro da molto tempo! grazie per l'aggiornamento si prega di aggiungere un commento alla risposta @chuck. Penso che Chuck sia molto tempo prima non usa SO.
Bassam Alugili,

La proprietà EmployeeID qui necessita di un attributo che ne limiti la lunghezza affinché possa essere indicizzata? Altrimenti viene creato con VARCHAR (MAX) che non può avere un indice? Aggiungi l'attributo [StringLength (255)] a EmployeeID
Lord Darth Vader il

EmployeeID è un GUID. Molti tutorial suggeriscono di mappare il GUID sulla stringa anziché su guid, non so perché
dalios

3

Presumo che tu voglia sempre EntityIdessere la chiave primaria, quindi sostituirla con una chiave composita non è un'opzione (se non altro perché le chiavi composite sono molto più complicate con cui lavorare e perché non è molto ragionevole avere chiavi primarie che abbiano anche un significato nella logica aziendale).

Il minimo da fare è creare una chiave univoca su entrambi i campi nel database e verificare in modo specifico le eccezioni alla violazione delle chiavi univoche durante il salvataggio delle modifiche.

Inoltre, è possibile (dovrebbe) verificare i valori univoci prima di salvare le modifiche. Il modo migliore per farlo è tramite una Any()query, perché riduce al minimo la quantità di dati trasferiti:

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

Attenzione che questo controllo da solo non è mai abbastanza. C'è sempre un po 'di latenza tra il controllo e il commit effettivo, quindi avrai sempre bisogno del vincolo univoco + gestione delle eccezioni.


3
Grazie @GertArnold per la risposta, ma non voglio convalidare l'unicità a livello aziendale, questo è un lavoro di database e questo deve essere fatto nel database!
Bassam Alugili,

2
OK, attenersi all'indice univoco quindi. Ma dovrai comunque affrontare le violazioni dell'indice nel livello aziendale.
Gert Arnold,

1
dall'esterno quando ricevo questo tipo di eccezione, verro 'catturato e forse segnalando l'errore e interrompevo il processo o spegnevo l'applicazione.
Bassam Alugili,

3
Sì, devo rispondere a questo? Ricorda che non conosco nulla della tua candidatura, non posso dirti quale sia il modo migliore per gestire queste eccezioni, solo che devi gestirle.
Gert Arnold,

2
Diffidare di vincoli univoci DB con EF. Se lo fai e poi finisci con un aggiornamento che ribalta i valori di una delle colonne che fa parte della chiave univoca, l'entità frameowkr fallirà nel salvataggio a meno che non aggiungi un intero livello di transazione. Ad esempio: l'oggetto Page ha una raccolta figlio di Elements. Ogni elemento ha SortOrder. Desideri che la combinazione di PageID e SortOrder sia unica. Nel front-end, l'utente capovolge l'ordine degli elementi con l'ordinamento 1 e 2. Entity Framework fallirà il salvataggio b / c che sta provando ad aggiornare gli ordinamenti uno alla volta.
EGP,

1

Recentemente aggiunto una chiave composita con l'unicità di 2 colonne usando l'approccio consigliato da 'chuck', grazie a @chuck. Solo questo approccio mi è sembrato più pulito:

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; }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.