Eindeutige Einschränkung im Entity Framework-Code zuerst

125

Frage

Ist es möglich, eine eindeutige Einschränkung für eine Eigenschaft mithilfe der fließenden Syntax oder eines Attributs zu definieren? Wenn nicht, wie lauten die Problemumgehungen?

Ich habe eine Benutzerklasse mit einem Primärschlüssel, möchte aber sicherstellen, dass die E-Mail-Adresse auch eindeutig ist. Ist dies möglich, ohne die Datenbank direkt zu bearbeiten?

Lösung (basierend auf Matts Antwort)

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
quelle
1
Beachten Sie, dass Ihre App dadurch nur auf Datenbanken beschränkt wird, die genau diese Syntax akzeptieren - in diesem Fall SQL Server. Wenn Sie Ihre App bei einem Oracle-Anbieter ausführen, schlägt dies fehl.
DamienG
1
In dieser Situation müsste ich nur eine neue Initializer-Klasse erstellen, aber es ist ein gültiger Punkt.
Kim3er
3
Schauen Sie sich diesen Beitrag an: ValidationAttribute, das ein eindeutiges Feld anhand seiner Mitzeilen in der Datenbank validiert. Die Lösung zielt entweder auf ObjectContextoder ab DbContext.
Shimmy Weitzhandler
Ja, es wird jetzt seit EF 6.1 unterstützt .
Evandro Pomatti

Antworten:

61

Soweit ich das beurteilen kann, gibt es derzeit mit Entity Framework keine Möglichkeit, dies zu tun. Dies ist jedoch nicht nur ein Problem mit eindeutigen Einschränkungen. Möglicherweise möchten Sie Indizes erstellen, Einschränkungen überprüfen und möglicherweise auch Trigger und andere Konstrukte. Hier ist ein einfaches Muster, das Sie mit Ihrem Code-First-Setup verwenden können, obwohl es zugegebenermaßen nicht datenbankunabhängig ist:

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

Eine andere Option ist, wenn Ihr Domänenmodell die einzige Methode zum Einfügen / Aktualisieren von Daten in Ihre Datenbank ist, können Sie die Eindeutigkeitsanforderung selbst implementieren und die Datenbank aus dieser Datenbank herauslassen. Dies ist eine portablere Lösung und zwingt Sie dazu, Ihre Geschäftsregeln in Ihrem Code klar zu definieren, lässt Ihre Datenbank jedoch offen, damit ungültige Daten rückgängig gemacht werden.

mattmc3
quelle
Ich mag es, wenn meine Datenbank so eng wie eine Trommel ist, die Logik wird in der Geschäftsschicht repliziert. Ihre Antwort funktioniert nur mit CTP4, hat mich aber auf den richtigen Weg gebracht. Ich habe eine Lösung bereitgestellt, die mit CTP5 kompatibel ist, unterhalb meiner ursprünglichen Frage. Vielen Dank!
Kim3er
23
Wenn Ihre App kein Einzelbenutzer ist, ist eine eindeutige Einschränkung meines Erachtens eine Sache, die Sie mit Code allein nicht durchsetzen können. Sie können die Wahrscheinlichkeit einer Verletzung des Codes drastisch reduzieren (indem Sie die Eindeutigkeit vor dem Aufruf überprüfen SaveChanges()), aber es besteht immer noch die Möglichkeit, dass zwischen dem Zeitpunkt der Eindeutigkeitsprüfung und dem Zeitpunkt von ein weiteres Einfügen / Aktualisieren abrutscht SaveChanges(). Abhängig davon, wie geschäftskritisch die App ist und wie wahrscheinlich eine Verletzung der Eindeutigkeit ist, ist es wahrscheinlich am besten, die Einschränkung zur Datenbank hinzuzufügen.
Devuxer
1
Ihr Scheck auf Eindeutigkeit muss Teil derselben Transaktion sein wie Ihre SaveChanges. Angenommen, Ihre Datenbank ist säurekonform, sollten Sie auf diese Weise unbedingt die Eindeutigkeit erzwingen können. Ob Sie mit EF den Transaktionslebenszyklus auf diese Weise ordnungsgemäß verwalten können, ist eine andere Frage.
Mattmc3
1
@ mattmc3 Dies hängt von Ihrer Transaktionsisolationsstufe ab. Nur die serializable isolation level(oder benutzerdefinierte Tabellensperre, ugh) würde es Ihnen tatsächlich ermöglichen, die Eindeutigkeit Ihres Codes zu gewährleisten. Aber die meisten Leute benutzen das serializable isolation levelaus Leistungsgründen nicht. Der Standardwert in MS SQL Server ist read committed. Siehe die 4-teilige Serie ab: michaeljswart.com/2010/03/…
Nathan
3
EntityFramework 6.1.0 unterstützt jetzt IndexAttribute, das Sie grundsätzlich zusätzlich zu den Eigenschaften hinzufügen können.
SOTN
45

Ab EF 6.1 ist es nun möglich:

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

