Unikalne ograniczenie w Entity Framework Code First

125

Pytanie

Czy można zdefiniować unikalne ograniczenie dla właściwości przy użyciu płynnej składni lub atrybutu? Jeśli nie, jakie są obejścia?

Mam klasę użytkownika z kluczem podstawowym, ale chciałbym się upewnić, że adres e-mail jest również niepowtarzalny. Czy jest to możliwe bez bezpośredniej edycji bazy danych?

Rozwiązanie (na podstawie odpowiedzi Matta)

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
źródło
1
Należy pamiętać, że wykonanie tej czynności ogranicza aplikację tylko do baz danych, które akceptują tę dokładną składnię - w tym przypadku SQL Server. Jeśli uruchomisz aplikację u dostawcy Oracle, zakończy się niepowodzeniem.
DamienG
1
W takiej sytuacji musiałbym tylko utworzyć nową klasę Initializer, ale jest to ważny punkt.
kim3er
3
Sprawdź ten wpis: ValidationAttribute, który sprawdza poprawność unikatowego pola względem innych wierszy w bazie danych , rozwiązanie jest przeznaczone dla ObjectContextlub DbContext.
Shimmy Weitzhandler
Tak, jest teraz obsługiwany od wersji EF 6.1 .
Evandro Pomatti

Odpowiedzi:

61

O ile wiem, w tej chwili nie ma sposobu, aby to zrobić za pomocą Entity Framework. Jednak nie jest to tylko problem z unikalnymi ograniczeniami ... możesz chcieć utworzyć indeksy, sprawdzić ograniczenia i prawdopodobnie wyzwalacze i inne konstrukcje. Oto prosty wzorzec, którego możesz użyć w swojej pierwszej konfiguracji kodu, chociaż wprawdzie nie jest to agnostyk bazy danych:

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

Inną opcją jest to, że jeśli model domeny jest jedyną metodą wstawiania / aktualizowania danych w bazie danych, możesz samodzielnie zaimplementować wymóg unikalności i pozostawić bazę danych poza nim. Jest to bardziej przenośne rozwiązanie i wymusza jasne określenie reguł biznesowych w kodzie, ale pozostawia bazę danych otwartą na nieprawidłowe dane, które zostaną cofnięte.

mattmc3
źródło
Lubię, gdy moja baza danych jest tak zwarta jak bęben, a logika jest replikowana w warstwie biznesowej. Twoja odpowiedź działa tylko z CTP4, ale zaprowadziła mnie na właściwą ścieżkę. Podałem rozwiązanie zgodne z CTP5 poniżej mojego pierwotnego pytania. Wielkie dzięki!
kim3er
23
Jeśli Twoja aplikacja nie jest przeznaczona dla jednego użytkownika, uważam, że unikalne ograniczenie to jedyna rzecz, której nie można wymusić samym kodem. Możesz radykalnie zmniejszyć prawdopodobieństwo naruszenia kodu (sprawdzając unikalność przed wywołaniem SaveChanges()), ale nadal istnieje możliwość, że kolejna wstawka / aktualizacja pojawi się między czasem sprawdzenia unikalności a czasem SaveChanges(). Tak więc, w zależności od tego, jak krytyczna jest aplikacja i prawdopodobieństwo naruszenia jej wyjątkowości, prawdopodobnie najlepiej jest dodać ograniczenie do bazy danych.
devuxer
1
Czek na unikalność musiałby być częścią tej samej transakcji, co Twoje SaveChanges. Zakładając, że Twoja baza danych jest zgodna z kwasami, absolutnie powinieneś być w stanie wymusić unikalność w ten sposób. Teraz, czy EF umożliwia prawidłowe zarządzanie cyklem życia transakcji w ten sposób, to inna kwestia.
mattmc3
1
@ mattmc3 To zależy od poziomu izolacji transakcji. Tylko serializable isolation level(lub niestandardowe blokowanie tabeli, ugh) faktycznie pozwoliłoby ci zagwarantować unikalność twojego kodu. Ale większość ludzi nie używa go ze serializable isolation levelwzględu na wydajność. Wartość domyślna w MS Sql Server to read committed. Zobacz serię 4 części zaczynającą się od: michaeljswart.com/2010/03/…
Nathan
3
EntityFramework 6.1.0 obsługuje teraz IndexAttribute, który można w zasadzie dodać do właściwości.
sotn
45

Począwszy od EF 6.1 jest to teraz możliwe:

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

Zapewni to unikalny indeks zamiast unikalnego ograniczenia, ściśle mówiąc. W większości praktycznych celów są takie same .

