Eindeutige Schlüsseleinschränkungen für mehrere Spalten in Entity Framework

243

Ich verwende zuerst 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;}
 }

Ich möchte die Kombination zwischen FirstColumnund SecondColumnals einzigartig machen.

Beispiel:

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

Gibt es sowieso etwas zu tun?

Bassam Alugili
quelle

Antworten:

368

Mit Entity Framework 6.1 können Sie jetzt Folgendes tun:

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

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

Im zweiten Parameter des Attributs können Sie die Reihenfolge der Spalten im Index angeben.
Weitere Informationen: MSDN

Charlie
quelle
12
Dies ist korrekt für Datenanmerkungen :), wenn Sie die Antwort für die Verwendung der fließenden API wünschen, siehe Niahers Antwort unter stackoverflow.com/a/25779348/2362036
tekiegirl
8
Aber ich brauche es für Fremdschlüssel! Können Sie mir helfen?
feedc0de
2
@ 0xFEEDC0DE siehe meine Antwort unten, die sich mit der Verwendung von Fremdschlüsseln in Indizes befasst.
Kryptos
1
Können Sie posten, wie dieser Index mit linq to sql verwendet wird?
Bluebaron
4
@JJS - Ich habe es zum Laufen gebracht, wo eine der Eigenschaften ein Fremdschlüssel war. Ist Ihr Schlüssel zufällig ein Varchar oder ein Nvarchar? Es gibt eine Begrenzung für die Länge, die als eindeutiger Schlüssel verwendet werden kann. Stackoverflow.com/questions/2863993/…
Dave Lawrence
129

Ich habe drei Möglichkeiten gefunden, um das Problem zu lösen.

Eindeutige Indizes in EntityFramework Core:

Erste Ansatz:

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

Der zweite Ansatz zum Erstellen eindeutiger Einschränkungen mit EF Core mithilfe alternativer Schlüssel.

Beispiele

Eine Spalte:

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

Mehrere Spalten:

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

EF 6 und darunter:


Erste Ansatz:

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

Dieser Ansatz ist sehr schnell und nützlich, aber das Hauptproblem ist, dass Entity Framework nichts über diese Änderungen weiß!


Zweiter Ansatz:
Ich habe ihn in diesem Beitrag gefunden, aber nicht selbst ausprobiert.

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

Das Problem dieses Ansatzes ist das folgende: Es benötigt DbMigration. Was tun Sie also, wenn Sie es nicht haben?


Dritter Ansatz:
Ich denke, dies ist der beste, aber es erfordert einige Zeit, um dies zu tun. Ich werde Ihnen nur die Idee dahinter zeigen: Unter diesem Link http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a finden Sie den Code für die eindeutige Annotation von Schlüsseldaten:

[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

Das Problem für diesen Ansatz; Wie kann ich sie kombinieren? Ich habe eine Idee, diese Microsoft-Implementierung zu erweitern, zum Beispiel:

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

Später im IDatabaseInitializer, wie im Microsoft-Beispiel beschrieben, können Sie die Schlüssel gemäß der angegebenen Ganzzahl kombinieren. Eines muss jedoch beachtet werden: Wenn die eindeutige Eigenschaft vom Typ string ist, müssen Sie die MaxLength festlegen.

Bassam Alugili
quelle
1
(y) Ich finde diese Antwort besser. Eine andere Sache, der dritte Ansatz muss nicht unbedingt der beste sein. (Ich mag das erste wirklich.) Ich persönlich bevorzuge es, keine EF-Artefakte in meine Entitätsklassen übertragen zu lassen.
Najeeb
1
Möglicherweise sollte der zweite Ansatz sein : CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2})? (siehe BOL )
AK
2
Dumme Frage: Warum fängst du deinen ganzen Namen mit "IX_" an?
Bastien Vandamme
1
@ BastienVandamme ist eine gute Frage. Der automatisch generierte Index von EF beginnt mit IX_. Es scheint standardmäßig eine Konvention im EF-Index zu sein. Der Indexname lautet IX_ {Eigenschaftsname}.
Bassam Alugili
1
Ja sollte es sein. Vielen Dank für die Implementierung der Fluent API. Es gibt einen ernsthaften Mangel an Dokumentation zu diesem Thema
JSON
75