Auf diese Weise erhalten Sie streng genommen einen eindeutigen Index anstelle einer eindeutigen Einschränkung. Für die meisten praktischen Zwecke sind sie gleich .

Mihkel Müür
quelle
5
@ Dave: Verwenden Sie einfach denselben Indexnamen für die Attribute der jeweiligen Eigenschaften ( Quelle ).
Mihkel Müür
Beachten Sie, dass dadurch ein eindeutiger Index und kein eindeutiger Kontraint erstellt wird . Obwohl sie fast gleich sind, sind sie nicht ganz gleich (so wie ich es verstehe, können eindeutige Einschränkungen als Ziel eines FK verwendet werden). Für eine Einschränkung müssen Sie SQL ausführen.
Richard
(Nach dem letzten Kommentar) Andere Quellen schlagen vor, dass diese Einschränkung in neueren Versionen von SQL Server aufgehoben wurde ... aber BOL ist nicht vollständig konsistent.
Richard
@Richard: Attributbasierte eindeutige Einschränkungen sind ebenfalls möglich (siehe meine zweite Antwort ), jedoch nicht sofort einsatzbereit .
Mihkel Müür
1
@exSnake: Seit SQL Server 2008 unterstützt der eindeutige Index standardmäßig einen einzelnen NULL-Wert pro Spalte. Falls die Unterstützung mehrerer NULL-Werte erforderlich ist, wird ein gefilterter Index benötigt, siehe eine andere Frage .
Mihkel Müür
28

Nicht wirklich damit verbunden, aber es könnte in einigen Fällen helfen.

Wenn Sie einen eindeutigen zusammengesetzten Index für beispielsweise zwei Spalten erstellen möchten, die als Einschränkung für Ihre Tabelle dienen, können Sie ab Version 4.3 den neuen Migrationsmechanismus verwenden, um dies zu erreichen:

Grundsätzlich müssen Sie einen solchen Aufruf in eines Ihrer Migrationsskripte einfügen:

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

Sowas in der Art:

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
quelle
Schön zu sehen, dass EF Migrationen im Rails-Stil hat. Wenn ich es nur auf Mono laufen lassen könnte.
Kim3er
2
Sollten Sie nicht auch einen DropIndex in der Down () -Prozedur haben? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg
5

Ich mache einen vollständigen Hack, um SQL auszuführen, wenn die Datenbank erstellt wird. Ich erstelle meinen eigenen DatabaseInitializer und erbe von einem der bereitgestellten Initialisierer.

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

Dies ist der einzige Ort, an dem ich meine SQL-Anweisungen einklemmen kann.

Dies ist von CTP4. Ich weiß nicht, wie es in CTP5 funktioniert.

Kelly Ethridge
quelle
Danke Kelly! Dieser Event-Handler war mir nicht bekannt. Meine mögliche Lösung platziert das SQL in der InitializeDatabase-Methode.
Kim3er
5

Ich habe nur versucht herauszufinden, ob es eine Möglichkeit gibt, dies zu tun. Bisher habe ich es nur selbst durchgesetzt. Ich habe ein Attribut erstellt, das jeder Klasse hinzugefügt wird, in der Sie den Namen der Felder angeben, die eindeutig sein müssen:

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

Dann werde ich es in meiner Klasse hinzufügen:

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

Schließlich füge ich eine Methode in mein Repository ein, in die Add-Methode oder beim Speichern von Änderungen wie folgt:

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

Nicht allzu schön, da wir uns auf Reflexion verlassen müssen, aber dies ist der Ansatz, der für mich funktioniert! = D.

Rosendo
quelle
5

Auch in 6.1 können Sie die fließende Syntaxversion von @ mihkelmuurs Antwort wie folgt verwenden:

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

Die fließende Methode ist IMO nicht perfekt, aber zumindest jetzt möglich.

Weitere Details auf Arthur Vickers Blog http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

Nicht geliebt
quelle
4

Ein einfacher Weg in Visual Basic mit EF5 Code First Migrations

Public Class Sample

    Public Property SampleId As Integer

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

    Public Property Code() As String

Klasse beenden

Das Attribut MaxLength ist sehr wichtig für einen eindeutigen Index des Zeichenfolgentyps

Führen Sie cmd: update-database -verbose aus

Nach dem Ausführen von cmd: add-migration 1

in der generierten Datei

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
quelle
Dies ist tatsächlich eine geeignetere Antwort als ein benutzerdefinierter DB-Initialisierer.
Shaun Wilson
4

Ähnlich wie Tobias Schittkowskis Antwort, aber C # und hat die Fähigkeit, mehrere Felder in den Constrtaints zu haben.

Um dies zu verwenden, platzieren Sie einfach ein [Einzigartig] auf einem beliebigen Feld, das Sie eindeutig sein möchten. Für Zeichenfolgen müssen Sie Folgendes tun (beachten Sie das MaxLength-Attribut):

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

weil das Standardzeichenfolgenfeld nvarchar (max) ist und dies in einem Schlüssel nicht zulässig ist.

