Kendala Unik dalam Kode Kerangka Entitas Pertama

125

Pertanyaan

Apakah mungkin untuk mendefinisikan batasan unik pada properti menggunakan sintaks yang lancar atau atribut? Jika tidak, apa solusinya?

Saya memiliki kelas pengguna dengan kunci utama, tetapi saya ingin memastikan alamat emailnya juga unik. Apakah ini mungkin tanpa mengedit database secara langsung?

Solusi (berdasarkan jawaban Matt)

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
sumber
1
Ingatlah bahwa melakukan ini membatasi aplikasi Anda hanya pada basis data yang menerima sintaks yang tepat - dalam hal ini SQL Server. Jika Anda menjalankan aplikasi Anda dengan penyedia Oracle itu akan gagal.
DamienG
1
Dalam situasi itu saya hanya perlu membuat kelas Initializer baru, tetapi ini adalah poin yang valid.
kim3er
3
Lihat pos ini: ValidationAttribute yang memvalidasi bidang unik terhadap sesama baris dalam database , solusi menargetkan salah satu ObjectContextatau DbContext.
Shimmy Weitzhandler
Ya, sekarang didukung sejak EF 6.1 .
Evandro Pomatti

Jawaban:

61

Sejauh yang saya tahu, tidak ada cara untuk melakukan ini dengan Kerangka Entity saat ini. Namun, ini bukan hanya masalah dengan kendala unik ... Anda mungkin ingin membuat indeks, memeriksa kendala, dan mungkin pemicu dan konstruksi lainnya juga. Berikut adalah pola sederhana yang dapat Anda gunakan dengan pengaturan kode-pertama Anda, meskipun diakui itu bukan basis data agnostik:

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

Opsi lain adalah jika model domain Anda adalah satu-satunya metode memasukkan / memperbarui data ke dalam basis data Anda, Anda dapat mengimplementasikan sendiri persyaratan keunikan dan membiarkan basis data keluar darinya. Ini adalah solusi yang lebih portabel dan memaksa Anda untuk lebih jelas tentang aturan bisnis Anda dalam kode Anda, tetapi membiarkan database Anda terbuka untuk data yang tidak valid mendapatkan pintu belakang.

mattmc3
sumber
Saya suka DB saya sekencang drum, logikanya direplikasi di lapisan bisnis. Jawaban Anda hanya berfungsi dengan CTP4 tetapi membuat saya berada di jalur yang benar, saya telah memberikan solusi yang kompatibel dengan CTP5 di bawah pertanyaan awal saya. Terima kasih banyak!
kim3er
23
Kecuali jika aplikasi Anda adalah pengguna tunggal, saya percaya kendala unik adalah satu hal yang tidak dapat Anda terapkan hanya dengan kode. Anda dapat secara dramatis mengurangi kemungkinan pelanggaran dalam kode (dengan memeriksa keunikan sebelum menelepon SaveChanges()), tetapi masih ada kemungkinan penyisipan / pembaruan lainnya tergelincir di antara waktu pemeriksaan keunikan dan waktu SaveChanges(). Jadi, tergantung pada seberapa kritis misi aplikasi tersebut dan kemungkinan pelanggaran keunikan, mungkin yang terbaik adalah menambahkan kendala ke database.
devuxer
1
Anda harus memeriksa keunikan Anda untuk menjadi bagian dari transaksi yang sama dengan SaveChanges Anda. Dengan asumsi database Anda sesuai dengan asam Anda harus benar-benar dapat menegakkan keunikan dengan cara ini. Sekarang apakah EF memungkinkan Anda untuk mengelola siklus transaksi dengan baik dengan cara ini adalah pertanyaan lain.
mattmc3
1
@ mattmc3 Tergantung pada tingkat isolasi transaksi Anda. Hanya serializable isolation level(atau penguncian tabel khusus, ugh) yang benar-benar memungkinkan Anda untuk menjamin keunikan dalam kode Anda. Tetapi kebanyakan orang tidak menggunakan serializable isolation levelkarena alasan kinerja. Default di MS Sql Server adalah read committed. Lihat seri 4 bagian mulai dari: michaeljswart.com/2010/03/...
Nathan
3
EntityFramework 6.1.0 memiliki dukungan untuk IndexAttribute sekarang yang pada dasarnya Anda dapat menambahkannya di atas properti.
sotn
45

Mulai dengan EF 6.1 sekarang dimungkinkan:

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

Ini akan membuat Anda mendapatkan indeks yang unik alih-alih kendala unik, secara tegas. Untuk tujuan paling praktis, keduanya sama .

