Contraintes de clé uniques pour plusieurs colonnes dans Entity Framework

244

J'utilise d'abord le code Entity Framework 5.0;

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

Je veux rendre la combinaison entre FirstColumnet SecondColumnaussi unique.

Exemple:

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

Y'a-t'il un quelconque moyen d'y arriver?

Bassam Alugili
la source

Réponses:

370

Avec Entity Framework 6.1, vous pouvez désormais procéder comme suit:

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

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

Le deuxième paramètre de l'attribut est l'endroit où vous pouvez spécifier l'ordre des colonnes dans l'index.
Plus d'informations: MSDN

Charlie
la source
12
C'est correct pour les annotations de données :), si vous voulez la réponse pour utiliser l'API fluide, voir la réponse de Niaher ci-dessous stackoverflow.com/a/25779348/2362036
tekiegirl
8
Mais j'en ai besoin pour les clés étrangères! Pouvez-vous m'aider?
feedc0de
2
@ 0xFEEDC0DE voir ma réponse ci-dessous qui traite de l'utilisation des clés étrangères dans les index.
Kryptos
1
Pouvez-vous poster comment utiliser cet index avec linq pour sql?
Bluebaron
4
@JJS - Je l'ai fait fonctionner là où l'une des propriétés était une clé étrangère. Par hasard, votre clé est-elle un varchar ou un nvarchar? Il y a une limite à la longueur de laquelle peut être utilisée comme clé unique .. stackoverflow.com/questions/2863993/…
Dave Lawrence
129

J'ai trouvé trois façons de résoudre le problème.

Index uniques dans EntityFramework Core:

Première approche:

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

La seconde approche pour créer des contraintes uniques avec EF Core en utilisant des clés alternatives.

Exemples

Une colonne:

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

Plusieurs colonnes:

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

EF 6 et inférieurs:


Première approche:

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

Cette approche est très rapide et utile mais le principal problème est qu'Entity Framework ne sait rien de ces changements!


Deuxième approche:
je l'ai trouvé dans ce post mais je n'ai pas essayé par moi-même.

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

Le problème de cette approche est le suivant: elle a besoin de DbMigration, alors que faites-vous si vous ne l'avez pas?


Troisième approche:
je pense que c'est la meilleure mais cela demande du temps. Je vais juste vous montrer l'idée derrière cela: Dans ce lien http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a, vous pouvez trouver le code d'annotation de données de clé unique:

[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

Le problème de cette approche; Comment puis-je les combiner? J'ai une idée d'étendre cette implémentation Microsoft par exemple:

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

Plus loin dans IDatabaseInitializer comme décrit dans l'exemple Microsoft, vous pouvez combiner les clés en fonction de l'entier donné. Une chose doit être notée cependant: si la propriété unique est de type chaîne, vous devez définir la longueur maximale.

Bassam Alugili
la source
1
(y) Je trouve cette réponse meilleure. Autre chose, la troisième approche n'est pas nécessairement la meilleure. (J'aime le premier en fait.) Personnellement, je préfère ne pas avoir d'artefacts EF transférés dans mes classes d'entités.
Najeeb
1
Peut-être, la deuxième approche devrait être CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2}):? (voir BOL )
AK
2
Question stupide: pourquoi donc vous commencez tout votre nom par "IX_"?
Bastien Vandamme
1
@BastienVandamme c'est une bonne question. l'index de génération automatique par EF commence par IX_. Il semble que ce soit une convention dans l'index EF par défaut, le nom d'index sera IX_ {nom de la propriété}.
Bassam Alugili
1
Oui, ça devrait l'être. Merci pour l'implémentation de l'API Fluent. Il y a un sérieux manque de documentation à ce sujet
JSON
75

Si vous utilisez Code-First , vous pouvez implémenter une extension personnalisée 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);
    }
}

Ensuite, utilisez-le comme ceci:

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

Ce qui entraînera cette 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");
}

Et finalement finir dans la base de données comme:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
la source
3
Mais ce n'est pas une contrainte d'index!
Roman Pokrovskij
3
Dans votre deuxième bloc de code ( this.Property(t => t.Email)), quelle est cette classe contenant? (c.-à-d. ce qui est this)
JoeBrockhaus
2
nvm. EntityTypeConfiguration<T>
JoeBrockhaus
5
@RomanPokrovskij: La différence entre un index unique et une contrainte unique semble être une question de la façon dont les enregistrements de celui-ci sont conservés dans SQL Server. Voir technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx pour plus de détails.
Mass Dot Net
1
@niaher J'apprécie votre belle méthode d'extension
Mohsen Afshin
18

Vous devez définir une clé composite.

Avec les annotations de données, cela ressemble à ceci:

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

Vous pouvez également le faire avec modelBuilder lors de la substitution de OnModelCreating en spécifiant:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Admir Tuzović
la source
2
Mais ce ne sont pas des clés que je veux juste comme uniques, la clé devrait être l'identifiant? J'ai mis à jour la question merci pour votre aide!
Bassam Alugili
16