Mihkel Müür
źródło
5
@Dave: po prostu użyj tej samej nazwy indeksu w atrybutach odpowiednich właściwości ( źródło ).
Mihkel Müür
Uwaga ta tworzy unikatowy indeks zamiast unikalny contraint . Chociaż są prawie takie same, nie są całkiem takie same (jak rozumiem, unikalne ograniczenia mogą być używane jako cel FK). W przypadku ograniczenia musisz wykonać SQL.
Richard,
(Po ostatnim komentarzu) Inne źródła sugerują, że to ograniczenie zostało usunięte w nowszych wersjach SQL Server ... ale BOL nie jest całkowicie spójny.
Richard,
@Richard: unikalne ograniczenia oparte na atrybutach są również możliwe (zobacz moją drugą odpowiedź ), ale nie są one wyjęte z pudełka.
Mihkel Müür
1
@exSnake: Od SQL Server 2008, unikalny indeks obsługuje domyślnie jedną wartość NULL na kolumnę. W przypadku, gdy wymagana jest obsługa wielu wartości NULL, filtrowany indeks byłby potrzebny. Zobacz inne pytanie .
Mihkel Müür
28

Niezupełnie z tym związane, ale w niektórych przypadkach może to pomóc.

Jeśli chcesz utworzyć unikalny indeks złożony na, powiedzmy, 2 kolumnach, który będzie stanowił ograniczenie dla Twojej tabeli, od wersji 4.3 możesz użyć nowego mechanizmu migracji, aby to osiągnąć:

Zasadniczo musisz wstawić takie wywołanie w jednym ze swoich skryptów migracji:

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

Coś w tym stylu:

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
źródło
Miło widzieć, że EF ma migracje w stylu Railsów. Teraz gdybym tylko mógł uruchomić to na Mono.
kim3er
2
Czy nie powinieneś mieć również DropIndex w procedurze Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg
5

Robię kompletny hack, aby wykonać SQL podczas tworzenia bazy danych. Tworzę własny DatabaseInitializer i dziedziczę z jednego z dostarczonych inicjatorów.

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

To jedyne miejsce, w którym mogłem znaleźć klin w moich instrukcjach SQL.

To pochodzi z CTP4. Nie wiem, jak to działa w CTP5.

Kelly Ethridge
źródło
Dzięki Kelly! Nie byłem świadomy tego programu obsługi zdarzeń. Moje ewentualne rozwiązanie umieszcza SQL w metodzie InitializeDatabase.
kim3er
5

Po prostu próbując dowiedzieć się, czy istnieje sposób na zrobienie tego, jedyny sposób, który do tej pory odkryłem, polegał na samodzielnym egzekwowaniu tego, stworzyłem atrybut, który ma być dodany do każdej klasy, w której podajesz nazwy pól, które musisz być niepowtarzalny:

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

Następnie w mojej klasie dodam to:

[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]
}

Na koniec dodam metodę w moim repozytorium, w metodzie Add lub podczas zapisywania zmian w następujący sposób:

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

Niezbyt ładne, ponieważ musimy polegać na refleksji, ale jak dotąd jest to podejście, które działa dla mnie! = D

Rosendo
źródło
5

Również w 6.1 możesz użyć płynnej wersji składni odpowiedzi @ mihkelmuur w następujący sposób:

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

Metoda płynna nie jest idealna IMO, ale przynajmniej jest teraz możliwa.

Więcej informacji na blogu Arthura Vickersa http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

Nie kochany
źródło
4

Łatwy sposób w Visual Basic przy użyciu migracji EF5 Code First

Przykład klasy publicznej

    Public Property SampleId As Integer

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

    Public Property Code() As String

Koniec klasy

Atrybut MaxLength jest bardzo ważny dla unikalnego indeksu typu string

Uruchom cmd: update-database -verbose

po uruchomieniu cmd: dodaj-migrację 1

w wygenerowanym pliku

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
Despota
źródło
W rzeczywistości jest to bardziej odpowiednia odpowiedź niż niestandardowy inicjator bazy danych.
Shaun Wilson
4

Podobny do odpowiedzi Tobiasa Schittkowskiego, ale C # i ma możliwość posiadania wielu pól w constrtaints.

Aby z tego skorzystać, po prostu umieść [Unique] na dowolnym polu, które chcesz, aby było unikalne. W przypadku ciągów będziesz musiał zrobić coś takiego (zwróć uwagę na atrybut MaxLength):

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

ponieważ domyślne pole ciągu to nvarchar (max) i nie będzie to dozwolone w kluczu.

W przypadku wielu pól w ograniczeniu możesz:

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

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

Następnie dołącz przydatne rozszerzenie, aby uzyskać nazwę tabeli bazy danych z typu:

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

