Contrainte unique dans le code Entity Framework First

125

Question

Est-il possible de définir une contrainte unique sur une propriété en utilisant soit la syntaxe fluide, soit un attribut? Sinon, quelles sont les solutions de contournement?

J'ai une classe d'utilisateurs avec une clé primaire, mais je voudrais m'assurer que l'adresse e-mail est également unique. Est-ce possible sans modifier directement la base de données?

Solution (basée sur la réponse de 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
la source
1
Gardez à l'esprit que cela limite votre application aux seules bases de données qui acceptent cette syntaxe exacte - dans ce cas SQL Server. Si vous exécutez votre application avec un fournisseur Oracle, elle échouera.
DamienG
1
Dans cette situation, je n'aurais besoin que de créer une nouvelle classe d'initialisation, mais c'est un point valide.
kim3er
3
Consultez cet article: ValidationAttribute qui valide un champ unique par rapport à ses autres lignes dans la base de données , la solution cible soit ObjectContextou DbContext.
Shimmy Weitzhandler
Oui, il est désormais pris en charge depuis EF 6.1 .
Evandro Pomatti

Réponses:

61

Pour autant que je sache, il n'y a aucun moyen de le faire avec Entity Framework pour le moment. Cependant, ce n'est pas seulement un problème avec des contraintes uniques ... vous voudrez peut-être créer des index, vérifier des contraintes et éventuellement des déclencheurs et d'autres constructions. Voici un modèle simple que vous pouvez utiliser avec votre configuration code-first, bien qu'il ne soit certes pas indépendant de la base de données:

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

Une autre option est que si votre modèle de domaine est la seule méthode d'insertion / mise à jour de données dans votre base de données, vous pouvez implémenter vous-même l'exigence d'unicité et laisser la base de données en dehors. Il s'agit d'une solution plus portable et vous oblige à être clair sur vos règles métier dans votre code, mais laisse votre base de données ouverte aux données non valides qui sont back-doored.

mattmc3
la source
J'aime que ma base de données soit aussi serrée qu'un tambour, la logique est répliquée dans la couche métier. Votre réponse ne fonctionne qu'avec CTP4 mais m'a mis sur la bonne voie, j'ai fourni une solution compatible avec CTP5 ci-dessous ma question initiale. Merci beaucoup!
kim3er
23
À moins que votre application ne soit mono-utilisateur, je pense qu'une contrainte unique est une chose que vous ne pouvez pas appliquer avec du code seul. Vous pouvez réduire considérablement la probabilité d'une violation dans le code (en vérifiant l'unicité avant d'appeler SaveChanges()), mais il est toujours possible qu'une autre insertion / mise à jour se glisse entre le moment de la vérification d'unicité et l'heure de SaveChanges(). Ainsi, en fonction de l'importance de la mission de l'application et de la probabilité d'une violation d'unicité, il est probablement préférable d'ajouter la contrainte à la base de données.
devuxer
1
Vous devrez faire en sorte que votre chèque d'unicité fasse partie de la même transaction que vos SaveChanges. En supposant que votre base de données soit conforme à l'acide, vous devriez absolument être en mesure d'appliquer l'unicité de cette façon. Maintenant, si EF vous permet de gérer correctement le cycle de vie des transactions de cette manière est une autre question.
mattmc3
1
@ mattmc3 Cela dépend de votre niveau d'isolement de transaction. Seul le serializable isolation level(ou le verrouillage de table personnalisé, ugh) vous permettrait en fait de garantir l'unicité de votre code. Mais la plupart des gens n'utilisent pas le serializable isolation levelpour des raisons de performances. La valeur par défaut dans MS Sql Server est read committed. Voir la série en 4 parties à partir de: michaeljswart.com/2010/03/…
Nathan
3
EntityFramework 6.1.0 prend maintenant en charge IndexAttribute que vous pouvez essentiellement l'ajouter au-dessus des propriétés.
sotn le
45

À partir d'EF 6.1, il est désormais possible:

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

Cela vous donnera un index unique au lieu d'une contrainte unique, à proprement parler. Pour la plupart des raisons pratiques, ce sont les mêmes .

Mihkel Müür
la source
5
@Dave: utilisez simplement le même nom d'index sur les attributs des propriétés respectives ( source ).
Mihkel Müür
Notez que cela crée un index unique plutôt qu'une contraint unique . Bien que presque identiques, elles ne sont pas tout à fait les mêmes (si je comprends bien, des contraintes uniques peuvent être utilisées comme cible d'un FK). Pour une contrainte, vous devez exécuter SQL.
Richard
(Suite au dernier commentaire) D'autres sources suggèrent que cette limitation a été supprimée dans les versions plus récentes de SQL Server ... mais BOL n'est pas complètement cohérent.
Richard
@Richard: des contraintes uniques basées sur les attributs sont également possibles (voir ma deuxième réponse ), mais pas hors de la boîte.
Mihkel Müür
1
@exSnake: depuis SQL Server 2008, l'index unique prend en charge une seule valeur NULL par colonne par défaut. Dans le cas où la prise en charge de plusieurs NULL est requise, un index filtré serait nécessaire, voir une autre question .
Mihkel Müür
28