Für mehrere Felder in der Einschränkung können Sie Folgendes tun:

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

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

Fügen Sie dann eine nützliche Erweiterung hinzu, um den Namen der Datenbanktabelle von einem Typ abzurufen:

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

Dann der Datenbankinitialisierer:

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

Ich habe das Problem durch Nachdenken gelöst (sorry, Leute, VB.Net ...)

Definieren Sie zunächst ein Attribut UniqueAttribute:

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

End Class

Dann verbessern Sie Ihr Modell wie

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

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

End Class

Erstellen Sie abschließend einen benutzerdefinierten DatabaseInitializer (In meiner Version erstelle ich die DB bei DB-Änderungen nur im Debug-Modus neu ...). In diesem DatabaseInitializer werden die Indizes automatisch basierend auf den eindeutigen Attributen erstellt:

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

Vielleicht hilft das ...

Tobias Schittkowski
quelle
1

Wenn Sie die ValidateEntity-Methode in Ihrer DbContext-Klasse überschreiben, können Sie die Logik auch dort ablegen. Der Vorteil hierbei ist, dass Sie vollen Zugriff auf alle Ihre DbSets haben. Hier ist ein Beispiel:

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

Wenn Sie EF5 verwenden und diese Frage immer noch haben, hat die folgende Lösung sie für mich gelöst.

Ich verwende den Code First-Ansatz und setze daher Folgendes ein:

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

im Migrationsskript hat die Arbeit gut gemacht. Es erlaubt auch NULL-Werte!

FDIM
quelle
1

Mit dem EF Code First-Ansatz kann die Unterstützung von Attribut-basierten eindeutigen Einschränkungen mithilfe der folgenden Technik implementiert werden.

Erstellen Sie ein Markerattribut

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

Markieren Sie Eigenschaften, die für Entitäten eindeutig sein sollen, z

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

Erstellen Sie einen Datenbankinitialisierer oder verwenden Sie einen vorhandenen, um die eindeutigen Einschränkungen zu erstellen

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

Stellen Sie Ihren Datenbankkontext so ein, dass dieser Initialisierer im Startcode verwendet wird (z. B. in main()oder Application_Start()).

Database.SetInitializer(new DbInitializer());

Die Lösung ähnelt der von mheyman, mit der Vereinfachung, dass zusammengesetzte Schlüssel nicht unterstützt werden. Zur Verwendung mit EF 5.0+.

Mihkel Müür
quelle
1

Fließende Api-Lösung:

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

Ich habe mich heute diesem Problem gestellt und konnte es endlich lösen. Ich weiß nicht, ob es ein richtiger Ansatz ist, aber zumindest kann ich weitermachen:

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

Verwenden Sie einen eindeutigen Eigenschaftsprüfer.

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

ValidateEntitywird nicht innerhalb derselben Datenbanktransaktion aufgerufen. Daher kann es zu Rennbedingungen mit anderen Entitäten in der Datenbank kommen. Sie müssen EF etwas hacken, um eine Transaktion um das SaveChanges(und daher ValidateEntity) zu erzwingen . DBContextkann die Verbindung nicht direkt öffnen, ObjectContextkann aber .

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

Nachdem ich diese Frage gelesen hatte, hatte ich meine eigene Frage beim Versuch, ein Attribut zum Festlegen von Eigenschaften als eindeutige Schlüssel zu implementieren, wie die Antworten von Mihkel Müür , Tobias Schittkowski und mheyman : Zuordnen von Entity Framework-Codeeigenschaften zu Datenbankspalten (CSpace to SSpace)

Endlich bin ich zu dieser Antwort gekommen, die sowohl Skalar- als auch Navigationseigenschaften auf Datenbankspalten abbilden und einen eindeutigen Index in einer bestimmten Reihenfolge erstellen kann, die für das Attribut festgelegt ist. In diesem Code wird davon ausgegangen, dass Sie ein UniqueAttribute mit einer Sequence-Eigenschaft implementiert und auf EF-Entitätsklasseneigenschaften angewendet haben, die den eindeutigen Schlüssel der Entität (außer dem Primärschlüssel) darstellen sollen.

Hinweis: Dieser Code basiert auf EF Version 6.1 (oder höher), die EntityContainerMappingin früheren Versionen nicht verfügbar ist.

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

Für Benutzer, die Code-First-Konfigurationen verwenden, können Sie das IndexAttribute-Objekt auch als ColumnAnnotation verwenden und seine IsUnique-Eigenschaft auf true setzen.

Zum Beispiel:

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

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

Dadurch wird ein eindeutiger Index mit dem Namen IX_name in der Spalte Name erstellt.

Pascal Charbonneau
quelle
0

Entschuldigung für die späte Antwort, aber ich fand es gut, sie mit dir zu teilen

Ich habe darüber bei Code Project gepostet

Im Allgemeinen hängt es von den Attributen ab, die Sie den Klassen zuweisen, um Ihre eindeutigen Indizes zu generieren

Wahid Bitar
quelle