129

質問

流暢な構文または属性を使用して、プロパティに一意の制約を定義することは可能ですか? そうでない場合、回避策は何ですか?

主キーを持つユーザー クラスがありますが、電子メール アドレスも一意であることを確認したいと考えています。データベースを直接編集せずにこれは可能ですか?

解決策(マットの回答に基づく)

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

19 に答える 19

62

私が知る限り、現時点では Entity Framework でこれを行う方法はありません。ただし、これは一意の制約だけの問題ではありません...インデックスを作成し、制約をチェックし、場合によってはトリガーやその他の構成も作成する必要があります。 コードファーストのセットアップで使用できる簡単なパターンを次に示しますが、データベースに依存しないことは確かです。

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

別のオプションは、ドメイン モデルがデータベースにデータを挿入/更新する唯一の方法である場合、一意性の要件を自分で実装し、データベースを除外することです。これはより移植性の高いソリューションであり、コード内のビジネス ルールを明確にする必要がありますが、データベースは無効なデータのバックドアにさらされる可能性があります。

于 2010-12-13T02:25:46.990 に答える
48

EF 6.1 以降では、次のことが可能になりました。

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

これにより、厳密に言えば、一意の制約ではなく一意のインデックスが取得されます。ほとんどの実用的な目的では、それらは同じです。

于 2014-05-26T16:59:50.600 に答える
28

これとはあまり関係ありませんが、場合によっては役立つかもしれません。

たとえば、テーブルの制約として機能する 2 つの列に一意の複合インデックスを作成する場合は、バージョン 4.3 以降、新しい移行メカニズムを使用してそれを実現できます。

基本的に、移行スクリプトの 1 つに次のような呼び出しを挿入する必要があります。

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

そんな感じ:

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" });
        }
    }
}
于 2012-04-20T19:15:13.880 に答える
5

これを行う方法があるかどうかを調べようとしましたが、これまでに見つけた唯一の方法は自分で強制することでした.一意にする必要があるフィールドの名前を指定する各クラスに追加する属性を作成しました:

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

次に、クラスに追加します。

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

最後に、リポジトリ、Add メソッド、または変更を保存するときに、次のようにメソッドを追加します。

private void ValidateDuplicatedKeys(T entity)
{
    var atts = typeof(T).GetCustomAttributes(typeof(UniqueAttribute), true);
    if (atts == null || atts.Count() < 1)
    {
        return;
    }
    foreach (var att in atts)
    {
        UniqueAttribute uniqueAtt = (UniqueAttribute)att;
        var newkeyValues = from pi in entity.GetType().GetProperties()
                            join k in uniqueAtt.KeyFields on pi.Name equals k
                            select new { KeyField = k, Value = pi.GetValue(entity, null).ToString() };
        foreach (var item in _objectSet)
        {
            var keyValues = from pi in item.GetType().GetProperties()
                            join k in uniqueAtt.KeyFields on pi.Name equals k
                            select new { KeyField = k, Value = pi.GetValue(item, null).ToString() };
            var exists = keyValues.SequenceEqual(newkeyValues);
            if (exists)
            {
                throw new System.Exception("Duplicated Entry found");
            }
        }
    }
}

リフレクションに頼る必要があるのであまり良くありませんが、これまでのところ、これは私にとってうまくいくアプローチです! =D

于 2011-05-19T05:54:15.303 に答える
5

また、6.1では、@ mihkelmuurの回答の流暢な構文バージョンを次のように使用できます。

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

流暢な方法は完璧な IMO ではありませんが、少なくとも現在は可能です。

Arthur Vickers ブログの詳細http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

于 2015-03-27T03:49:12.610 に答える
5

データベースの作成時に SQL を実行するために完全なハックを行います。独自の DatabaseInitializer を作成し、提供された初期化子の 1 つから継承します。

public class MyDatabaseInitializer : RecreateDatabaseIfModelChanges<MyDbContext>
{
    protected override void Seed(MyDbContext context)
    {
        base.Seed(context);
        context.Database.Connection.StateChange += new StateChangeEventHandler(Connection_StateChange);
    }