Wenn Sie Code-First verwenden , können Sie eine benutzerdefinierte Erweiterung HasUniqueIndexAnnotation implementieren

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

Dann benutze es so:

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

Was zu dieser Migration führen wird:

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

Und schließlich in der Datenbank landen als:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
quelle
3
Aber das ist Index keine Einschränkung!
Roman Pokrovskij
3
this.Property(t => t.Email)Was enthält die Klasse in Ihrem zweiten Codeblock ( )? (dh: was ist this)
JoeBrockhaus
2
nvm. EntityTypeConfiguration<T>
JoeBrockhaus
5
@RomanPokrovskij: Der Unterschied zwischen einem eindeutigen Index und einer eindeutigen Einschränkung scheint darauf zurückzuführen zu sein, wie Datensätze in SQL Server verwaltet werden. Weitere Informationen finden Sie unter technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx .
Mass Dot Net
1
@niaher Ich schätze Ihre nette Erweiterungsmethode
Mohsen Afshin
18

Sie müssen einen zusammengesetzten Schlüssel definieren.

Mit Datenanmerkungen sieht es so aus:

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

Sie können dies auch mit modelBuilder tun, wenn Sie OnModelCreating überschreiben, indem Sie Folgendes angeben:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Bewunderer Tuzović
quelle
2
Aber das sind keine Schlüssel, die ich nur als eindeutig haben möchte. Der Schlüssel sollte die ID sein. Ich habe die Frage aktualisiert, danke für die Hilfe!
Bassam Alugili
16

Die Antwort von niaher, dass Sie zur Verwendung der fließenden API eine benutzerdefinierte Erweiterung benötigen, war zum Zeitpunkt des Schreibens möglicherweise korrekt. Sie können jetzt (EF Core 2.1) die fließende API wie folgt verwenden:

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

Vervollständigung der @ chuck-Antwort für die Verwendung zusammengesetzter Indizes mit Fremdschlüsseln .

Sie müssen eine Eigenschaft definieren, die den Wert des Fremdschlüssels enthält. Sie können diese Eigenschaft dann in der Indexdefinition verwenden.

Zum Beispiel haben wir eine Firma mit Mitarbeitern und nur wir haben eine eindeutige Einschränkung (Name, Firma) für jeden Mitarbeiter:

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

Nun die Zuordnung der Employee-Klasse:

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

Beachten Sie, dass ich auch die Erweiterung @niaher für eindeutige Indexanmerkungen verwendet habe.

Kryptos
quelle
1
In diesem Beispiel haben Sie sowohl Company als auch CompanyId. Dies bedeutet, dass der Anrufer das eine, aber nicht das andere ändern kann und eine Entität mit falschen Daten hat.
LosManos
1
@LosManos Über welchen Anrufer sprichst du? Die Klasse repräsentiert Daten in einer Datenbank. Durch Ändern des Werts durch Abfragen wird die Konsistenz sichergestellt. Abhängig davon, was die Clientanwendung tun kann, müssen Sie möglicherweise Überprüfungen implementieren, aber das ist nicht der Umfang des OP.
Kryptos
4

In der akzeptierten Antwort von @chuck gibt es einen Kommentar, der besagt, dass es im Fall von FK nicht funktioniert.

