Ограничения уникального ключа для нескольких столбцов в Entity Framework

244

Я использую 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;}
 }

Я хочу сделать сочетание между FirstColumnи SecondColumnкак уникальным.

Пример:

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

Есть ли способ сделать это?

Бассам Алугили
источник

Ответы:

370

С Entity Framework 6.1 теперь вы можете сделать это:

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

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

Второй параметр в атрибуте - это место, где вы можете указать порядок столбцов в индексе.
Дополнительная информация: MSDN

Чарли
источник
12
Это верно для аннотаций данных :), если вы хотите получить ответ об использовании свободного API, см. Ответ Niaher ниже stackoverflow.com/a/25779348/2362036
tekiegirl
8
Но мне нужно, чтобы он работал для внешних ключей! Вы можете помочь мне?
feedc0de
2
@ 0xFEEDC0DE, смотрите мой ответ ниже, посвященный использованию внешних ключей в индексах.
Криптос
1
Можете ли вы опубликовать, как использовать этот индекс с linq to sql?
Bluebaron
4
@JJS - Я заставил его работать, где одно из свойств было внешним ключом .. случайно ли ваш ключ был varchar или nvarchar? Существует ограничение на длину, которую можно использовать в качестве уникального ключа. Stackoverflow.com/questions/2863993/…
Дейв Лоуренс
129

Я нашел три способа решения проблемы.

Уникальные индексы в EntityFramework Core:

Первый подход:

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

Второй подход к созданию уникальных ограничений с помощью EF Core с использованием альтернативных ключей.

Примеры

Один столбец:

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

Несколько столбцов:

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

EF 6 и ниже:


Первый подход:

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

Этот подход очень быстрый и полезный, но главная проблема в том, что Entity Framework ничего не знает об этих изменениях!


Второй подход:
я нашел это в этом посте, но сам не пробовал.

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

Проблема этого подхода заключается в следующем: он нуждается в DbMigration, что вы будете делать, если у вас его нет?


Третий подход:
я думаю, что это лучший, но для этого нужно время. Я просто покажу вам идею, лежащую в основе этого: в этой ссылке http://code.msdn.microsoft.com/CSASPNETUniqueConstraintInE-d357224a вы можете найти код для аннотации уникальных ключевых данных:

[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

Проблема для этого подхода; Как я могу их объединить? У меня есть идея расширить эту реализацию Microsoft, например:

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

Позже в IDatabaseInitializer, как описано в примере Microsoft, вы можете комбинировать ключи в соответствии с заданным целым числом. Однако следует отметить одну вещь: если уникальное свойство имеет тип string, то вы должны установить MaxLength.

Бассам Алугили
источник
1
(у) я нахожу этот ответ лучше. Другое дело, что третий подход не обязательно может быть лучшим. (Мне действительно нравится первый.) Лично я предпочитаю, чтобы в моих классах сущностей не было никаких артефактов EF.
Наджиб
1
Возможно, второй подход должен быть: CREATE UNIQUE INDEX ix_{1}_{2} ON {0} ({1}, {2})? (см. BOL )
AK
2
Глупый вопрос: почему так вы начинаете все свое имя с "IX_"?
Бастьен Вандамм
1
@BastienVandamme это хороший вопрос. автогенерация индекса EF начинается с IX_. Похоже, по умолчанию в индексе EF существует соглашение, имя индекса будет IX_ {имя свойства}.
Бассам Алугили
1
Да, так и должно быть. Спасибо за свободную реализацию API. Существует серьезная нехватка документации по этому вопросу
JSON
75

Если вы используете Code-First , вы можете реализовать собственное расширение 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);
    }
}

Тогда используйте это так:

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

Что приведет к этой миграции:

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

И в конечном итоге в базе данных, как:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_User_EmailPerApplication] ON [dbo].[User]
(
    [Email] ASC,
    [ApplicationId] ASC
)
niaher
источник
3
Но это индекс, а не ограничение!
Роман Покровский
3
Во втором блоке кода ( this.Property(t => t.Email)), что это за класс? (то есть: что это this)
JoeBrockhaus
2
NVM. EntityTypeConfiguration<T>
JoeBrockhaus
5
@RomanPokrovskij: Разница между уникальным индексом и уникальным ограничением заключается в том, как его записи хранятся в SQL Server. Подробнее см. Technet.microsoft.com/en-us/library/aa224827%28v=sql.80%29.aspx .
Mass Dot Net
1
@niaher Я ценю ваш хороший метод расширения
Мохсен Афшин
18

Вам нужно определить составной ключ.

С аннотациями данных это выглядит так:

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

Вы также можете сделать это с modelBuilder при переопределении OnModelCreating, указав:

modelBuilder.Entity<Entity>().HasKey(x => new { x.FirstColumn, x.SecondColumn });
Адмир Тузович
источник
2
Но разве это не ключи, я просто хочу их как уникальные, ключ должен быть идентификатором? Я обновил вопрос, спасибо за помощь!
Бассам Алугили
16

Ответ от niaher о том, что для использования свободного API вам нужно собственное расширение, может быть правильным на момент написания. Теперь вы можете (EF core 2.1) использовать свободный API следующим образом:

modelBuilder.Entity<ClassName>()
            .HasIndex(a => new { a.Column1, a.Column2}).IsUnique();
GilShalit
источник
9

Завершение ответа @chuck для использования составных индексов с внешними ключами .

Вам необходимо определить свойство, которое будет содержать значение внешнего ключа. Затем вы можете использовать это свойство внутри определения индекса.

Например, у нас есть компания с сотрудниками, и только у нас есть уникальное ограничение (имя, компания) для любого сотрудника:

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

Теперь отображение класса 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);
    }
}

Обратите внимание, что я также использовал расширение @niaher для уникальной аннотации индекса.

Kryptos
источник
1
В этом примере у вас есть и Company, и CompanyId. Это означает, что вызывающая сторона может изменить одну, но не другую, и получить объект с неверными данными.
LosManos
1
@LosManos О каком абоненте вы говорите? Класс представляет данные в базе данных. Изменение значения с помощью запросов обеспечит согласованность. В зависимости от того, что может сделать клиентское приложение, вам может потребоваться реализовать проверки, но это не входит в сферу действия OP.
Криптос
4

В принятом ответе @chuck есть комментарий о том, что в случае с FK это не сработает.

у меня сработало, дело 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; }
}
Далиос
источник
Много времени. скажем так, это была работа в течение долгого времени! спасибо за обновление, пожалуйста, добавьте комментарий к ответу @chuck. Я думаю, что Чак в течение долгого времени не использует ТАК.
Бассам Алугили
Нужно ли свойству EmployeeID Here атрибут для ограничения его длины для индексации? Иначе он создан с помощью VARCHAR (MAX), который не может иметь индекс? Добавить атрибут [StringLength (255)] для EmployeeID
лорд Дарт Вейдер
EmployeeID - это GUID. Многие учебники предлагают сопоставить GUID со строкой вместо guid, я не знаю почему
dalios
3

Я предполагаю, что вы всегда хотите EntityIdбыть первичным ключом, поэтому замена его составным ключом не вариант (хотя бы потому, что с составными ключами гораздо сложнее работать и потому что не очень разумно иметь первичные ключи, которые также имеют значение в бизнес-логике).

Минимум, что вам нужно сделать, - это создать уникальный ключ в обоих полях базы данных и, в частности, проверить наличие исключений нарушения уникального ключа при сохранении изменений.

Кроме того, вы можете (должны) проверить уникальные значения перед сохранением изменений. Лучший способ сделать это с помощью Any()запроса, поскольку он минимизирует объем передаваемых данных:

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

Остерегайтесь, что одной этой проверки никогда не бывает достаточно. Между проверкой и фактической фиксацией всегда есть некоторая задержка, поэтому вам всегда потребуется уникальное ограничение + обработка исключений.

Герт Арнольд
источник
3
Спасибо @GertArnold за ответ, но я не хочу проверять уникальность на бизнес-уровне, это работа базы данных, и это должно быть сделано в базе данных!
Бассам Алугили
2
Хорошо, тогда придерживайтесь уникального индекса. Но вам все равно придется иметь дело с нарушениями индекса на бизнес-уровне.
Герт Арнольд
1
извне, когда я получу такого рода исключения, меня поймают, а затем, возможно, сообщат об ошибке и прервут процесс или завершат работу приложения.
Бассам Алугили
3
Да, ... мне нужно ответить на это? Помните, я ничего не знаю о вашем заявлении, я не могу сказать вам, как лучше всего справиться с этими исключениями, только то , что вы должны иметь дело с ними.
Герт Арнольд
2
Остерегайтесь уникальных ограничений БД с EF. Если вы сделаете это, а затем у вас будет обновление, которое изменяет значения одного из столбцов, являющихся частью уникального ключа, сущность frameowkr потерпит неудачу при сохранении, если вы не добавите весь слой транзакций. Например: объект Page имеет дочернюю коллекцию Elements. Каждый элемент имеет SortOrder. Вы хотите, чтобы комбинирование PageID и SortOrder было уникальным. В интерфейсе пользователя порядок элементов триггеров с сортировщиками 1 и 2 сбой. Entity Framework потерпит неудачу при сохранении, поскольку он пытается обновить сортировщики по одному.
EGP
1

Недавно добавили составной ключ с уникальностью 2 столбцов, используя подход, рекомендованный 'chuck', спасибо @chuck. Только этот подход выглядел чище для меня:

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; }
Шеб Хасан
источник