Уникальное ограничение в Entity Framework Code First

125

Вопрос

Можно ли определить уникальное ограничение для свойства, используя свободный синтаксис или атрибут? Если нет, каковы обходные пути?

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

Решение (на основе ответа Мэтта)

public class MyContext : DbContext {
    public DbSet<User> Users { get; set; }

    public override int SaveChanges() {
        foreach (var item in ChangeTracker.Entries<IModel>())
            item.Entity.Modified = DateTime.Now;

        return base.SaveChanges();
    }

    public class Initializer : IDatabaseInitializer<MyContext> {
        public void InitializeDatabase(MyContext context) {
            if (context.Database.Exists() && !context.Database.CompatibleWithModel(false))
                context.Database.Delete();

            if (!context.Database.Exists()) {
                context.Database.Create();
                context.Database.ExecuteSqlCommand("alter table Users add constraint UniqueUserEmail unique (Email)");
            }
        }
    }
}
kim3er
источник
1
Помните, что это ограничивает ваше приложение только базами данных, которые принимают этот точный синтаксис - в данном случае SQL Server. Если вы запустите свое приложение с поставщиком Oracle, оно завершится ошибкой.
DamienG
1
В этой ситуации мне нужно было бы только создать новый класс Initializer, но это верный момент.
kim3er
3
Ознакомьтесь с этим сообщением: ValidationAttribute, который проверяет уникальное поле по сравнению с другими строками в базе данных , решение предназначено либо, ObjectContextлибо DbContext.
Шимми Вайцхандлер
Да, теперь он поддерживается с EF 6.1 .
Evandro Pomatti 01

Ответы:

61

Насколько я могу судить, на данный момент нет возможности сделать это с помощью Entity Framework. Однако это проблема не только с уникальными ограничениями ... вы можете также создавать индексы, проверять ограничения и, возможно, триггеры и другие конструкции. Вот простой шаблон, который вы можете использовать с настройкой сначала кода, хотя, по общему признанию, он не зависит от базы данных:

public class MyRepository : DbContext {
    public DbSet<Whatever> Whatevers { get; set; }

    public class Initializer : IDatabaseInitializer<MyRepository> {
        public void InitializeDatabase(MyRepository context) {
            if (!context.Database.Exists() || !context.Database.ModelMatchesDatabase()) {
                context.Database.DeleteIfExists();
                context.Database.Create();

                context.ObjectContext.ExecuteStoreCommand("CREATE UNIQUE CONSTRAINT...");
                context.ObjectContext.ExecuteStoreCommand("CREATE INDEX...");
                context.ObjectContext.ExecuteStoreCommand("ETC...");
            }
        }
    }
}

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

mattmc3
источник
Мне нравится, чтобы моя БД была жесткой как барабан, логика тиражируется на бизнес-уровне. Ваш ответ работает только с CTP4, но я на правильном пути, я предоставил решение, совместимое с CTP5, ниже моего исходного вопроса. Большое спасибо!
kim3er
23
Если ваше приложение не является однопользовательским, я считаю, что уникальное ограничение - это то, что вы не можете реализовать только с помощью кода. Вы можете значительно снизить вероятность нарушения в коде (проверяя уникальность перед вызовом SaveChanges()), но все же существует вероятность проскальзывания еще одной вставки / обновления между временем проверки уникальности и временем SaveChanges(). Итак, в зависимости от того, насколько критично приложение и вероятность нарушения уникальности, вероятно, лучше всего добавить ограничение в базу данных.
devuxer
1
Вам нужно, чтобы ваш чек на уникальность был частью той же транзакции, что и ваши SaveChanges. Предполагая, что ваша база данных совместима с кислотами, вы должны иметь возможность таким образом обеспечить уникальность. Другой вопрос, позволяет ли EF правильно управлять жизненным циклом транзакции таким образом.
mattmc3
1
@ mattmc3 Это зависит от уровня изоляции вашей транзакции. Только serializable isolation level(или настраиваемая блокировка таблицы, тьфу) на самом деле позволит вам гарантировать уникальность вашего кода. Но большинство людей не используют его serializable isolation levelиз соображений производительности. По умолчанию в MS Sql Server установлено read committed. Смотрите серию из 4 частей, начиная с: michaeljswart.com/2010/03/…
Nathan
3
EntityFramework 6.1.0 теперь поддерживает IndexAttribute, который вы можете добавить поверх свойств.
сотн
45