Mihkel Müür
sumber
5
@Dave: cukup gunakan nama indeks yang sama pada atribut masing-masing properti ( sumber ).
Mihkel Müür
Perhatikan bahwa ini membuat indeks yang unik dan bukan dari kendala unik . Meskipun hampir sama, mereka tidak persis sama (seperti yang saya pahami kendala unik dapat digunakan sebagai target FK). Untuk kendala, Anda perlu menjalankan SQL.
Richard
(Mengikuti komentar terakhir) Sumber-sumber lain menyarankan pembatasan ini telah dihapus di versi yang lebih baru dari SQL Server ... tetapi BOL tidak sepenuhnya konsisten.
Richard
@ Richard: kendala unik berbasis atribut juga mungkin (lihat jawaban kedua saya ), meskipun tidak keluar dari kotak.
Mihkel Müür
1
@exSnake: Sejak SQL Server 2008, indeks unik mendukung nilai NULL tunggal per kolom secara default. Jika diperlukan dukungan untuk beberapa NULL, indeks yang difilter akan diperlukan untuk melihat pertanyaan lain .
Mihkel Müür
28

Tidak benar-benar terkait dengan ini tetapi mungkin membantu dalam beberapa kasus.

Jika Anda ingin membuat indeks komposit unik di misalkan 2 kolom yang akan bertindak sebagai kendala untuk tabel Anda, maka pada versi 4.3 Anda dapat menggunakan mekanisme migrasi baru untuk mencapainya:

Pada dasarnya Anda perlu memasukkan panggilan seperti ini di salah satu skrip migrasi Anda:

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

Sesuatu seperti itu:

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
sumber
Senang melihat EF mendapat migrasi gaya Rails. Sekarang seandainya saya bisa menjalankannya di Mono.
kim3er
2
Bukankah seharusnya Anda juga memiliki prosedur DropIndex dalam Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg
5

Saya melakukan hack lengkap untuk mengeksekusi SQL ketika database sedang dibuat. Saya membuat DatabaseInitializer saya sendiri dan mewarisi dari salah satu initializers yang disediakan.

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

Itulah satu-satunya tempat yang bisa saya temukan dalam pernyataan SQL saya.

Ini dari CTP4. Saya tidak tahu cara kerjanya di CTP5.

Kelly Ethridge
sumber
Kelly terima kasih! Saya tidak mengetahui event handler itu. Solusi akhirnya saya menempatkan SQL dalam metode InitializeDatabase.
kim3er
5

Hanya mencoba mencari tahu apakah ada cara untuk melakukan ini, satu-satunya cara yang saya temukan sejauh ini adalah menegakkannya sendiri, saya membuat atribut untuk ditambahkan ke setiap kelas di mana Anda memberikan nama bidang yang Anda harus unik:

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

Kemudian di kelas saya, saya akan menambahkannya:

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

Akhirnya, saya akan menambahkan metode di repositori saya, di metode Tambahkan atau ketika Menyimpan Perubahan seperti ini:

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

Tidak terlalu baik karena kita perlu mengandalkan refleksi tetapi sejauh ini adalah pendekatan yang cocok untuk saya! = D

Rosendo
sumber
5

Juga di 6.1 Anda dapat menggunakan versi sintaks yang lancar dari jawaban @ mihkelmuur seperti:

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

Metode fasih bukan IMO sempurna tetapi setidaknya mungkin sekarang.

Lebih banyak deet di blog Arthur Vickers http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

Tidak dicintai
sumber
4

Cara mudah dalam visual basic menggunakan Migrasi Kode Pertama EF5

Sampel Kelas Publik

    Public Property SampleId As Integer

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

    Public Property Code() As String

Kelas Akhir

Atribut MaxLength sangat penting untuk indeks unik tipe string

Jalankan cmd: perbarui-database -verbose

setelah jalankan cmd: add-migrasi 1

dalam file yang dihasilkan

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
sumber
Ini sebenarnya jawaban yang lebih tepat daripada penginisialisasi DB kustom.
Shaun Wilson
4

Mirip dengan jawaban Tobias Schittkowski tetapi C # dan memiliki kemampuan untuk memiliki beberapa bidang dalam kendala.

Untuk menggunakan ini, cukup tempatkan [Unik] di bidang apa pun yang Anda ingin menjadi unik. Untuk string, Anda harus melakukan sesuatu seperti (perhatikan atribut MaxLength):

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

karena bidang string default adalah nvarchar (maks) dan itu tidak akan diizinkan dalam kunci.

