Unikalne kluczowe ograniczenia dla wielu kolumn w Entity Framework

243

Najpierw używam kodu 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;}
 }

Chcę zrobić kombinację między FirstColumn i SecondColumntak wyjątkowy.

Przykład:

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

Czy istnieje jakiś sposób, aby to zrobić?

Bassam Alugili
źródło

Odpowiedzi:

369

Dzięki Entity Framework 6.1 możesz teraz to zrobić:

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

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

Drugi parametr w atrybucie służy do określania kolejności kolumn w indeksie.
Więcej informacji: MSDN

Charlie
źródło
12
Jest to poprawne w przypadku adnotacji danych :), jeśli chcesz uzyskać odpowiedź na temat używania płynnego interfejsu API, zobacz odpowiedź Niaher poniżej stackoverflow.com/a/25779348/2362036
tekiegirl
8
Ale potrzebuję go do pracy z kluczami obcymi! Możesz mi pomóc?
feedc0de
2
@ 0xFEEDC0DE patrz moja odpowiedź poniżej, która dotyczy użycia kluczy obcych w indeksach.
Kryptos
1
Czy możesz napisać, jak używać tego indeksu z linq do sql?
Bluebaron,
4
@JJS - Mam go do pracy, gdzie jedną z właściwości był klucz obcy .. czy to możliwe, że twój klucz jest varchar lub nvarchar? Istnieje limit długości, którego można użyć jako unikalnego klucza .. stackoverflow.com/questions/2863993/…
Dave Lawrence
129

Znalazłem trzy sposoby rozwiązania problemu.

Unikalne indeksy w EntityFramework Core:

Pierwsze podejście:

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

Drugie podejście do tworzenia unikalnych ograniczeń za pomocą EF Core za pomocą alternatywnych kluczy.

Przykłady

Jedna kolumna:

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

Wiele kolumn:

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

EF 6 i poniżej:


Pierwsze podejście:

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

To podejście jest bardzo szybkie i przydatne, ale głównym problemem jest to, że Entity Framework nie wie nic o tych zmianach!


Drugie podejście:
znalazłem to w tym poście, ale nie próbowałem sam.

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

Problem tego podejścia jest następujący: Wymaga DbMigration, więc co robisz, jeśli go nie masz?


Trzecie podejście:
myślę, że jest to najlepsze, ale zajmuje to trochę czasu. Pokażę ci po prostu pomysł: w tym linku http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a można znaleźć kod unikalnej adnotacji danych kluczowych:

[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

Problem tego podejścia; Jak mogę je połączyć? Mam pomysł na rozszerzenie tej implementacji Microsoft na przykład:

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

Później w IDatabaseInitializer, jak opisano w przykładzie Microsoft, możesz łączyć klucze zgodnie z podaną liczbą całkowitą. Należy jednak zwrócić uwagę na jedno: jeśli unikalna właściwość jest typu string, musisz ustawić MaxLength.

Bassam Alugili
źródło
1
(y) Uważam, że ta odpowiedź jest lepsza. Inna sprawa, trzecie podejście niekoniecznie musi być najlepsze. (Właściwie podoba mi się ten pierwszy.) Osobiście wolę, aby żadne artefakty EF nie były przenoszone do moich klas jednostek.
Najeeb
1
Być może drugim podejściem powinno być CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2}):? (patrz BOL )
AK
2
Głupie pytanie: dlaczego więc zaczynasz całe swoje imię od „IX_”?
Bastien Vandamme
1
@BastienVandamme to dobre pytanie. indeks automatycznego generowania według EF zaczyna się od IX_. Domyślnie jest to konwencja w indeksie EF, nazwa indeksu to IX_ {nazwa właściwości}.
Bassam Alugili
1
Tak powinno być. Dziękujemy za wdrożenie Fluent API. Poważny brak dokumentacji na ten temat
JSON
75

Jeśli używasz Code-First , możesz zaimplementować niestandardowe rozszerzenie 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);
    }
}

Następnie użyj go w następujący sposób:

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

Co spowoduje migrację:

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

I ostatecznie trafiają do bazy danych jako:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
źródło
3
Ale to indeks nie jest ograniczeniem!
Roman Pokrovskij
3
Co zawiera Twój drugi blok kodu ( this.Property(t => t.Email))? (tj. co to jest this)
JoeBrockhaus,
2
NVM. EntityTypeConfiguration<T>
JoeBrockhaus,
5
@RomanPokrovskij: Różnica między unikalnym indeksem a unikalnym ograniczeniem wydaje się być kwestią sposobu, w jaki zapisuje się go w SQL Server. Aby uzyskać szczegółowe informacje, zobacz technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx .
Mass Dot Net
1
@niaher Doceniam twoją miłą metodę rozszerzenia
Mohsen Afshin
18

Musisz zdefiniować klucz złożony.

Z adnotacjami danych wygląda to tak:

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

Można to również zrobić za pomocą modeluBuilder podczas nadpisywania OnModelCreating, określając:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Admir Tuzović
źródło
2
Ale czy nie są to klucze, chcę tylko, aby były unikalne, czy klucz powinien być identyfikatorem? Zaktualizowałem pytanie dzięki za pomoc!
Bassam Alugili,
16

