Vincolo univoco nel codice di Entity Framework per primo


125

Domanda

È possibile definire un vincolo univoco su una proprietà utilizzando la sintassi fluida o un attributo? In caso contrario, quali sono le soluzioni alternative?

Ho una classe utente con una chiave primaria, ma vorrei assicurarmi che anche l'indirizzo e-mail sia univoco. È possibile senza modificare direttamente il database?

Soluzione (basata sulla risposta di 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)");
            }
        }
    }
}

1
Tieni presente che ciò limita la tua app solo ai database che accettano quella sintassi esatta, in questo caso SQL Server. Se esegui la tua app con un provider Oracle, fallirà.
DamienG,

1
In quella situazione avrei solo bisogno di creare una nuova classe Initializer, ma è un punto valido.
kim3er,

3
Dai un'occhiata a questo post: ValidationAttribute che convalida un campo univoco rispetto alle sue righe nel database , la soluzione ha come target ObjectContexto DbContext.
Shimmy Weitzhandler,

Sì, ora è supportato da EF 6.1 .
Evandro Pomatti,

Risposte:


61

Per quanto ne so, al momento non è possibile farlo con Entity Framework. Tuttavia, questo non è solo un problema con vincoli univoci ... potresti voler creare indici, verificare vincoli e possibilmente anche trigger e altri costrutti. Ecco un semplice schema che puoi usare con la tua configurazione in codice, sebbene sia vero che non è indipendente dal database:

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

Un'altra opzione è se il tuo modello di dominio è l'unico metodo di inserimento / aggiornamento dei dati nel tuo database, potresti implementare tu stesso il requisito di unicità e lasciarne il database. Questa è una soluzione più portatile e ti costringe a essere chiaro sulle regole aziendali nel tuo codice, ma lascia il tuo database aperto a dati non validi che vengono sottoposti a back-doored.


Mi piace che il mio DB sia stretto come un tamburo, la logica viene replicata nel livello aziendale. La tua risposta funziona solo con CTP4 ma mi ha portato sulla strada giusta, ho fornito una soluzione compatibile con CTP5 sotto la mia domanda originale. Molte grazie!
Kim3er,

