Kendala kunci unik untuk beberapa kolom di Entity Framework

244

Saya menggunakan Entity Framework 5.0 Code First;

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

Saya ingin membuat kombinasi antara FirstColumndan SecondColumnunik.

Contoh:

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

Apakah ada cara untuk melakukan itu?

Bassam Alugili
sumber

Jawaban:

370

Dengan Entity Framework 6.1, Anda sekarang dapat melakukan ini:

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

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

Parameter kedua dalam atribut adalah di mana Anda dapat menentukan urutan kolom dalam indeks.
Informasi lebih lanjut: MSDN

charlie
sumber
12
Ini benar untuk anotasi data :), jika Anda ingin jawaban untuk menggunakan API yang lancar lihat jawaban Niaher di bawah ini stackoverflow.com/a/25779348/2362036
puzzleegirl
8
Tetapi saya membutuhkannya bekerja untuk kunci asing! Bisakah kamu membantuku?
feedc0de
2
@ 0xFEEDC0DE lihat jawaban saya di bawah ini yang membahas penggunaan kunci asing dalam indeks.
Kryptos
1
Bisakah Anda memposting cara menggunakan indeks ini dengan linq ke sql?
Bluebaron
4
@JJS - Saya membuatnya bekerja di mana salah satu properti adalah kunci asing .. kebetulan kunci Anda adalah varchar atau nvarchar? Ada batas panjang yang dapat digunakan sebagai kunci unik .. stackoverflow.com/questions/2863993/…
Dave Lawrence
129

Saya menemukan tiga cara untuk menyelesaikan masalah.

Indeks unik di EntityFramework Core:

Pendekatan pertama:

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

Pendekatan kedua untuk membuat Batasan Unik dengan EF Core dengan menggunakan Tombol Alternatif.

Contohnya

Satu kolom:

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

Beberapa kolom:

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

EF 6 dan di bawah ini:


Pendekatan pertama:

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

Pendekatan ini sangat cepat dan bermanfaat tetapi masalah utamanya adalah Kerangka Entity tidak tahu apa-apa tentang perubahan itu!


Pendekatan kedua:
Saya menemukannya di posting ini tetapi saya tidak mencoba sendiri.

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

Masalah dari pendekatan ini adalah sebagai berikut: Perlu DbMigration jadi apa yang Anda lakukan jika Anda tidak memilikinya?


Pendekatan ketiga:
Saya pikir ini yang terbaik tetapi membutuhkan waktu untuk melakukannya. Saya hanya akan menunjukkan kepada Anda ide di baliknya: Dalam tautan ini http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a Anda dapat menemukan kode untuk anotasi data kunci unik:

[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

Masalah untuk pendekatan ini; Bagaimana saya bisa menggabungkannya? Saya punya ide untuk memperpanjang implementasi Microsoft ini misalnya:

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

Kemudian di IDatabaseInitializer seperti yang dijelaskan dalam contoh Microsoft Anda dapat menggabungkan kunci sesuai dengan bilangan bulat yang diberikan. Satu hal yang harus diperhatikan: Jika properti unik adalah tipe string maka Anda harus mengatur MaxLength.

Bassam Alugili
sumber
1
(y) Saya menemukan jawaban ini lebih baik. Hal lain, pendekatan ketiga mungkin belum tentu yang terbaik. (Sebenarnya saya suka yang pertama.) Saya pribadi lebih suka tidak membawa artefak EF ke dalam kelas entitas saya.
Najeeb
1
Mungkin, pendekatan kedua harus: CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2})? (lihat BOL )
AK
2
Pertanyaan bodoh: Kenapa begitu Anda memulai semua nama Anda dengan "IX_"?
Bastien Vandamme
1
@BastienVandamme itu pertanyaan yang bagus. indeks hasil otomatis oleh EF dimulai dengan IX_. Tampaknya menjadi konvensi dalam indeks EF secara default, nama indeksnya adalah IX_ {nama properti}.
Bassam Alugili
1
Ya seharusnya begitu. Terima kasih untuk penerapan API Lancar. Ada kekurangan dokumentasi yang serius tentang hal ini
JSON
75

Jika Anda menggunakan Code-First , Anda dapat menerapkan ekstensi kustom 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);
    }
}

Kemudian gunakan seperti ini:

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

Yang akan menghasilkan migrasi ini:

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

Dan akhirnya berakhir di database sebagai:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
sumber
3
Tapi itu indeks bukan kendala!
Roman Pokrovskij
3
Di blok kode kedua Anda ( this.Property(t => t.Email)), apa yang berisi kelas? (ie: what is this)
JoeBrockhaus
2
nvm. EntityTypeConfiguration<T>
JoeBrockhaus
5
@RomanPokrovskij: Perbedaan antara indeks unik dan batasan unik tampaknya menjadi masalah bagaimana catatan itu dikelola dalam SQL Server. Lihat technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx untuk detailnya.
Mass Dot Net
1
@niaher Saya menghargai metode ekstensi Anda yang bagus
Mohsen Afshin
18

Anda perlu mendefinisikan kunci komposit.

Dengan anotasi data, tampilannya seperti ini:

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

Anda juga dapat melakukan ini dengan modelBuilder saat meng-override OnModelCreating dengan menetapkan:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Admir Tuzović
sumber
2
Tapi bukan kunci saya hanya ingin mereka sebagai unik kuncinya harus id? Saya telah memperbarui kuenya, terima kasih atas bantuannya!
Bassam Alugili
16