es hat bei mir funktioniert, bei 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
quelle
Lange Zeit. Nehmen wir an, es war schon lange Arbeit! Vielen Dank für das Update. Bitte fügen Sie einen Kommentar zur Antwort von @chuck hinzu. Ich denke Chuck ist schon lange nicht mehr SO.
Bassam Alugili
Benötigt die Eigenschaft EmployeeID Here ein Attribut, um ihre Länge für die Indizierung zu begrenzen? Sonst wird es mit VARCHAR (MAX) erstellt, das keinen Index haben kann? Fügen Sie das Attribut [StringLength (255)] zu EmployeeID
Lord Darth Vader
EmployeeID ist eine GUID. Viele Tutorials schlagen vor, die GUID anstelle von
guid einem
3

Ich gehe davon aus, dass Sie immer EntityIdder Primärschlüssel sein möchten , daher ist das Ersetzen durch einen zusammengesetzten Schlüssel keine Option (schon allein deshalb, weil die Arbeit mit zusammengesetzten Schlüsseln weitaus komplizierter ist und es nicht sehr sinnvoll ist, Primärschlüssel zu haben, die auch Bedeutung haben in der Geschäftslogik).

Das Mindeste, was Sie tun sollten, ist, einen eindeutigen Schlüssel für beide Felder in der Datenbank zu erstellen und beim Speichern von Änderungen speziell nach Ausnahmen für eindeutige Schlüsselverletzungen zu suchen.

Außerdem können (sollten) Sie vor dem Speichern von Änderungen nach eindeutigen Werten suchen. Der beste Weg, dies zu tun, ist eine Any()Abfrage, da dadurch die Menge der übertragenen Daten minimiert wird:

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

Beachten Sie, dass diese Überprüfung allein niemals ausreicht. Zwischen der Prüfung und dem tatsächlichen Festschreiben besteht immer eine gewisse Latenz, sodass Sie immer die eindeutige Einschränkung + Ausnahmebehandlung benötigen.

Gert Arnold
quelle
3
Vielen Dank an @GertArnold für die Antwort, aber ich möchte die Eindeutigkeit auf der Business-Ebene nicht überprüfen. Dies ist ein Datenbankjob und dies soll in der Datenbank erfolgen!
Bassam Alugili
2
OK, bleiben Sie dann beim eindeutigen Index. Aber Sie müssen sich trotzdem mit Indexverletzungen in der Business-Schicht auseinandersetzen.
Gert Arnold
1
Wenn ich diese Art von Ausnahme von außen erhalte, werde ich den Fehler abfangen und dann möglicherweise melden und den Prozess unterbrechen oder die Anwendung herunterfahren.
Bassam Alugili
3
Ja, ... muss ich darauf antworten? Denken Sie daran, ich weiß nichts von Ihrer Bewerbung. Ich kann Ihnen nicht sagen, wie Sie mit diesen Ausnahmen am besten umgehen können, nur, dass Sie sich mit ihnen befassen müssen.
Gert Arnold
2
Seien Sie vorsichtig mit DB-Einschränkungen bei EF. Wenn Sie dies tun und dann ein Update erhalten, das die Werte einer der Spalten, die Teil des eindeutigen Schlüssels sind, umdreht, schlägt Entity Frameowkr beim Speichern fehl, es sei denn, Sie fügen eine ganze Transaktionsschicht hinzu. Beispiel: Das Seitenobjekt verfügt über eine untergeordnete Sammlung von Elementen. Jedes Element hat SortOrder. Sie möchten, dass die Kombination aus PageID und SortOrder eindeutig ist. Im Front-End schlägt der Benutzer die Reihenfolge der Elemente mit der Sortierreihenfolge 1 und 2 um. Entity Framework schlägt beim Speichern fehl, da es versucht, die Sortierreihenfolge einzeln zu aktualisieren.
EGP
1

Kürzlich wurde ein zusammengesetzter Schlüssel mit der Eindeutigkeit von 2 Spalten hinzugefügt, wobei der von 'chuck' empfohlene Ansatz verwendet wurde, danke @chuck. Nur diese Annäherung sah für mich sauberer aus:

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; }
Schuh Hasan
quelle