Начиная с EF 6.1 теперь возможно:

[Index(IsUnique = true)]
public string EmailAddress { get; set; }

Это даст вам уникальный индекс вместо уникального ограничения, строго говоря. Для большинства практических целей они одинаковы .

Михкель Мююр
источник
5
@Dave: просто используйте одно и то же имя индекса для атрибутов соответствующих свойств ( источника ).
Mihkel Müür
Обратите внимание , это создает уникальный индекс , а не уникальный contraint . Хотя они почти одинаковы, они не совсем такие же (насколько я понимаю, уникальные ограничения могут использоваться в качестве цели FK). Для ограничения вам нужно выполнить SQL.
Ричард
(После последнего комментария) Другие источники предполагают, что это ограничение было снято в более поздних версиях SQL Server ... но BOL не полностью согласован.
Ричард
@Richard: также возможны уникальные ограничения на основе атрибутов (см. Мой второй ответ ), но не из коробки.
Mihkel Müür
1
@exSnake: Начиная с SQL Server 2008, уникальный индекс по умолчанию поддерживает одно значение NULL для каждого столбца. Если требуется поддержка нескольких значений NULL, потребуется отфильтрованный индекс, см. Другой вопрос .
Mihkel Müür 02
28

На самом деле это не связано с этим, но в некоторых случаях может помочь.

Если вы хотите создать уникальный составной индекс, скажем, для двух столбцов, которые будут действовать как ограничение для вашей таблицы, то начиная с версии 4.3 вы можете использовать новый механизм миграции для этого:

Обычно вам нужно вставить такой вызов в один из сценариев миграции:

CreateIndex("TableName", new string[2] { "Column1", "Column2" }, true, "IX_UniqueColumn1AndColumn2");

Что-то такое:

namespace Sample.Migrations
{
    using System;
    using System.Data.Entity.Migrations;

    public partial class TableName_SetUniqueCompositeIndex : DbMigration
    {
        public override void Up()
        {
            CreateIndex("TableName", new[] { "Column1", "Column2" }, true, "IX_UniqueColumn1AndColumn2");
        }

        public override void Down()
        {
            DropIndex("TableName", new[] { "Column1", "Column2" });
        }
    }
}
lnaie
источник
Приятно видеть, что в EF есть миграции в стиле Rails. Вот если бы я только мог запустить его на Mono.
kim3er
2
Разве у вас не должно быть DropIndex в процедуре Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Майкл Бисбьерг
5

Я делаю полный взлом, чтобы SQL выполнялся при создании базы данных. Я создаю свой собственный DatabaseInitializer и наследую от одного из предоставленных инициализаторов.

public class MyDatabaseInitializer : RecreateDatabaseIfModelChanges<MyDbContext>
{
    protected override void Seed(MyDbContext context)
    {
        base.Seed(context);
        context.Database.Connection.StateChange += new StateChangeEventHandler(Connection_StateChange);
    }

    void Connection_StateChange(object sender, StateChangeEventArgs e)
    {
        DbConnection cnn = sender as DbConnection;

        if (e.CurrentState == ConnectionState.Open)
        {
            // execute SQL to create indexes and such
        }

        cnn.StateChange -= Connection_StateChange;
    }
}

Это единственное место, где я мог бы вклиниться в свои операторы SQL.

Это из CTP4. Не знаю, как это работает в CTP5.

Келли Этридж
источник
Спасибо, Келли! Я не знал об этом обработчике событий. Мое возможное решение помещает SQL в метод InitializeDatabase.
kim3er
5

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

    [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=false,Inherited=true)]