Jawaban dari niaher yang menyatakan bahwa untuk menggunakan API yang lancar Anda memerlukan ekstensi khusus mungkin sudah benar pada saat penulisan. Anda sekarang dapat (EF core 2.1) menggunakan API yang lancar sebagai berikut:

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

Menyelesaikan jawaban chuck untuk menggunakan indeks komposit dengan kunci asing .

Anda perlu mendefinisikan properti yang akan menyimpan nilai kunci asing. Anda kemudian dapat menggunakan properti ini di dalam definisi indeks.

Misalnya, kami memiliki perusahaan dengan karyawan dan hanya kami yang memiliki batasan unik pada (nama, perusahaan) untuk setiap karyawan:

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

Sekarang pemetaan kelas Karyawan:

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

Perhatikan bahwa saya juga menggunakan ekstensi @niaher untuk anotasi indeks yang unik.

Kripto
sumber
1
Dalam contoh ini Anda memiliki CompanyId dan CompanyId. Ini berarti pemanggil dapat mengubah satu tetapi tidak yang lain dan memiliki entitas dengan data yang salah.
LosManos
1
@ Losos Penelepon mana yang sedang Anda bicarakan? Kelas mewakili data dalam database. Mengubah nilai melalui kueri akan memastikan konsistensi. Bergantung pada apa yang dapat dilakukan aplikasi klien, Anda mungkin perlu melakukan pemeriksaan, tetapi itu bukan cakupan OP.
Kryptos
4

Dalam jawaban yang diterima oleh @ chuck, ada komentar yang mengatakan itu tidak akan berfungsi dalam kasus FK.

itu bekerja untuk saya, kasus 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
sumber
Lama. katakanlah itu sudah bekerja sebelum waktu yang lama! terima kasih atas pembaruannya, silakan tambahkan komentar ke jawaban @chuck. Saya pikir Chuck sebelum lama tidak menggunakan SO.
Bassam Alugili
Apakah properti EmployeeID Di sini perlu atribut untuk membatasi panjangnya agar diindeks? Atau dibuat dengan VARCHAR (MAX) yang tidak dapat memiliki indeks? Tambahkan Atribut [StringLength (255)] ke EmployeeID
Lord Darth Vader
EmployeeID adalah GUID. Banyak tutorial menyarankan untuk memetakan GUID ke string alih-alih panduan, saya tidak tahu mengapa
dalios
3

Saya berasumsi bahwa Anda selalu ingin EntityIdmenjadi kunci utama, jadi menggantinya dengan kunci komposit bukan merupakan pilihan (jika saja karena kunci komposit jauh lebih rumit untuk digunakan dan karena sangat tidak masuk akal untuk memiliki kunci primer yang juga memiliki makna dalam logika bisnis).

Paling tidak yang harus Anda lakukan adalah membuat kunci unik di kedua bidang dalam database dan secara khusus memeriksa pengecualian pelanggaran kunci unik saat menyimpan perubahan.

Selain itu Anda dapat (harus) memeriksa nilai-nilai unik sebelum menyimpan perubahan. Cara terbaik untuk melakukannya adalah dengan Any()kueri, karena meminimalkan jumlah data yang ditransfer:

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

Berhati-hatilah karena pemeriksaan ini saja tidak pernah cukup. Selalu ada beberapa latensi antara cek dan komit aktual, sehingga Anda akan selalu memerlukan penanganan kendala + pengecualian yang unik.

Gert Arnold
sumber
3
Terima kasih @GertArnold untuk jawabannya tetapi saya tidak ingin memvalidasi keunikan pada lapisan bisnis ini adalah pekerjaan basis data dan ini harus dilakukan dalam basis data!
Bassam Alugili
2
OK, patuhi indeks unik itu. Tetapi Anda harus berurusan dengan pelanggaran indeks di lapisan bisnis.
Gert Arnold
1
dari luar ketika saya menerima pengecualian semacam ini saya akan menangkap dan daripada mungkin melaporkan kesalahan dan menghancurkan proses atau mematikan aplikasi.
Bassam Alugili
3
Ya, ... apakah saya perlu menanggapinya? Ingat saya tahu apa-apa tentang aplikasi Anda, saya tidak bisa mengatakan apa cara terbaik adalah untuk menangani pengecualian tersebut, hanya yang Anda harus berurusan dengan mereka.
Gert Arnold
2
Waspadai batasan unik DB dengan EF. Jika Anda melakukan ini dan kemudian Anda memiliki pembaruan yang membalik nilai-nilai dari salah satu kolom yang merupakan bagian dari kunci unik, entitas frameowkr akan gagal pada save kecuali Anda menambahkan seluruh lapisan transaksi. Misalnya: Objek halaman memiliki koleksi anak-anak Elemen. Setiap elemen memiliki SortOrder. Anda ingin kombo PageID dan SortOrder unik. Di ujung depan, pengguna flip flop urutan elemen dengan sortorder 1 dan 2. Entity Framework akan gagal menyimpan b / c itu mencoba memperbarui sortorders satu per satu.
EGP
1

Baru-baru ini menambahkan kunci komposit dengan keunikan 2 kolom menggunakan pendekatan yang 'direkomendasikan', terima kasih @chuck. Hanya ini yang didekati tampak lebih bersih bagi saya:

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
sumber