Untuk beberapa bidang dalam batasan yang dapat Anda lakukan:

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

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

Kemudian, sertakan ekstensi yang berguna untuk mendapatkan nama tabel database dari suatu jenis:

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

Kemudian, penginisialisasi basis data:

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

Saya memecahkan masalah dengan refleksi (maaf, teman-teman, VB.Net ...)

Pertama, tentukan atribut UniqueAttribute:

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

End Class

Kemudian, tingkatkan model Anda seperti

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

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

End Class

Akhirnya, buat DatabaseInitializer kustom (Dalam versi saya, saya membuat kembali perubahan DB pada DB hanya jika dalam mode debug ...). Di DatabaseInitializer ini, indeks secara otomatis dibuat berdasarkan Atribut-Unik:

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

Mungkin ini membantu ...

Tobias Schittkowski
sumber
1

Jika Anda mengganti metode ValidateEntity di kelas DbContext Anda, Anda bisa meletakkan logika di sana juga. Keuntungannya di sini adalah Anda akan memiliki akses penuh ke semua DbSets Anda. Ini sebuah contoh:

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
sumber
1

Jika Anda menggunakan EF5 dan masih memiliki pertanyaan ini, solusi di bawah ini menyelesaikannya untuk saya.

Saya menggunakan pendekatan kode pertama, oleh karena itu menempatkan:

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

dalam skrip migrasi melakukan pekerjaan dengan baik. Ini juga memungkinkan nilai NULL!

FDIM
sumber
1

Dengan pendekatan EF Code First, seseorang dapat mengimplementasikan dukungan kendala unik berbasis atribut menggunakan teknik berikut.

Buat atribut marker

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

Tandai properti yang Anda ingin menjadi unik pada entitas, misalnya

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

Buat penginisialisasi basis data atau gunakan yang sudah ada untuk membuat batasan unik

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

Setel konteks basis data Anda untuk menggunakan penginisialisasi ini dalam kode startup (misalnya dalam main()atau Application_Start())

Database.SetInitializer(new DbInitializer());

Solusi mirip dengan mheyman, dengan penyederhanaan tidak mendukung kunci komposit. Untuk digunakan dengan EF 5.0+.

Mihkel Müür
sumber
1

Solusi 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
sumber
0

Saya menghadapi masalah itu hari ini dan akhirnya saya bisa menyelesaikannya. Saya tidak tahu apakah ini pendekatan yang tepat tetapi setidaknya saya bisa terus:

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
sumber
0

Gunakan validator properti unik.

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

ValidateEntitytidak dipanggil dalam transaksi basis data yang sama. Oleh karena itu, mungkin ada kondisi lomba dengan entitas lain dalam database. Anda harus meretas EF untuk memaksa transaksi di sekitar SaveChanges(dan karenanya, ValidateEntity). DBContexttidak dapat membuka koneksi secara langsung, tetapi ObjectContextbisa.

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

Setelah membaca pertanyaan ini, saya memiliki pertanyaan saya sendiri dalam proses mencoba menerapkan atribut untuk menentukan properti sebagai kunci unik seperti Mihkel Müür , Tobias Schittkowski, dan jawaban mheyman menyarankan: Map Entity Framework kode properti ke kolom basis data (CSpace ke SSpace)

Saya akhirnya tiba pada jawaban ini yang dapat memetakan properti skalar dan navigasi ke kolom database dan membuat indeks unik dalam urutan tertentu yang ditunjuk pada atribut. Kode ini mengasumsikan Anda telah mengimplementasikan UniqueAttribute dengan properti Sequence, dan menerapkannya pada properti kelas entitas EF yang harus mewakili kunci unik entitas (selain kunci utama).

Catatan: Kode ini bergantung pada EF versi 6.1 (atau lebih baru) yang mengekspos EntityContainerMappingtidak tersedia di versi sebelumnya.

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
sumber
0

Bagi mereka yang menggunakan konfigurasi kode pertama Anda juga dapat menggunakan objek IndexAttribute sebagai ColumnAnnotation dan mengatur properti IsUnique menjadi true.

Sebagai contoh:

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

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

Ini akan membuat indeks unik bernama IX_name pada kolom Nama.

Pascal Charbonneau
sumber
0

Maaf atas jawaban yang terlambat tetapi saya merasa senang jika harus bersamamu

Saya telah memposting tentang ini di proyek kode

Secara umum, itu tergantung pada atribut yang Anda letakkan di kelas untuk menghasilkan indeks unik Anda

Wahid Bitar
sumber