public class UniqueAttribute:System.Attribute
{
    private string[] _atts;
    public string[] KeyFields
    {
        get
        {
            return _atts;
        }
    }
    public UniqueAttribute(string keyFields)
    {
        this._atts = keyFields.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);
    }
}

Затем в своем классе я добавлю это:

[CustomAttributes.Unique("Name")]
public class Item: BasePOCO
{
    public string Name{get;set;}
    [StringLength(250)]
    public string Description { get; set; }
    [Required]
    public String Category { get; set; }
    [Required]
    public string UOM { get; set; }
    [Required]
}

Наконец, я добавлю метод в свой репозиторий, в метод Add или при сохранении изменений следующим образом:

private void ValidateDuplicatedKeys(T entity)
{
    var atts = typeof(T).GetCustomAttributes(typeof(UniqueAttribute), true);
    if (atts == null || atts.Count() < 1)
    {
        return;
    }
    foreach (var att in atts)
    {
        UniqueAttribute uniqueAtt = (UniqueAttribute)att;
        var newkeyValues = from pi in entity.GetType().GetProperties()
                            join k in uniqueAtt.KeyFields on pi.Name equals k
                            select new { KeyField = k, Value = pi.GetValue(entity, null).ToString() };
        foreach (var item in _objectSet)
        {
            var keyValues = from pi in item.GetType().GetProperties()
                            join k in uniqueAtt.KeyFields on pi.Name equals k
                            select new { KeyField = k, Value = pi.GetValue(item, null).ToString() };
            var exists = keyValues.SequenceEqual(newkeyValues);
            if (exists)
            {
                throw new System.Exception("Duplicated Entry found");
            }
        }
    }
}

Не слишком хорошо, так как нам нужно полагаться на размышления, но пока этот подход работает для меня! = D

Росендо
источник
5

Также в 6.1 вы можете использовать версию ответа @ mihkelmuur с свободным синтаксисом следующим образом:

Property(s => s.EmailAddress).HasColumnAnnotation(IndexAnnotation.AnnotationName,
new IndexAnnotation(
    new IndexAttribute("IX_UniqueEmail") { IsUnique = true }));

Беглый метод не идеален, ИМО, но, по крайней мере, теперь он возможен.

Больше подробностей в блоге Артура Виккерса http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

Не любил
источник
4

Простой способ в Visual Basic с использованием EF5 Code First Migrations

Образец публичного класса

    Public Property SampleId As Integer

    <Required>
    <MinLength(1),MaxLength(200)>

    Public Property Code() As String

Конец класса

Атрибут MaxLength очень важен для уникального индекса строкового типа.

Запустите cmd: update-database -verbose

после запуска cmd: add-migration 1

в сгенерированном файле

Public Partial Class _1
    Inherits DbMigration

    Public Overrides Sub Up()
        CreateIndex("dbo.Sample", "Code", unique:=True, name:="IX_Sample_Code")
    End Sub

    Public Overrides Sub Down()
        'DropIndex if you need it
    End Sub

End Class
деспота
источник
На самом деле это более подходящий ответ, чем настраиваемый инициализатор БД.
Шон Уилсон
4

Подобно ответу Тобиаса Шитковски, но С # и имеет возможность иметь несколько полей в константах.

Чтобы использовать это, просто поместите [Уникальный] в любое поле, которое вы хотите сделать уникальным. Для строк вам нужно будет сделать что-то вроде (обратите внимание на атрибут MaxLength):

[Unique]
[MaxLength(450)] // nvarchar(450) is max allowed to be in a key
public string Name { get; set; }

потому что поле строки по умолчанию - nvarchar (max), и это не будет разрешено в ключе.

Для нескольких полей в ограничении вы можете:

[Unique(Name="UniqueValuePairConstraint", Position=1)]
public int Value1 { get; set; }
[Unique(Name="UniqueValuePairConstraint", Position=2)]
public int Value2 { get; set; }