23
A meno che la tua app non sia un utente singolo, credo che un vincolo unico sia una cosa che non puoi applicare con il solo codice. Puoi ridurre drasticamente la probabilità di una violazione del codice (controllando l'unicità prima della chiamata SaveChanges()), ma c'è ancora la possibilità che un altro inserto / aggiornamento scivoli tra il tempo del controllo di unicità e il tempo di SaveChanges(). Pertanto, a seconda della mission dell'app e della probabilità di una violazione dell'unicità, è probabilmente meglio aggiungere il vincolo al database.
devuxer

1
Dovresti avere il tuo assegno per unicità essere parte della stessa transazione dei tuoi SaveChanges. Supponendo che il tuo database sia compatibile con l'acido, dovresti assolutamente essere in grado di applicare l'unicità in questo modo. Ora se EF ti consente di gestire correttamente il ciclo di vita delle transazioni in questo modo è un'altra domanda.
mattmc3,

1
@ mattmc3 Dipende dal livello di isolamento della transazione. Solo il serializable isolation level(o il blocco personalizzato della tabella, ugh) in realtà ti permetterebbe di garantire unicità nel tuo codice. Ma la maggior parte delle persone non lo usa serializable isolation levelper motivi di prestazioni. L'impostazione predefinita in MS Sql Server è read committed. Guarda le 4 serie a partire da: michaeljswart.com/2010/03/…
Nathan

3
EntityFramework 6.1.0 ha ora il supporto per IndexAttribute che puoi praticamente aggiungere in cima alle proprietà.
Dal

45

A partire da EF 6.1 è ora possibile:

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

Questo ti darà un indice univoco anziché un vincolo univoco, a rigor di termini. Per la maggior parte degli scopi pratici sono gli stessi .


5
@Dave: basta usare lo stesso nome indice sugli attributi delle rispettive proprietà ( sorgente ).
Mihkel Müür,

Nota che questo crea un indice univoco piuttosto che un contrappunto univoco . Sebbene quasi uguali, non sono affatto uguali (come ho capito, vincoli unici possono essere usati come bersaglio di un FK). Per un vincolo è necessario eseguire SQL.
Richard,

(Dopo l'ultimo commento) Altre fonti suggeriscono che questa limitazione è stata rimossa nelle versioni più recenti di SQL Server ... ma BOL non è completamente coerente.
Richard,

@Richard: sono possibili anche vincoli univoci basati sugli attributi (vedi la mia seconda risposta ), anche se non pronti all'uso.
Mihkel Müür

1
@exSnake: da SQL Server 2008, l'indice univoco supporta un singolo valore NULL per colonna per impostazione predefinita. Nel caso in cui sia necessario il supporto per più NULL, sarebbe necessario un indice filtrato per vedere un'altra domanda .
Mihkel Müür

28

Non proprio correlato a questo, ma potrebbe aiutare in alcuni casi.

Se stai cercando di creare un indice composito univoco su diciamo 2 colonne che fungeranno da vincolo per la tua tabella, quindi dalla versione 4.3 puoi usare il nuovo meccanismo di migrazione per raggiungerlo:

Fondamentalmente devi inserire una chiamata come questa in uno dei tuoi script di migrazione:

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

Qualcosa del genere:

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

È bello vedere che EF ha migrazioni in stile Rails. Ora, se solo potessi eseguirlo su Mono.
Kim3er,

2
Non dovresti avere anche DropIndex nella procedura Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg,

5

Faccio un hack completo per far eseguire SQL durante la creazione del database. Creo il mio DatabaseInitializer ed eredito da uno degli inizializzatori forniti.

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

Questo è l'unico posto che sono riuscito a trovare nelle mie istruzioni SQL.

Questo è da CTP4. Non so come funzioni in CTP5.


Grazie Kelly! Non ero a conoscenza di quel gestore di eventi. La mia eventuale soluzione inserisce l'SQL nel metodo InitializeDatabase.
kim3er,

5

Stavo solo cercando di scoprire se c'era un modo per farlo, l'unico modo che ho scoperto finora era applicarlo da solo, ho creato un attributo da aggiungere a ogni classe in cui fornisci il nome dei campi che devi essere univoco:

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

Quindi nella mia classe lo aggiungerò:

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

Infine, aggiungerò un metodo nel mio repository, nel metodo Aggiungi o durante il salvataggio delle modifiche in questo modo:

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

Non troppo bello perché dobbiamo fare affidamento sulla riflessione, ma finora questo è l'approccio che funziona per me! = D



4

Un modo semplice in Visual Basic usando il codice EF5 First Migrations

Esempio di classe pubblica

    Public Property SampleId As Integer

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

    Public Property Code() As String

End Class

L'attributo MaxLength è molto importante per l'indice univoco di tipo stringa

Esegui cmd: update-database -verbose

dopo l'esecuzione cmd: add-migration 1

nel file generato

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

Questa è in realtà una risposta più appropriata di un inizializzatore DB personalizzato.
Shaun Wilson,

4

Simile alla risposta di Tobias Schittkowski ma C # e ha la capacità di avere più campi nelle strutture.

Per usarlo, basta posizionare un [Unico] su qualsiasi campo che desideri essere unico. Per le stringhe, dovrai fare qualcosa del genere (nota l'attributo MaxLength):

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

perché il campo stringa predefinito è nvarchar (max) e non sarà consentito in una chiave.

Per più campi nel vincolo è possibile eseguire:

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

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

Quindi, includere un'estensione utile per ottenere il nome della tabella del database da un tipo:

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

Quindi, l'inizializzatore del database:

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

Ho risolto il problema con la riflessione (scusate, gente, VB.Net ...)

Innanzitutto, definire un attributo UniqueAttribute:

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

End Class

Quindi, migliora il tuo modello come

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

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

End Class

Infine, crea un DatabaseInitializer personalizzato (nella mia versione, ricreare le modifiche DB su DB solo se in modalità debug ...). In questo DatabaseInitializer, gli indici vengono creati automaticamente in base agli attributi univoci:

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

Forse questo aiuta ...


1

Se si sovrascrive il metodo ValidateEntity nella classe DbContext, è possibile inserire anche la logica. Il vantaggio qui è che avrai pieno accesso a tutti i tuoi DbSet. Ecco un esempio:

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

Se stai usando EF5 e hai ancora questa domanda, la soluzione qui sotto l'ha risolta per me.

Sto usando il primo approccio al codice, quindi inserendo:

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

nello script di migrazione ha fatto bene il lavoro. Permette anche valori NULL!


1

Con l'approccio EF Code First, è possibile implementare il supporto dei vincoli univoci basato sugli attributi utilizzando la seguente tecnica.

Crea un attributo marker

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

Contrassegna le proprietà che desideri siano univoche nelle entità, ad es

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

Creare un inizializzatore di database o utilizzarne uno esistente per creare vincoli univoci

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

Impostare il contesto del database per utilizzare questo inizializzatore nel codice di avvio (ad esempio in main()o Application_Start())

Database.SetInitializer(new DbInitializer());

La soluzione è simile a quella di mheyman, con una semplificazione di non supportare chiavi composite. Da utilizzare con EF 5.0+.


1

Ottima soluzione Api:

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

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

    entity.HasIndex(e => e.Email)
          .HasName("IX_Email")
          .IsUnique();
});

0

Ho affrontato quel problema oggi e finalmente sono stato in grado di risolverlo. Non so se sia un approccio giusto ma almeno posso continuare:

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

Utilizzare un validatore di proprietà univoco.

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

ValidateEntitynon viene chiamato all'interno della stessa transazione del database. Pertanto, potrebbero esserci condizioni di competizione con altre entità nel database. Devi hackerare un po 'EF per forzare una transazione attorno al SaveChanges(e quindi, ValidateEntity). DBContextimpossibile aprire direttamente la connessione, ma è ObjectContextpossibile.

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


0

Dopo aver letto questa domanda, ho avuto la mia domanda nel tentativo di implementare un attributo per designare le proprietà come chiavi uniche come Mihkel Müür , Tobias Schittkowski e le risposte di mheyman suggeriscono: Mappare le proprietà del codice di Entity Framework nelle colonne del database (da CSpace a SSpace)

Sono finalmente arrivato a questa risposta che può mappare le proprietà scalari e di navigazione fino alle colonne del database e creare un indice univoco in una sequenza specifica designata sull'attributo. Questo codice presuppone che tu abbia implementato un UniqueAttribute con una proprietà Sequence e lo abbia applicato alle proprietà della classe di entità EF che dovrebbero rappresentare la chiave univoca dell'entità (diversa dalla chiave primaria).

Nota: questo codice si basa su EF versione 6.1 (o successive) che EntityContainerMappingnon è disponibile nelle versioni precedenti.

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

Per coloro che utilizzano le prime configurazioni di codice, è anche possibile utilizzare l'oggetto IndexAttribute come ColumnAnnotation e impostare la proprietà IsUnique su true.

Per esempio:

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

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

Ciò creerà un indice univoco denominato IX_name nella colonna Nome.


0

Ci scusiamo per la risposta tardiva, ma ho trovato bello raderlo con te

Ho pubblicato su questo al progetto di codice

In generale, dipende dagli attributi che inserisci nelle classi per generare i tuoi indici univoci

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.