Pas vraiment lié à cela mais cela pourrait aider dans certains cas.

Si vous cherchez à créer un index composite unique sur disons 2 colonnes qui agira comme une contrainte pour votre table, alors à partir de la version 4.3, vous pouvez utiliser le nouveau mécanisme de migration pour y parvenir:

En gros, vous devez insérer un appel comme celui-ci dans l'un de vos scripts de migration:

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

Quelque chose comme ca:

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
la source
C'est bien de voir que EF a des migrations de style Rails. Maintenant, si seulement je pouvais l'exécuter sur Mono.
kim3er
2
Ne devriez-vous pas également avoir un DropIndex dans la procédure Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg
5

Je fais un hack complet pour exécuter SQL lors de la création de la base de données. Je crée mon propre DatabaseInitializer et hérite de l'un des initialiseurs fournis.

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

C'est le seul endroit que j'ai pu trouver pour caler mes instructions SQL.

C'est de CTP4. Je ne sais pas comment cela fonctionne dans CTP5.

Kelly Ethridge
la source
Merci Kelly! Je n'étais pas au courant de ce gestionnaire d'événements. Ma solution éventuelle place le SQL dans la méthode InitializeDatabase.
kim3er
5

En essayant simplement de savoir s'il y avait un moyen de le faire, le seul moyen que j'ai trouvé jusqu'à présent était de l'appliquer moi-même, j'ai créé un attribut à ajouter à chaque classe où vous fournissez le nom des champs dont vous avez besoin pour être unique:

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

Puis dans ma classe je vais l'ajouter:

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

Enfin, j'ajouterai une méthode dans mon référentiel, dans la méthode Add ou lors de l'enregistrement des modifications comme ceci:

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

Pas trop gentil car il faut s'appuyer sur la réflexion mais c'est jusqu'à présent l'approche qui fonctionne pour moi! = D

Rosendo
la source
5

Également en 6.1, vous pouvez utiliser la version à syntaxe fluide de la réponse de @ mihkelmuur comme ceci:

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

La méthode fluide n'est pas parfaite IMO mais au moins c'est possible maintenant.

Plus de détails sur le blog Arthur Vickers http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

Pas aimé
la source
4

Un moyen simple dans Visual Basic à l'aide des migrations EF5 Code First

Exemple de classe publique

    Public Property SampleId As Integer

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

    Public Property Code() As String

Fin de classe

L'attribut MaxLength est très important pour un index unique de type chaîne

Exécutez cmd: update-database -verbose

après l'exécution de cmd: add-migration 1

dans le fichier généré

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
la source
C'est en fait une réponse plus appropriée qu'un initialiseur de base de données personnalisé.
Shaun Wilson
4

Similaire à la réponse de Tobias Schittkowski mais C # et a la capacité d'avoir plusieurs champs dans les constrtaints.

Pour l'utiliser, placez simplement un [Unique] sur n'importe quel champ que vous souhaitez être unique. Pour les chaînes, vous devrez faire quelque chose comme (notez l'attribut MaxLength):

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

car le champ de chaîne par défaut est nvarchar (max) et cela ne sera pas autorisé dans une clé.

Pour plusieurs champs de la contrainte, vous pouvez faire:

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

Tout d'abord, 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; }
}