Во-первых, UniqueAttribute:

/// <summary>
/// The unique attribute. Use to mark a field as unique. The
/// <see cref="DatabaseInitializer"/> looks for this attribute to 
/// create unique constraints in tables.
/// </summary>
internal class UniqueAttribute : Attribute
{
    /// <summary>
    /// Gets or sets the name of the unique constraint. A name will be 
    /// created for unnamed unique constraints. You must name your
    /// constraint if you want multiple fields in the constraint. If your 
    /// constraint has only one field, then this property can be ignored.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Gets or sets the position of the field in the constraint, lower 
    /// numbers come first. The order is undefined for two fields with 
    /// the same position. The default position is 0.
    /// </summary>
    public int Position { get; set; }
}

Затем включите полезное расширение, чтобы получить имя таблицы базы данных из типа:

public static class Extensions
{
    /// <summary>
    /// Get a table name for a class using a DbContext.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="type">
    /// The class to look up the table name for.
    /// </param>
    /// <returns>
    /// The table name; null on failure;
    /// </returns>
    /// <remarks>
    /// <para>
    /// Like:
    /// <code>
    ///   DbContext context = ...;
    ///   string table = context.GetTableName&lt;Foo&gt;();
    /// </code>
    /// </para>
    /// <para>
    /// This code uses ObjectQuery.ToTraceString to generate an SQL 
    /// select statement for an entity, and then extract the table
    /// name from that statement.
    /// </para>
    /// </remarks>
    public static string GetTableName(this DbContext context, Type type)
    {
        return ((IObjectContextAdapter)context)
               .ObjectContext.GetTableName(type);
    }

    /// <summary>
    /// Get a table name for a class using an ObjectContext.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="type">
    /// The class to look up the table name for.
    /// </param>
    /// <returns>
    /// The table name; null on failure;
    /// </returns>
    /// <remarks>
    /// <para>
    /// Like:
    /// <code>
    ///   ObjectContext context = ...;
    ///   string table = context.GetTableName&lt;Foo&gt;();
    /// </code>
    /// </para>
    /// <para>
    /// This code uses ObjectQuery.ToTraceString to generate an SQL 
    /// select statement for an entity, and then extract the table
    /// name from that statement.
    /// </para>
    /// </remarks>
    public static string GetTableName(this ObjectContext context, Type type)
    {
        var genericTypes = new[] { type };
        var takesNoParameters = new Type[0];
        var noParams = new object[0];
        object objectSet = context.GetType()
                            .GetMethod("CreateObjectSet", takesNoParameters)
                            .MakeGenericMethod(genericTypes)
                            .Invoke(context, noParams);
        var sql = (string)objectSet.GetType()
                  .GetMethod("ToTraceString", takesNoParameters)
                  .Invoke(objectSet, noParams);
        Match match = 
            Regex.Match(sql, @"FROM\s+(.*)\s+AS", RegexOptions.IgnoreCase);
        return match.Success ? match.Groups[1].Value : null;
    }
}

Затем инициализатор базы данных:

/// <summary>
///     The database initializer.
/// </summary>
public class DatabaseInitializer : IDatabaseInitializer<PedContext>
{
    /// <summary>
    /// Initialize the database.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    public void InitializeDatabase(FooContext context)
    {
        // if the database has changed, recreate it.
        if (context.Database.Exists()
            && !context.Database.CompatibleWithModel(false))
        {
            context.Database.Delete();
        }

        if (!context.Database.Exists())
        {
            context.Database.Create();

            // Look for database tables in the context. Tables are of
            // type DbSet<>.
            foreach (PropertyInfo contextPropertyInfo in 
                     context.GetType().GetProperties())
            {
                var contextPropertyType = contextPropertyInfo.PropertyType;
                if (contextPropertyType.IsGenericType
                    && contextPropertyType.Name.Equals("DbSet`1"))
                {
                    Type tableType = 
                        contextPropertyType.GetGenericArguments()[0];
                    var tableName = context.GetTableName(tableType);
                    foreach (var uc in UniqueConstraints(tableType, tableName))
                    {
                        context.Database.ExecuteSqlCommand(uc);
                    }
                }
            }

            // this is a good place to seed the database
            context.SaveChanges();
        }
    }

