Entity Framework Code First'te Benzersiz Kısıtlama


125

Soru

Akıcı sözdizimi veya bir öznitelik kullanarak bir özellik için benzersiz bir kısıt tanımlamak mümkün müdür? Değilse, geçici çözümler nelerdir?

Birincil anahtara sahip bir kullanıcı sınıfım var, ancak e-posta adresinin de benzersiz olduğundan emin olmak istiyorum. Veritabanını doğrudan düzenlemeden bu mümkün müdür?

Çözüm (Matt'in cevabına göre)

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
Bunu yapmanın, uygulamanızı yalnızca sözdizimini tam olarak kabul eden veritabanlarıyla, bu durumda SQL Server ile sınırlandıracağını unutmayın. Uygulamanızı bir Oracle sağlayıcısı ile çalıştırırsanız başarısız olur.
DamienG

1
Bu durumda sadece yeni bir Başlatıcı sınıfı oluşturmam gerekir, ancak bu geçerli bir noktadır.
kim3er


Evet, artık EF 6.1'den beri desteklenmektedir .
Evandro Pomatti

Yanıtlar:


61

Anladığım kadarıyla, bunu şu anda Entity Framework ile yapmanın bir yolu yok. Ancak, bu sadece benzersiz kısıtlamalarla ilgili bir sorun değildir ... dizinler oluşturmak, kısıtlamaları kontrol etmek ve muhtemelen tetikleyiciler ve diğer yapılar da isteyebilirsiniz. İşte kod ilk kurulumunuzla kullanabileceğiniz basit bir model , kuşkusuz veritabanından bağımsız değildir:

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

Diğer bir seçenek de, veritabanınıza veri eklemenin / güncellemenin tek yöntemi etki alanı modelinizse, benzersizlik gereksinimini kendiniz uygulayabilir ve veritabanını bunun dışında bırakabilirsiniz. Bu daha taşınabilir bir çözümdür ve sizi kodunuzdaki iş kurallarınız konusunda net olmaya zorlar, ancak veritabanınızı geçersiz verilerin arka kapılara açılmasına açık bırakır.


DB'min tambur kadar sıkı olmasını seviyorum, mantık iş katmanında çoğaltılıyor. Cevabınız sadece CTP4 ile çalışıyor ama beni doğru yola götürdü, orijinal sorumun altında CTP5 ile uyumlu bir çözüm sağladım. Çok teşekkürler!
kim3er

23
Uygulamanız tek kullanıcılı değilse , benzersiz bir kısıtlamanın yalnızca kodla uygulayamayacağınız bir şey olduğuna inanıyorum . Kodda bir ihlal olasılığını önemli ölçüde azaltabilirsiniz (aramadan önce benzersizliği kontrol ederek SaveChanges()), ancak yine de benzersizlik kontrolü zamanı ile zamanı arasında başka bir ekleme / güncelleme olasılığı vardır SaveChanges(). Bu nedenle, uygulamanın görev açısından ne kadar kritik olduğuna ve benzersizlik ihlalinin olasılığına bağlı olarak, kısıtlamayı veritabanına eklemek muhtemelen en iyisidir.
devuxer

1
Benzersizlik çekinizin SaveChanges ile aynı işlemin bir parçası olması gerekir. Veritabanınızın asit uyumlu olduğunu varsayarsak, bu şekilde benzersizliği kesinlikle uygulayabilmelisiniz. Şimdi EF'in işlem yaşam döngüsünü bu şekilde doğru şekilde yönetmenize izin verip vermediği başka bir sorudur.
mattmc3

1
@ mattmc3 İşlem yalıtım seviyenize bağlıdır. Yalnızca serializable isolation level(veya özel tablo kilitleme, ugh) aslında kodunuzda benzersizliği garanti etmenize izin verir. Ancak çoğu insan serializable isolation levelperformans nedenlerinden dolayı kullanmıyor . MS Sql Sunucusunda varsayılan değer read committed. Şu adresten
Nathan

3
EntityFramework 6.1.0 artık IndexAttribute için desteğe sahiptir, bunu temelde özelliklerin üzerine ekleyebilirsiniz.
2014

45

EF 6.1 ile başlayarak artık mümkündür:

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

Bu size kesinlikle benzersiz kısıtlama yerine benzersiz bir dizin sağlayacaktır. Çoğu pratik amaç için aynıdırlar .


5
@Dave: ilgili özelliklerin ( kaynak ) özniteliklerinde aynı dizin adını kullanın .
Mihkel Müür

Bunun benzersiz bir kontraint yerine benzersiz bir dizin oluşturduğunu unutmayın . Neredeyse aynı olsalar da tamamen aynı değiller (anladığım kadarıyla benzersiz kısıtlamalar bir FK'nin hedefi olarak kullanılabilir). Bir kısıtlama için SQL çalıştırmanız gerekir.
Richard

(Son yorumun ardından) Diğer kaynaklar, bu sınırlamanın SQL Server'ın daha yeni sürümlerinde kaldırıldığını öne sürüyor ... ancak BOL tam olarak tutarlı değil.
Richard

@Richard: öznitelik tabanlı benzersiz kısıtlamalar da mümkündür ( ikinci cevabıma bakın ), ancak kutunun dışında değil.
Mihkel Müür

1
@exSnake: SQL Server 2008'den beri, benzersiz dizin varsayılan olarak sütun başına tek bir NULL değerini destekler. Birden fazla NULL için destek gerektiğinde, filtrelenmiş bir dizine ihtiyaç duyulacaktır, başka bir soruya bakın .
Mihkel Müür

28

Gerçekten bununla ilgili değil ama bazı durumlarda yardımcı olabilir.

Tablonuz için bir kısıtlama görevi görecek 2 sütun üzerinde benzersiz bir bileşik dizin oluşturmak istiyorsanız, sürüm 4.3'ten itibaren bunu başarmak için yeni geçiş mekanizmasını kullanabilirsiniz:

Temel olarak, taşıma komut dosyalarınızdan birine buna benzer bir çağrı eklemeniz gerekir:

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

Bunun gibi bir şey:

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'in Rails stili geçişlere sahip olduğunu görmek güzel. Şimdi keşke Mono'da çalıştırabilsem.
kim3er

2
Down () prosedüründe DropIndex'iniz olması gerekmez mi? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg

5

Veritabanı oluşturulurken SQL'in yürütülmesi için tam bir hack yapıyorum. Kendi DatabaseInitializer'ımı oluşturuyorum ve sağlanan başlatıcılardan birinden devralıyorum.

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 ifadelerimde bulabildiğim tek yer orası.

Bu CTP4'ten. CTP5'te nasıl çalıştığını bilmiyorum.


Kelly teşekkürler! O olay işleyicinin farkında değildim. Nihai çözümüm, SQL'i InitializeDatabase yöntemine yerleştirir.
kim3er

5

Sadece bunu yapmanın bir yolu olup olmadığını bulmaya çalışıyorum, şimdiye kadar bulduğum tek yol bunu kendim uygulamaktı, benzersiz olması gereken alanların adını sağladığınız her sınıfa eklenecek bir öznitelik oluşturdum:

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

Sonra sınıfımda ekleyeceğim:

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

Son olarak, depomda, Ekle yönteminde veya aşağıdaki gibi Değişiklikleri Kaydederken bir yöntem ekleyeceğim:

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üşünceye güvenmemiz gerektiğinden çok hoş değil ama şu ana kadar benim için işe yarayan yaklaşım bu! = D



4

EF5 Code First Migrations kullanarak görsel temelde kolay bir yol

Herkese Açık Sınıf Örneği

    Public Property SampleId As Integer

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

    Public Property Code() As String

Sınıf Sonu

MaxLength özelliği, benzersiz dize türü dizini için çok önemlidir

Cmd'yi çalıştırın: update-database -verbose

cmd çalıştırdıktan sonra: add-migration 1

oluşturulan dosyada

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

Bu aslında özel bir DB başlatıcıdan daha uygun bir cevaptır.
Shaun Wilson

4

Tobias Schittkowski'nin cevabına benzer ancak C # ve yorumlarda birden fazla alana sahip olma yeteneğine sahiptir.

Bunu kullanmak için, benzersiz olmasını istediğiniz herhangi bir alana bir [Benzersiz] yerleştirin. Dizeler için aşağıdaki gibi bir şey yapmanız gerekecektir (MaxLength niteliğine dikkat edin):

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

çünkü varsayılan dize alanı nvarchar (maks) ve buna bir anahtarda izin verilmeyecek.

Kısıtlamadaki birden çok alan için şunları yapabilirsiniz:

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

İlk olarak, 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; }
}

