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ć?

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


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.


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
)

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

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

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.


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

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.


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; }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.