    /// <summary>
    /// Get a list of TSQL commands to create unique constraints on the given 
    /// table. Looks through the table for fields with the UniqueAttribute
    /// and uses those and the table name to build the TSQL strings.
    /// </summary>
    /// <param name="tableClass">
    /// The class that expresses the database table.
    /// </param>
    /// <param name="tableName">
    /// The table name in the database.
    /// </param>
    /// <returns>
    /// The list of TSQL statements for altering the table to include unique 
    /// constraints.
    /// </returns>
    private static IEnumerable<string> UniqueConstraints(
        Type tableClass, string tableName)
    {
        // the key is the name of the constraint and the value is a list 
        // of (position,field) pairs kept in order of position - the entry
        // with the lowest position is first.
        var uniqueConstraints = 
            new Dictionary<string, List<Tuple<int, string>>>();
        foreach (PropertyInfo entityPropertyInfo in tableClass.GetProperties())
        {
            var unique = entityPropertyInfo.GetCustomAttributes(true)
                         .OfType<UniqueAttribute>().FirstOrDefault();
            if (unique != null)
            {
                string fieldName = entityPropertyInfo.Name;

                // use the name field in the UniqueAttribute or create a
                // name if none is given
                string constraintName = unique.Name
                                        ?? string.Format(
                                            "constraint_{0}_unique_{1}",
                                            tableName
                                               .Replace("[", string.Empty)
                                               .Replace("]", string.Empty)
                                               .Replace(".", "_"),
                                            fieldName);

                List<Tuple<int, string>> constraintEntry;
                if (!uniqueConstraints.TryGetValue(
                        constraintName, out constraintEntry))
                {
                    uniqueConstraints.Add(
                        constraintName, 
                        new List<Tuple<int, string>> 
                        {
                            new Tuple<int, string>(
                                unique.Position, fieldName) 
                        });
                }
                else
                {
                    // keep the list of fields in order of position
                    for (int i = 0; ; ++i)
                    {
                        if (i == constraintEntry.Count)
                        {
                            constraintEntry.Add(
                                new Tuple<int, string>(
                                    unique.Position, fieldName));
                            break;
                        }

                        if (unique.Position < constraintEntry[i].Item1)
                        {
                            constraintEntry.Insert(
                                i, 
                                new Tuple<int, string>(
                                    unique.Position, fieldName));
                            break;
                        }
                    }
                }
            }
        }

        return
            uniqueConstraints.Select(
                uc =>
                string.Format(
                    "ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE ({2})",
                    tableName,
                    uc.Key,
                    string.Join(",", uc.Value.Select(v => v.Item2))));
    }
}
mheyman
источник
2

Решил проблему размышлением (извините, ребята, VB.Net ...)

Сначала определите атрибут UniqueAttribute:

<AttributeUsage(AttributeTargets.Property, AllowMultiple:=False, Inherited:=True)> _
Public Class UniqueAttribute
    Inherits Attribute

End Class

Затем улучшите свою модель, например

<Table("Person")> _
Public Class Person

    <Unique()> _
    Public Property Username() As String

End Class

Наконец, создайте собственный DatabaseInitializer (в моей версии я воссоздаю БД при изменениях БД только в режиме отладки ...). В этом DatabaseInitializer индексы автоматически создаются на основе уникальных атрибутов:

Imports System.Data.Entity
Imports System.Reflection
Imports System.Linq
Imports System.ComponentModel.DataAnnotations