Następnie inicjator bazy danych:

/// <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
źródło
2

Rozwiązałem problem przez refleksję (przepraszam, ludzie, VB.Net ...)

Najpierw zdefiniuj atrybut UniqueAttribute:

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

End Class

Następnie ulepsz swój model

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

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

End Class

Na koniec utwórz niestandardowy DatabaseInitializer (w mojej wersji tworzę bazę danych na zmianach DB tylko w trybie debugowania ...). W tym DatabaseInitializerze indeksy są tworzone automatycznie na podstawie Unique-Attributes:

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

Być może to pomoże ...

Tobias Schittkowski
źródło
1

Jeśli zastąpisz metodę ValidateEntity w klasie DbContext, możesz również umieścić tam logikę. Zaletą jest to, że będziesz mieć pełny dostęp do wszystkich swoich zestawów DbSets. Oto przykład:

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; }
    }
}
Frank Hoffman
źródło
1

Jeśli używasz EF5 i nadal masz to pytanie, poniższe rozwiązanie rozwiązało je za mnie.

Używam podejścia najpierw kodu, dlatego umieszczam:

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

w skrypcie migracji wykonał swoją pracę dobrze. Pozwala również na wartości NULL!

FDIM
źródło
1

Dzięki podejściu EF Code First można zaimplementować obsługę ograniczeń unikatowych opartych na atrybutach przy użyciu następującej techniki.

Utwórz atrybut znacznika

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

Zaznacz właściwości, które chcesz, aby były unikalne w obiektach, np

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

Utwórz inicjator bazy danych lub użyj istniejącego, aby utworzyć unikatowe ograniczenia

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

Ustaw kontekst bazy danych, aby użyć tego inicjatora w kodzie startowym (np. W main()lub Application_Start())

Database.SetInitializer(new DbInitializer());

Rozwiązanie jest podobne do mheymana, z uproszczeniem polegającym na nieobsługiwaniu kluczy złożonych. Do użytku z EF 5.0+.

Mihkel Müür
źródło
1

Rozwiązanie 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();
});
Pierre
źródło
0

Zmierzyłem się dzisiaj z tym problemem i wreszcie udało mi się go rozwiązać. Nie wiem, czy to właściwe podejście, ale przynajmniej mogę kontynuować:

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);
        }
    }
}
Juan Carlos Puerto
źródło
0

Użyj unikalnego walidatora właściwości.

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

ValidateEntitynie jest wywoływana w ramach tej samej transakcji bazy danych. Dlatego w bazie danych mogą występować warunki wyścigu z innymi podmiotami. Musisz trochę zhakować EF, aby wymusić transakcję wokół SaveChanges(a zatem ValidateEntity). DBContextnie może bezpośrednio otworzyć połączenia, ale ObjectContextmoże.

using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required)) {
   ((IObjectContextAdapter)data_context).ObjectContext.Connection.Open();
   data_context.SaveChanges();
   transaction.Complete();
}
Alex
źródło
0

Po przeczytaniu tego pytania miałem własne pytanie w procesie próby zaimplementowania atrybutu do oznaczania właściwości jako unikalnych kluczy, takich jak odpowiedzi Mihkel Müür , Tobiasa Schittkowskiego i mheymana sugerują: Mapowanie właściwości kodu Entity Framework na kolumny bazy danych (CSpace to SSpace)

W końcu dotarłem do tej odpowiedzi, która może odwzorować zarówno właściwości skalarne, jak i właściwości nawigacji w dół do kolumn bazy danych i stworzyć unikalny indeks w określonej kolejności wyznaczonej na atrybucie. Ten kod zakłada, że ​​zaimplementowano UniqueAttribute z właściwością Sequence i zastosowano ją do właściwości klasy jednostki EF, które powinny reprezentować unikalny klucz jednostki (inny niż klucz podstawowy).

Uwaga: ten kod opiera się na programie EF w wersji 6.1 (lub nowszej), który uwidacznia EntityContainerMappingniedostępny w poprzednich wersjach.

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
źródło
0

W przypadku osób korzystających z konfiguracji Code First można również użyć obiektu IndexAttribute jako ColumnAnnotation i ustawić jego właściwość IsUnique na true.

W przykładzie:

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

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

Spowoduje to utworzenie unikalnego indeksu o nazwie IX_name w kolumnie Name.

Pascal Charbonneau
źródło
0

Przepraszam za spóźnioną odpowiedź, ale uznałam, że dobrze jest z tobą porozmawiać

Napisałem o tym w Code Project

Ogólnie rzecz biorąc, zależy to od atrybutów, które przypisujesz klasom, aby wygenerować unikalne indeksy

Wahid Bitar
źródło