    void Connection_StateChange(object sender, StateChangeEventArgs e)
    {
        DbConnection cnn = sender as DbConnection;

        if (e.CurrentState == ConnectionState.Open)
        {
            // execute SQL to create indexes and such
        }

        cnn.StateChange -= Connection_StateChange;
    }
}

これは、SQL ステートメントでくさびを見つけることができた唯一の場所です。

これはCTP4からのものです。CTP5でどのように機能するかわかりません。

于 2010-12-13T00:42:31.030 に答える
2

流暢な 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();
});
于 2020-05-15T08:42:26.940 に答える
2

リフレクションで問題を解決しました(申し訳ありませんが、VB.Net...)

まず、属性 UniqueAttribute を定義します。

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

End Class

次に、モデルを次のように拡張します

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

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

End Class

最後に、カスタム DatabaseInitializer を作成します (私のバージョンでは、デバッグ モードの場合にのみ DB の変更に応じて DB を再作成します...)。この DatabaseInitializer では、Unique-Attributes に基づいてインデックスが自動的に作成されます。

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

おそらくこれが役立ちます...

于 2011-09-15T06:36:01.230 に答える
1

DbContext クラスで ValidateEntity メソッドをオーバーライドすると、そこにもロジックを配置できます。ここでの利点は、すべての DbSet に完全にアクセスできることです。次に例を示します。

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; }
    }
}
于 2011-07-14T16:37:42.440 に答える
1

EF Code First アプローチでは、次の手法を使用して、属性ベースの一意の制約サポートを実装できます。

マーカー属性を作成する

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

エンティティで一意にしたいプロパティをマークします。

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

データベース初期化子を作成するか、既存のものを使用して一意の制約を作成します

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

このイニシャライザをスタートアップ コードで使用するようにデータベース コンテキストを設定します (例:main()またはApplication_Start()) 。

Database.SetInitializer(new DbInitializer());

ソリューションは mheyman のものと似ていますが、複合キーをサポートしないという単純化が行われています。EF 5.0+ で使用します。

于 2014-04-01T16:08:28.903 に答える
0

私は今日その問題に直面し、ついにそれを解決することができました。正しいアプローチかどうかはわかりませんが、少なくとも続行できます。

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);
        }
    }
}
于 2012-10-04T01:39:40.393 に答える
0

一意のプロパティ バリデータを使用します。

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

ValidateEntity同じデータベース トランザクション内では呼び出されません。したがって、データベース内の他のエンティティとの競合状態が発生する可能性があります。SaveChanges(したがって、 )の周りでトランザクションを強制するには、EF を多少ハックする必要がありますValidateEntityDBContext接続を直接開くことはできませんが、ObjectContextできます。

using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required)) {
   ((IObjectContextAdapter)data_context).ObjectContext.Connection.Open();
   data_context.SaveChanges();
   transaction.Complete();
}
于 2011-10-03T21:03:51.367 に答える
0

コード ファースト構成を使用する場合は、IndexAttribute オブジェクトを ColumnAnnotation として使用し、その IsUnique プロパティを true に設定することもできます。

例:

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

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

これにより、名前列に IX_name という名前の一意のインデックスが作成されます。

于 2015-10-29T08:48:49.680 に答える
0

この質問を読んだ後、 Mihkel MüürTobias Schittkowski、およびmheyman の回答のように、プロパティを一意のキーとして指定するための属性を実装しようとする過程で、私自身の質問がありました: Map Entity Framework code properties to database columns (CSpace to SSpace)

最終的に、スカラー プロパティとナビゲーション プロパティの両方をデータベース列にマップし、属性に指定された特定のシーケンスで一意のインデックスを作成できるこの回答にたどり着きました。このコードは、Sequence プロパティを使用して UniqueAttribute を実装し、エンティティの一意のキー (主キー以外) を表す必要がある EF エンティティ クラスのプロパティに適用したことを前提としています。

注:このコードは、EF バージョン 6.1 (またはそれ以降) に依存してEntityContainerMappingおり、以前のバージョンでは使用できないものを公開しています。

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
于 2014-04-29T13:51:06.793 に答える