Public Class DatabaseInitializer
    Implements IDatabaseInitializer(Of DBContext)

    Public Sub InitializeDatabase(context As DBContext) Implements IDatabaseInitializer(Of DBContext).InitializeDatabase
        Dim t As Type
        Dim tableName As String
        Dim fieldName As String

        If Debugger.IsAttached AndAlso context.Database.Exists AndAlso Not context.Database.CompatibleWithModel(False) Then
            context.Database.Delete()
        End If

        If Not context.Database.Exists Then
            context.Database.Create()

            For Each pi As PropertyInfo In GetType(DBContext).GetProperties
                If pi.PropertyType.IsGenericType AndAlso _
                    pi.PropertyType.Name.Contains("DbSet") Then

                    t = pi.PropertyType.GetGenericArguments(0)

                    tableName = t.GetCustomAttributes(True).OfType(Of TableAttribute).FirstOrDefault.Name
                    For Each piEntity In t.GetProperties
                        If piEntity.GetCustomAttributes(True).OfType(Of Model.UniqueAttribute).Any Then

                            fieldName = piEntity.Name
                            context.Database.ExecuteSqlCommand("ALTER TABLE " & tableName & " ADD CONSTRAINT con_Unique_" & tableName & "_" & fieldName & " UNIQUE (" & fieldName & ")")

                        End If
                    Next
                End If
            Next

        End If

    End Sub

End Class

Возможно, это поможет ...

Тобиас Шитковски
источник
1

Если вы переопределите метод ValidateEntity в своем классе DbContext, вы также можете поместить туда логику. Преимущество здесь в том, что у вас будет полный доступ ко всем своим DbSet. Вот пример:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Data.Entity.Validation;
using System.Linq;

namespace MvcEfClient.Models
{
    public class Location
    {
        [Key]
        public int LocationId { get; set; }

        [Required]
        [StringLength(50)]
        public string Name { get; set; }
    }

    public class CommitteeMeetingContext : DbContext
    {
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }

        protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
        {
            List<DbValidationError> validationErrors = new List<DbValidationError>();

            // Check for duplicate location names

            if (entityEntry.Entity is Location)
            {
                Location location = entityEntry.Entity as Location;

                // Select the existing location

                var existingLocation = (from l in Locations
                                        where l.Name == location.Name && l.LocationId != location.LocationId
                                        select l).FirstOrDefault();

                // If there is an existing location, throw an error

                if (existingLocation != null)
                {
                    validationErrors.Add(new DbValidationError("Name", "There is already a location with the name '" + location.Name + "'"));
                    return new DbEntityValidationResult(entityEntry, validationErrors);
                }
            }

            return base.ValidateEntity(entityEntry, items);
        }

        public DbSet<Location> Locations { get; set; }
    }
}
Фрэнк Хоффман
источник
1

Если вы используете EF5 и у вас все еще есть этот вопрос, приведенное ниже решение решило его для меня.

Я использую сначала код, поэтому помещаю:

this.Sql("CREATE UNIQUE NONCLUSTERED INDEX idx_unique_username ON dbo.Users(Username) WHERE Username IS NOT NULL;");

в сценарии миграции хорошо справились со своей задачей. Он также допускает значения NULL!

FDIM
источник
1

Используя подход EF Code First, можно реализовать поддержку уникальных ограничений на основе атрибутов, используя следующую технику.

Создайте атрибут маркера

[AttributeUsage(AttributeTargets.Property)]
public class UniqueAttribute : System.Attribute { }

Отметьте свойства, которые вы хотите сделать уникальными для объектов, например

[Unique]
public string EmailAddress { get; set; }

Создайте инициализатор базы данных или используйте существующий для создания уникальных ограничений.

public class DbInitializer : IDatabaseInitializer<DbContext>
{
    public void InitializeDatabase(DbContext db)
    {
        if (db.Database.Exists() && !db.Database.CompatibleWithModel(false))
        {
            db.Database.Delete();
        }

        if (!db.Database.Exists())
        {
            db.Database.Create();
            CreateUniqueIndexes(db);
        }
    }