Ensuite, incluez une extension utile pour obtenir le nom de la table de base de données à partir d'un type:

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

Ensuite, l'initialiseur de base de données:

/// <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
la source
2

J'ai résolu le problème par réflexion (désolé, les amis, VB.Net ...)

Tout d'abord, définissez un attribut UniqueAttribute:

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

End Class

Ensuite, améliorez votre modèle comme

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

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

End Class

Enfin, créez un DatabaseInitializer personnalisé (dans ma version, je recrée la base de données sur les modifications de la base de données uniquement si en mode débogage ...). Dans ce DatabaseInitializer, les index sont automatiquement créés en fonction des attributs uniques:

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

Peut-être que cela aide ...

Tobias Schittkowski
la source
1

Si vous substituez la méthode ValidateEntity dans votre classe DbContext, vous pouvez également y placer la logique. L'avantage ici est que vous aurez un accès complet à tous vos DbSets. Voici un exemple:

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
la source
1

Si vous utilisez EF5 et que vous avez toujours cette question, la solution ci-dessous l'a résolue pour moi.

J'utilise la première approche du code, mettant donc:

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

dans le script de migration a bien fait le travail. Il autorise également les valeurs NULL!

FDIM
la source
1

Avec l'approche EF Code First, on peut implémenter la prise en charge des contraintes uniques basées sur les attributs à l'aide de la technique suivante.

Créer un attribut de marqueur

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

Marquez les propriétés que vous souhaitez rendre uniques sur les entités, par exemple

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

Créez un initialiseur de base de données ou utilisez-en un existant pour créer les contraintes uniques

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

Définissez le contexte de votre base de données pour utiliser cet initialiseur dans le code de démarrage (par exemple dans main()ou Application_Start())

Database.SetInitializer(new DbInitializer());

La solution est similaire à celle de mheyman, avec une simplification de ne pas prendre en charge les clés composites. À utiliser avec EF 5.0+.

Mihkel Müür
la source
1

Solution 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
la source
0

J'ai fait face à ce problème aujourd'hui et j'ai finalement pu le résoudre. Je ne sais pas si c'est une bonne approche mais au moins je peux continuer:

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
la source
0

Utilisez un validateur de propriété unique.

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

ValidateEntityn'est pas appelée dans la même transaction de base de données. Par conséquent, il peut y avoir des conditions de concurrence avec d'autres entités de la base de données. Vous devez quelque peu pirater EF pour forcer une transaction autour du SaveChanges(et donc, ValidateEntity). DBContextne peut pas ouvrir la connexion directement, mais ObjectContextpeut.

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

Après avoir lu cette question, j'avais ma propre question en train d'essayer d'implémenter un attribut pour désigner des propriétés comme des clés uniques comme celles de Mihkel Müür , Tobias Schittkowski et mheyman suggèrent: mapper les propriétés du code Entity Framework aux colonnes de la base de données (CSpace vers SSpace)

Je suis enfin arrivé à cette réponse qui peut mapper les propriétés scalaires et de navigation vers les colonnes de la base de données et créer un index unique dans une séquence spécifique désignée sur l'attribut. Ce code suppose que vous avez implémenté un UniqueAttribute avec une propriété Sequence et que vous l'avez appliqué aux propriétés de classe d'entité EF qui doivent représenter la clé unique de l'entité (autre que la clé primaire).

Remarque: ce code repose sur EF version 6.1 (ou ultérieure) qui expose EntityContainerMappingnon disponible dans les versions antérieures.

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
la source
0

Pour ceux qui utilisent les premières configurations de code, vous pouvez également utiliser l'objet IndexAttribute comme ColumnAnnotation et définir sa propriété IsUnique sur true.

Par exemple:

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

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

Cela créera un index unique nommé IX_name dans la colonne Name.

Pascal Charbonneau
la source
0

Désolé pour la réponse tardive mais j'ai trouvé bon de le partager avec vous

J'ai posté à ce sujet au projet de code

En général, cela dépend des attributs que vous mettez sur les classes pour générer vos index uniques

Wahid Bitar
la source