По-перше, єдине обмеження в сутності Рамкового кодексу


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

1
Майте на увазі, що це обмежує ваш додаток лише до баз даних, які приймають саме той синтаксис - у цьому випадку SQL Server. Якщо ви запускаєте свою програму у постачальника послуг Oracle, вона вийде з ладу.
DamienG

1
У цій ситуації мені потрібно було б створити новий клас Initializer, але це дійсна точка.
kim3er

3
Ознайомтеся з цією публікацією: ValidationAttribute, що перевіряє унікальне поле щодо своїх колег у базі даних , рішення націлене на ObjectContextабо DbContext.
Шиммі Вайцхандлер

Так, він зараз підтримується з EF 6.1 .
Евандро Поматті

Відповіді:


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

Інший варіант - якщо ваша модель домену є єдиним методом вставки / оновлення даних у вашу базу даних, ви можете самостійно реалізувати вимогу унікальності та залишити базу даних поза нею. Це більш портативне рішення і змушує вас бути чіткими щодо правил вашого бізнесу у своєму коді, але залишає вашу базу даних відкритою для неправомірних даних, що повертаються назад.


Мені подобається, що мій БД щільний, як барабан, логіка повторюється у бізнес-шарі. Ви відповідаєте, що працює тільки з 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/…
Натан

3
EntityFramework 6.1.0 має підтримку IndexAttribute зараз, яку ви можете в основному додати до версії властивостей.
sotn

45

Починаючи з EF 6.1, тепер можна:

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

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


5
@Dave: просто використовуйте те саме ім’я індексу в атрибутах відповідних властивостей ( джерело ).
Mihkel Müür

Зауважте, це створює унікальний індекс, а не унікальний контрант . Хоча майже однакові, вони не зовсім однакові (наскільки я розумію, унікальні обмеження можуть використовуватися як ціль ФК). Для обмеження потрібно виконати SQL.
Річард

(Після останнього коментаря) Інші джерела припускають, що це обмеження було знято в останніх версіях SQL Server ... але BOL не повністю відповідає.
Річард

@Richard: Можливі також унікальні обмеження, засновані на атрибутах (див. Мою другу відповідь ), але не поза рамками .
Mihkel Müür

1
@exSnake: Оскільки SQL Server 2008, унікальний індекс підтримує одне значення NULL на стовпець за замовчуванням. У випадку, якщо потрібна підтримка декількох NULL, потрібно буде відфільтрований індекс, див. Інше питання .
Mihkel Müür

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

Приємно бачити, що 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]
}

Нарешті, я додам метод у моє сховище, у метод Додати або при збереженні змін так:

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

Простий спосіб у візуальній основі за допомогою коду EF5 First Migrations

Зразок громадського класу

    Public Property SampleId As Integer

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

    Public Property Code() As String

Кінцевий клас

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

Виконати cmd: update-database -verbose

після запуску cmd: додавання-міграція 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

Подібно до відповіді Тобіаса Шитцьковського, але C # і має можливість мати кілька полів у constrataints.

Щоб скористатися цим, просто розмістіть [Унікальний] у будь-якому полі, яке ви хочете бути унікальним. Для рядків вам доведеться зробити щось на зразок (зверніть увагу на атрибут 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))));
    }
}

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, ви можете також ввести логіку. Перевага тут у тому, що ви будете мати повний доступ до всіх своїх DbSets. Ось приклад:

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 значень!


1

При застосуванні коду EF First 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());

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


1

Течний розчин 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();
}


0

Прочитавши це запитання, у мене виникло власне запитання під час спроби реалізувати атрибут для позначення властивостей унікальними ключами, як відповіді Міхеля Мюйра , відповіді Тобіаса Шітковського та Меймана : Властивості коду Map 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

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 у стовпці Ім'я.


0

Вибачте за несвоєчасну відповідь, але мені здалося, що це добре зв'язати з вами

Я написав про це в кодовому проекті

Взагалі, це залежить від атрибутів, які ви кладете на класи для створення ваших унікальних індексів

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.