Odpowiedź od niaher stwierdzająca, że ​​do korzystania z płynnego interfejsu API potrzebujesz niestandardowego rozszerzenia, mogła być poprawna w momencie pisania. Możesz teraz (EF core 2.1) korzystać z płynnego API w następujący sposób:

modelBuilder.Entity<ClassName>()
            .HasIndex(a => new { a.Column1, a.Column2}).IsUnique();
GilShalit
źródło
9

Wypełnianie odpowiedzi @chuck za używanie indeksów złożonych z kluczami obcymi .

Musisz zdefiniować właściwość, która będzie przechowywać wartość klucza obcego. Następnie możesz użyć tej właściwości w definicji indeksu.

Na przykład mamy firmę z pracownikami i tylko mamy wyjątkowe ograniczenie (nazwa, firma) dla każdego pracownika:

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

Teraz mapowanie klasy 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);
    }
}

Zauważ, że użyłem rozszerzenia @niaher do unikalnej adnotacji indeksu.

Kryptos
źródło
1
W tym przykładzie masz zarówno Company jak i CompanyId. Oznacza to, że dzwoniący może zmieniać jedno, ale nie drugie, i mieć podmiot z niepoprawnymi danymi.
LosManos,
1
@LosManos O którym rozmówcy mówisz? Klasa reprezentuje dane w bazie danych. Zmiana wartości poprzez zapytania zapewni spójność. W zależności od tego, co może zrobić aplikacja kliencka, może być konieczne wdrożenie kontroli, ale nie jest to zakres PO.
Kryptos
4

W zaakceptowanej odpowiedzi @chuck znajduje się komentarz mówiący, że nie zadziała w przypadku FK.

zadziałało dla mnie, sprawa 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
źródło
Długi czas. powiedzmy, że to działało już dawno! dziękuję za aktualizację, dodaj komentarz do odpowiedzi @chuck. Myślę, że Chuck już dawno nie używa SO.
Bassam Alugili,
Czy właściwość EmployeeID tutaj potrzebuje atrybutu, aby ograniczyć jej długość do indeksowania? Czy utworzono go za pomocą VARCHAR (MAX), który nie może mieć indeksu? Dodaj atrybut [StringLength (255)] do Identyfikator pracownika
Lord Darth Vader
Identyfikator pracownika to identyfikator GUID. Wiele samouczków sugeruje mapowanie GUID na ciąg zamiast GUID, nie wiem dlaczego
dalios
3

Zakładam, że zawsze chcesz EntityIdbyć kluczem podstawowym, więc zastąpienie go kluczem złożonym nie jest opcją (choćby dlatego, że klucze złożone są znacznie bardziej skomplikowane w pracy i ponieważ nie jest bardzo sensowne mieć klucze podstawowe, które również mają znaczenie w logice biznesowej).

Co najmniej powinieneś zrobić, to utworzyć unikalny klucz dla obu pól w bazie danych i szczególnie sprawdzić wyjątki naruszenia wyjątków podczas zapisywania zmian.

Dodatkowo możesz (powinieneś) sprawdzić unikalne wartości przed zapisaniem zmian. Najlepszym sposobem na to jest Any()zapytanie, ponieważ minimalizuje ilość przesyłanych danych:

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

Uważaj, że sama kontrola nigdy nie wystarczy. Zawsze występuje pewne opóźnienie między sprawdzeniem a rzeczywistym zatwierdzeniem, więc zawsze będziesz potrzebować unikalnego ograniczenia + obsługi wyjątków.

Gert Arnold
źródło
3
Dzięki @GertArnold za odpowiedź, ale nie chcę sprawdzać wyjątkowości na warstwie biznesowej, to jest zadanie bazy danych i należy to zrobić w bazie danych!
Bassam Alugili,
2
OK, trzymaj się wtedy unikalnego indeksu. Ale i tak będziesz musiał poradzić sobie z naruszeniami indeksu w warstwie biznesowej.
Gert Arnold,
1
z zewnątrz, kiedy otrzymam tego rodzaju wyjątek, wyłapię i być może zgłoszenie błędu i przerwę proces lub zamknięcie aplikacji.
Bassam Alugili,
3
Tak ... czy muszę na to odpowiedzieć? Pamiętaj, że nic nie wiem o Twojej aplikacji, nie mogę powiedzieć, jaki jest najlepszy sposób radzenia sobie z tymi wyjątkami, tylko że musisz sobie z nimi poradzić.
Gert Arnold,
2
Uważaj na unikalne ograniczenia DB dzięki EF. Jeśli to zrobisz, a następnie zakończy się aktualizacja z przerzucaniem wartości jednej z kolumn, która jest częścią unikalnego klucza, encja frameowkr zakończy się niepowodzeniem podczas zapisywania, chyba że dodasz całą warstwę transakcji. Na przykład: Obiekt strony ma podrzędną kolekcję elementów. Każdy element ma SortOrder. Chcesz, aby kombinacja PageID i SortOrder była unikalna. W interfejsie użytkownika kolejność przerzucania elementów w sortowniku 1 i 2. Entity Framework zakończy się niepowodzeniem podczas zapisu b / c, który próbuje aktualizować sortowniki pojedynczo.
EGP
1

Ostatnio dodałem klucz złożony z wyjątkowością 2 kolumn, stosując podejście zalecane przez „chuck”, dziękuję @ chuck. Tylko to podejście wydawało mi się czystsze:

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
źródło