    private static void CreateUniqueIndexes(DbContext db)
    {
        var props = from p in typeof(AppDbContext).GetProperties()
                    where p.PropertyType.IsGenericType
                       && p.PropertyType.GetGenericTypeDefinition()
                       == typeof(DbSet<>)
                    select p;

        foreach (var prop in props)
        {
            var type = prop.PropertyType.GetGenericArguments()[0];
            var fields = from p in type.GetProperties()
                         where p.GetCustomAttributes(typeof(UniqueAttribute),
                                                     true).Any()
                         select p.Name;

            foreach (var field in fields)
            {
                const string sql = "ALTER TABLE dbo.[{0}] ADD CONSTRAINT"
                                 + " [UK_dbo.{0}_{1}] UNIQUE ([{1}])";
                var command = String.Format(sql, type.Name, field);
                db.Database.ExecuteSqlCommand(command);
            }
        }
    }   
}

Настройте контекст базы данных для использования этого инициализатора в коде запуска (например, в main()или Application_Start())

Database.SetInitializer(new DbInitializer());

Решение похоже на решение Mheyman, но с упрощением, поскольку оно не поддерживает составные ключи. Для использования с EF 5.0+.

Михкель Мююр
источник
1

Решение Fluent Api:

modelBuilder.Entity<User>(entity =>
{
    entity.HasIndex(e => e.UserId)
          .HasName("IX_User")
          .IsUnique();

    entity.HasAlternateKey(u => u.Email);

    entity.HasIndex(e => e.Email)
          .HasName("IX_Email")
          .IsUnique();
});
пьер
источник
0

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

public class Person : IValidatableObject
{
    public virtual int ID { get; set; }
    public virtual string Name { get; set; }


    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var field = new[] { "Name" }; // Must be the same as the property

        PFContext db = new PFContext();

        Person person = validationContext.ObjectInstance as Person;

        var existingPerson = db.Persons.FirstOrDefault(a => a.Name == person.Name);

        if (existingPerson != null)
        {
            yield return new ValidationResult("That name is already in the db", field);
        }
    }
}
Хуан Карлос Пуэрто
источник
0

Используйте уникальный валидатор свойств.

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) {
   var validation_state = base.ValidateEntity(entityEntry, items);
   if (entityEntry.Entity is User) {
       var entity = (User)entityEntry.Entity;
       var set = Users;

       //check name unique
       if (!(set.Any(any_entity => any_entity.Name == entity.Name))) {} else {
           validation_state.ValidationErrors.Add(new DbValidationError("Name", "The Name field must be unique."));
       }
   }
   return validation_state;
}

ValidateEntityне вызывается в одной транзакции базы данных. Следовательно, могут существовать состояния гонки с другими объектами в базе данных. Вам нужно немного взломать EF, чтобы заставить транзакцию SaveChanges(и, следовательно, ValidateEntity). DBContextне может открыть соединение напрямую, но ObjectContextможет.

using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required)) {
   ((IObjectContextAdapter)data_context).ObjectContext.Connection.Open();
   data_context.SaveChanges();
   transaction.Complete();
}
Alex
источник
0

После прочтения этого вопроса у меня возник собственный вопрос в процессе попытки реализовать атрибут для обозначения свойств как уникальных ключей, таких как ответы Михкеля Мюура , Тобиаса Шиттковски и Мхеймана : Сопоставьте свойства кода Entity Framework с столбцами базы данных (CSpace в SSpace)

Я наконец-то пришел к этому ответу, который может отображать как скалярные, так и навигационные свойства до столбцов базы данных и создавать уникальный индекс в определенной последовательности, указанной в атрибуте. Этот код предполагает, что вы реализовали UniqueAttribute со свойством Sequence и применили его к свойствам класса сущности EF, которые должны представлять уникальный ключ сущности (отличный от первичного ключа).

Примечание. Этот код основан на версии EF 6.1 (или более поздней), которая EntityContainerMappingнедоступна в предыдущих версиях.