Ardından, veritabanı tablosu adını bir türden almak için yararlı bir uzantı ekleyin:

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

Ardından, veritabanı başlatıcı:

/// <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

Sorunu yansıtarak çözdüm (üzgünüm millet, VB.Net ...)

İlk olarak, bir UniqueAttribute özniteliği tanımlayın:

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

End Class

Ardından modelinizi şu şekilde geliştirin:

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

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

End Class

Son olarak, özel bir DatabaseInitializer oluşturun (Benim sürümümde, DB üzerinde DB değişikliklerini yalnızca hata ayıklama modunda yeniden oluşturuyorum ...). Bu DatabaseInitializer'da, endeksler Unique-Attributes'a göre otomatik olarak oluşturulur:

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

Belki bu yardımcı olur ...


1

DbContext sınıfınızda ValidateEntity yöntemini geçersiz kılarsanız, mantığı oraya da koyabilirsiniz. Buradaki avantaj, tüm DbSet'lerinize tam erişime sahip olmanızdır. İşte bir örnek:

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 kullanıyorsanız ve hala bu sorunuz varsa, aşağıdaki çözüm benim için çözdü.

Önce kod yaklaşımını kullanıyorum, bu nedenle şunu koyuyorum:

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

geçiş betiğinde işi iyi yaptı. Ayrıca NULL değerlere izin verir!