La réponse de niaher indiquant que pour utiliser l'API fluide, vous avez besoin d'une extension personnalisée peut-être correcte au moment de la rédaction. Vous pouvez maintenant (EF core 2.1) utiliser l'API couramment comme suit:

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

Compléter la réponse @chuck pour utiliser des indices composites avec des clés étrangères .

Vous devez définir une propriété qui contiendra la valeur de la clé étrangère. Vous pouvez ensuite utiliser cette propriété dans la définition d'index.

Par exemple, nous avons une entreprise avec des employés et seulement nous avons une contrainte unique sur (nom, entreprise) pour tout employé:

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

Maintenant, le mappage de la 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);
    }
}

Notez que j'ai également utilisé l'extension @niaher pour l'annotation d'index unique.

Kryptos
la source
1
Dans cet exemple, vous avez à la fois Company et CompanyId. Cela signifie que l'appelant peut changer l'un mais pas l'autre et avoir une entité avec des données incorrectes.
LosManos
1
@LosManos De quel appelant parlez-vous? La classe représente des données dans une base de données. La modification de la valeur par le biais de requêtes garantira la cohérence. En fonction de ce que l'application client peut faire, vous devrez peut-être implémenter des vérifications, mais ce n'est pas la portée de l'OP.
Kryptos
4

Dans la réponse acceptée par @chuck, il y a un commentaire disant que cela ne fonctionnera pas dans le cas de FK.

cela a fonctionné pour moi, cas 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
la source
Longtemps. disons que cela a fonctionné avant longtemps! merci pour la mise à jour, veuillez ajouter un commentaire à la réponse @chuck. Je pense que Chuck est avant longtemps n'utilise pas SO.
Bassam Alugili
La propriété EmployeeID Here a-t-elle besoin d'un attribut pour limiter sa longueur afin d'être indexée? Sinon, c'est créé avec VARCHAR (MAX) qui ne peut pas avoir d'index? Ajouter l'attribut [StringLength (255)] à EmployeeID
Lord Darth Vader
EmployeeID est un GUID. De nombreux tutoriels suggèrent de mapper le GUID sur une chaîne au lieu de guid, je ne sais pas pourquoi
dalios
3

Je suppose que vous voulez toujours EntityIdêtre la clé primaire, donc la remplacer par une clé composite n'est pas une option (ne serait-ce que parce que les clés composites sont beaucoup plus compliquées à utiliser et à utiliser parce qu'il n'est pas très judicieux d'avoir des clés primaires qui ont également un sens dans la logique métier).

Le moins que vous puissiez faire est de créer une clé unique sur les deux champs de la base de données et de vérifier spécifiquement les exceptions de violation de clé unique lors de l'enregistrement des modifications.

De plus, vous pouvez (devriez) vérifier les valeurs uniques avant d'enregistrer les modifications. La meilleure façon de le faire est par une Any()requête, car elle minimise la quantité de données transférées:

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

Attention, ce chèque à lui seul n'est jamais suffisant. Il y a toujours une certaine latence entre la vérification et la validation réelle, vous aurez donc toujours besoin de la gestion unique des contraintes et des exceptions.

Gert Arnold
la source
3
Merci @GertArnold pour la réponse mais je ne veux pas valider l'unicité sur la couche métier c'est un travail de base de données et cela doit être fait dans la base de données!
Bassam Alugili
2
OK, respectez alors l'index unique. Mais vous devrez de toute façon gérer les violations d'index dans la couche métier.
Gert Arnold
1
de l'extérieur lorsque je reçois ce genre d'exception, je vais attraper et peut-être signaler l'erreur et interrompre le processus ou arrêter l'application.
Bassam Alugili
3
Oui, ... dois-je répondre à cela? N'oubliez pas que je ne sais rien de votre demande, je ne peux pas vous dire quelle est la meilleure façon de traiter ces exceptions, mais seulement que vous devez y faire face.
Gert Arnold
2
Méfiez-vous des contraintes uniques DB avec EF. Si vous faites cela et que vous vous retrouvez avec une mise à jour qui bascule les valeurs de l'une des colonnes qui fait partie de la clé unique, l'entité frameowkr échouera lors de l'enregistrement sauf si vous ajoutez une couche de transaction entière. Par exemple: L'objet Page a une collection enfant d'éléments. Chaque élément a SortOrder. Vous voulez que la combinaison de PageID et SortOrder soit unique. En front-end, l'utilisateur bascule l'ordre des éléments avec l'ordre de tri 1 et 2. Entity Framework échouera lors de l'enregistrement b / c qu'il essaie de mettre à jour les trieurs un par un.
EGP
1

Récemment ajouté une clé composite avec l'unicité de 2 colonnes en utilisant l'approche recommandée par 'chuck', merci @chuck. Seule cette approche m'a paru plus propre:

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
la source