Public Sub InitializeDatabase(context As MyDB) Implements IDatabaseInitializer(Of MyDB).InitializeDatabase
    If context.Database.CreateIfNotExists Then
        Dim ws = DirectCast(context, System.Data.Entity.Infrastructure.IObjectContextAdapter).ObjectContext.MetadataWorkspace
        Dim oSpace = ws.GetItemCollection(Core.Metadata.Edm.DataSpace.OSpace)
        Dim entityTypes = oSpace.GetItems(Of EntityType)()
        Dim entityContainer = ws.GetItems(Of EntityContainer)(DataSpace.CSpace).Single()
        Dim entityMapping = ws.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).Single.EntitySetMappings
        Dim associations = ws.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).Single.AssociationSetMappings
        For Each setType In entityTypes
           Dim cSpaceEntitySet = entityContainer.EntitySets.SingleOrDefault( _
              Function(t) t.ElementType.Name = setType.Name)
           If cSpaceEntitySet Is Nothing Then Continue For ' Derived entities will be skipped
           Dim sSpaceEntitySet = entityMapping.Single(Function(t) t.EntitySet Is cSpaceEntitySet)
           Dim tableInfo As MappingFragment
           If sSpaceEntitySet.EntityTypeMappings.Count = 1 Then
              tableInfo = sSpaceEntitySet.EntityTypeMappings.Single.Fragments.Single
           Else
              ' Select only the mapping (esp. PropertyMappings) for the base class
              tableInfo = sSpaceEntitySet.EntityTypeMappings.Where(Function(m) m.IsOfEntityTypes.Count _
                 = 1 AndAlso m.IsOfEntityTypes.Single.Name Is setType.Name).Single().Fragments.Single
           End If
           Dim tableName = If(tableInfo.StoreEntitySet.Table, tableInfo.StoreEntitySet.Name)
           Dim schema = tableInfo.StoreEntitySet.Schema
           Dim clrType = Type.GetType(setType.FullName)
           Dim uniqueCols As IList(Of String) = Nothing
           For Each propMap In tableInfo.PropertyMappings.OfType(Of ScalarPropertyMapping)()
              Dim clrProp = clrType.GetProperty(propMap.Property.Name)
              If Attribute.GetCustomAttribute(clrProp, GetType(UniqueAttribute)) IsNot Nothing Then
                 If uniqueCols Is Nothing Then uniqueCols = New List(Of String)
                 uniqueCols.Add(propMap.Column.Name)
              End If
           Next
           For Each navProp In setType.NavigationProperties
              Dim clrProp = clrType.GetProperty(navProp.Name)
              If Attribute.GetCustomAttribute(clrProp, GetType(UniqueAttribute)) IsNot Nothing Then
                 Dim assocMap = associations.SingleOrDefault(Function(a) _
                    a.AssociationSet.ElementType.FullName = navProp.RelationshipType.FullName)
                 Dim sProp = assocMap.Conditions.Single
                 If uniqueCols Is Nothing Then uniqueCols = New List(Of String)
                 uniqueCols.Add(sProp.Column.Name)
              End If
           Next
           If uniqueCols IsNot Nothing Then
              Dim propList = uniqueCols.ToArray()
              context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IX_" & tableName & "_" & String.Join("_", propList) _
                 & " ON " & schema & "." & tableName & "(" & String.Join(",", propList) & ")")
           End If
        Next
    End If
End Sub
BlueMonkMN
источник
0

Для тех, кто использует конфигурации с первым кодом, вы также можете использовать объект IndexAttribute как ColumnAnnotation и установить для его свойства IsUnique значение true.

Например:

var indexAttribute = new IndexAttribute("IX_name", 1) {IsUnique = true};

Property(i => i.Name).HasColumnAnnotation("Index",new IndexAnnotation(indexAttribute));

Это создаст уникальный индекс с именем IX_name в столбце Name.

Паскаль Шарбонно
источник
0

Извините за поздний ответ, но мне было приятно поговорить с вами

Я написал об этом в проекте кода

В общем, это зависит от атрибутов, которые вы добавляете в классы для создания ваших уникальных индексов.

Вахид Битар
источник