1

EF Code First yaklaşımı ile, aşağıdaki teknik kullanılarak öznitelik tabanlı benzersiz kısıtlama desteği uygulanabilir.

Bir işaretçi niteliği oluşturun

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

Varlıklar üzerinde benzersiz olmasını istediğiniz özellikleri işaretleyin, örneğin

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

Bir veritabanı başlatıcısı oluşturun veya benzersiz kısıtlamalar oluşturmak için mevcut olanı kullanın

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

Veritabanı bağlamınızı bu başlatıcıyı başlangıç ​​kodunda kullanacak şekilde ayarlayın (ör. main()Veya içinde Application_Start())

Database.SetInitializer(new DbInitializer());

Çözüm, bileşik anahtarları desteklememenin basitleştirilmesiyle mheyman'a benzer. EF 5.0+ ile kullanılmak üzere.


1

Akıcı Api çözümü:

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

Bugün bu problemle karşılaştım ve sonunda çözebildim. Doğru bir yaklaşım olup olmadığını bilmiyorum ama en azından devam edebilirim:

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

Benzersiz bir mülk doğrulayıcı kullanın.

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

ValidateEntityaynı veritabanı işlemi içinde çağrılmaz. Bu nedenle, veritabanındaki diğer varlıklarla yarış koşulları olabilir. Etrafında SaveChanges(ve dolayısıyla ValidateEntity) bir işlemi zorlamak için EF'i biraz hacklemelisiniz . DBContextdoğrudan bağlantıyı açamaz, ancak ObjectContextaçabilir.

using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required)) {
   ((IObjectContextAdapter)data_context).ObjectContext.Connection.Open();
   data_context.SaveChanges();
   transaction.Complete();
}


0

Bu soruyu okuduktan sonra , Mihkel Müür'ün , Tobias Schittkowski'nin ve mheyman'ın yanıtları gibi, özellikleri benzersiz anahtarlar olarak belirlemek için bir özniteliği uygulamaya çalışma sürecinde kendi sorum vardı : Varlık Çerçevesi kod özelliklerini veritabanı sütunlarına eşle (CSpace'den SSpace'e)

Sonunda, hem skaler hem de gezinme özelliklerini veritabanı sütunlarına eşleyebilen ve öznitelikte belirtilen belirli bir sırayla benzersiz bir dizin oluşturabilen bu yanıta ulaştım. Bu kod, bir Sequence özelliğine sahip bir UniqueAttribute uyguladığınızı ve bunu varlığın benzersiz anahtarını (birincil anahtar dışında) temsil etmesi gereken EF varlık sınıfı özelliklerine uyguladığınızı varsayar.

Not: Bu kod EntityContainerMapping, önceki sürümlerde bulunmayan EF sürüm 6.1'e (veya sonrasına) dayanır .

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

Kod ilk konfigürasyonlarını kullananlar için IndexAttribute nesnesini ColumnAnnotation olarak kullanabilir ve IsUnique özelliğini true olarak ayarlayabilirsiniz.

Örnek olarak:

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

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

Bu, Ad sütununda IX_name adlı benzersiz bir dizin oluşturacaktır.


0

Geç cevap verdiğim için özür dilerim ama seninle konuşmayı iyi buldum

Bunun hakkında kod projesinde yayınladım

Genel olarak, benzersiz dizinlerinizi oluşturmak için sınıflara koyduğunuz özniteliklere bağlıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.