Entity Framework Code Firstの一意の制約


125

質問

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

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

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

public class MyContext : DbContext {
    public DbSet<User> Users { get; set; }

    public override int SaveChanges() {
        foreach (var item in ChangeTracker.Entries<IModel>())
            item.Entity.Modified = DateTime.Now;

        return base.SaveChanges();
    }

    public class Initializer : IDatabaseInitializer<MyContext> {
        public void InitializeDatabase(MyContext context) {
            if (context.Database.Exists() && !context.Database.CompatibleWithModel(false))
                context.Database.Delete();

            if (!context.Database.Exists()) {
                context.Database.Create();
                context.Database.ExecuteSqlCommand("alter table Users add constraint UniqueUserEmail unique (Email)");
            }
        }
    }
}

1
これを行うと、アプリがその正確な構文を受け入れるデータベースのみ(この場合はSQL Server)に制限されることに注意してください。Oracleプロバイダーでアプリを実行すると失敗します。
DamienG

1
そのような状況では、新しいInitializerクラスを作成するだけで十分ですが、これは有効なポイントです。
kim3er 2010

3
この投稿を確認してください。ValidationAttributeは、データベースの仲間の行に対して一意のフィールドを検証します。ソリューションは、ObjectContextまたはを対象としていますDbContext
Shimmy Weitzhandler

はい、EF 6.1以降でサポートされています。
Evandro Pomatti 2014

回答:


61

私の知る限り、現時点では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...");
            }
        }
    }
}

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


私は自分のDBを太鼓のようにタイトにしたいのですが、ロジックはビジネス層に複製されます。あなたは答えはCTP4でのみ機能しますが、正しい軌道に乗っています、私は元の質問の下でCTP5と互換性のあるソリューションを提供しました。どうもありがとう!
kim3er 2010

23
アプリがシングルユーザーでない限り、一意の制約はあなたの1つのことだと思います コードだけで実施できない。(を呼び出す前に一意性をチェックすることにより)コードの違反の可能性を劇的に減らすことができますがSaveChanges()、一意性チェックの時間との時間の間に別の挿入/更新がずれる可能性がありSaveChanges()ます。したがって、アプリのミッションクリティカル度と一意性違反の可能性に応じて、データベースに制約を追加するのがおそらく最善です。
devuxer '30

1
一意性のチェックをSaveChangesと同じトランザクションの一部にする必要があります。データベースが酸に準拠していると仮定すると、この方法で一意性を強制できるはずです。EFがトランザクションのライフサイクルをこのように適切に管理できるかどうかは、別の問題です。
mattmc3 2011年

1
@ mattmc3トランザクション分離レベルによって異なります。serializable isolation level実際には、コードでの一意性を保証できるのは(またはカスタムテーブルロック、ugh)だけです。しかしserializable isolation level、パフォーマンス上の理由から、ほとんどの人はを使用しません。MS SQL Serverのデフォルトはread committedです。michaeljswart.com/2010/03/…
Nathan

3
EntityFramework 6.1.0はIndexAttributeをサポートするようになりました。これにより、基本的にプロパティの上に追加できます。
ソート

45

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

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

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


5
@Dave:それぞれのプロパティ(source)の属性に同じインデックス名を使用するだけです。
MihkelMüür2014年

これは、一意のインデックスではなく、ユニークな作成することに注意してくださいcontraintを。ほぼ同じですが、まったく同じではありません(私が理解しているように、FKのターゲットとして一意の制約を使用できます)。制約については、SQLを実行する必要があります。
リチャード

(最後のコメントの後に)SQL Serverの最近のバージョンではこの制限が削除されたと他の情報源が示唆しています...しかし、BOLは完全に一貫していません。
リチャード

@Richard:すぐに使用できるわけではありませんが、属性ベースの一意の制約も可能です(2番目の回答を参照)。
MihkelMüür2014

1
@exSnake:SQL Server 2008以降、一意のインデックスはデフォルトで列ごとに単一のNULL値をサポートします。複数のNULLのサポートが必要な場合は、フィルターされたインデックスが必要になります。別の質問を参照してください。
MihkelMüür18年

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

EFがRailsスタイルのマイグレーションを持っているのを見るのはうれしい。モノで実行できればいいのに。
kim3er 2012

2
Down()プロシージャにもDropIndexを含めるべきではありませんか?DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg

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でどのように機能するかわかりません。


ケリーさん、ありがとう!私はそのイベントハンドラーを知りませんでした。私の最終的な解決策は、SQLをInitializeDatabaseメソッドに配置します。
kim3er 2010

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


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/


4

EF5 Code First Migrationsを使用したVisual Basicの簡単な方法

パブリッククラスのサンプル

    Public Property SampleId As Integer

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

    Public Property Code() As String

終了クラス

属性MaxLengthは、文字列型の一意のインデックスにとって非常に重要です

cmdを実行:update-database -verbose

cmdを実行した後:add-migration 1

生成されたファイル内

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

これは実際にはカスタムDB初期化子よりも適切な答えです。
Shaun Wilson

4

Tobias Schittkowskiの回答に似ていますが、C#であり、制約内に複数のフィールドを持つことができます。

これを使用するには、一意にするフィールドに[一意]を配置するだけです。文字列の場合、次のようにする必要があります(MaxLength属性に注意してください)。

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

デフォルトの文字列フィールドはnvarchar(max)であり、キーでは許可されないためです。

制約内の複数のフィールドに対して、次のことができます。

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

まず、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; }
}

次に、型からデータベーステーブル名を取得するための便利な拡張機能を含めます。

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

次に、データベース初期化子:

/// <summary>
///     The database initializer.
/// </summary>
public class DatabaseInitializer : IDatabaseInitializer<PedContext>
{
    /// <summary>
    /// Initialize the database.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    public void InitializeDatabase(FooContext context)
    {
        // if the database has changed, recreate it.
        if (context.Database.Exists()
            && !context.Database.CompatibleWithModel(false))
        {
            context.Database.Delete();
        }

        if (!context.Database.Exists())
        {
            context.Database.Create();

            // Look for database tables in the context. Tables are of
            // type DbSet<>.
            foreach (PropertyInfo contextPropertyInfo in 
                     context.GetType().GetProperties())
            {
                var contextPropertyType = contextPropertyInfo.PropertyType;
                if (contextPropertyType.IsGenericType
                    && contextPropertyType.Name.Equals("DbSet`1"))
                {
                    Type tableType = 
                        contextPropertyType.GetGenericArguments()[0];
                    var tableName = context.GetTableName(tableType);
                    foreach (var uc in UniqueConstraints(tableType, tableName))
                    {
                        context.Database.ExecuteSqlCommand(uc);
                    }
                }
            }

            // this is a good place to seed the database
            context.SaveChanges();
        }
    }

    /// <summary>
    /// Get a list of TSQL commands to create unique constraints on the given 
    /// table. Looks through the table for fields with the UniqueAttribute
    /// and uses those and the table name to build the TSQL strings.
    /// </summary>
    /// <param name="tableClass">
    /// The class that expresses the database table.
    /// </param>
    /// <param name="tableName">
    /// The table name in the database.
    /// </param>
    /// <returns>
    /// The list of TSQL statements for altering the table to include unique 
    /// constraints.
    /// </returns>
    private static IEnumerable<string> UniqueConstraints(
        Type tableClass, string tableName)
    {
        // the key is the name of the constraint and the value is a list 
        // of (position,field) pairs kept in order of position - the entry
        // with the lowest position is first.
        var uniqueConstraints = 
            new Dictionary<string, List<Tuple<int, string>>>();
        foreach (PropertyInfo entityPropertyInfo in tableClass.GetProperties())
        {
            var unique = entityPropertyInfo.GetCustomAttributes(true)
                         .OfType<UniqueAttribute>().FirstOrDefault();
            if (unique != null)
            {
                string fieldName = entityPropertyInfo.Name;

                // use the name field in the UniqueAttribute or create a
                // name if none is given
                string constraintName = unique.Name
                                        ?? string.Format(
                                            "constraint_{0}_unique_{1}",
                                            tableName
                                               .Replace("[", string.Empty)
                                               .Replace("]", string.Empty)
                                               .Replace(".", "_"),
                                            fieldName);

                List<Tuple<int, string>> constraintEntry;
                if (!uniqueConstraints.TryGetValue(
                        constraintName, out constraintEntry))
                {
                    uniqueConstraints.Add(
                        constraintName, 
                        new List<Tuple<int, string>> 
                        {
                            new Tuple<int, string>(
                                unique.Position, fieldName) 
                        });
                }
                else
                {
                    // keep the list of fields in order of position
                    for (int i = 0; ; ++i)
                    {
                        if (i == constraintEntry.Count)
                        {
                            constraintEntry.Add(
                                new Tuple<int, string>(
                                    unique.Position, fieldName));
                            break;
                        }

                        if (unique.Position < constraintEntry[i].Item1)
                        {
                            constraintEntry.Insert(
                                i, 
                                new Tuple<int, string>(
                                    unique.Position, fieldName));
                            break;
                        }
                    }
                }
            }
        }

        return
            uniqueConstraints.Select(
                uc =>
                string.Format(
                    "ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE ({2})",
                    tableName,
                    uc.Key,
                    string.Join(",", uc.Value.Select(v => v.Item2))));
    }
}

2

リフレクションで問題を解決しました(申し訳ありません、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

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


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

1

EF5を使用していてもこの質問がある場合は、以下の解決策で解決しました。

私はコードファーストアプローチを使用しているため、次のように記述します。

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

移行スクリプトでうまくいきました。NULL値も許可されます!


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以降で使用します。


1

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

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

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を多少ハックする必要があります(したがって、ValidateEntity)。DBContext接続を直接開くことはできませんが、開くことはObjectContextできます。

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


0

この質問を読んだ後、私はのようなユニークなキーとしての性質を指定するための属性実装しようとする過程で自分の質問だったMihkel Muur料理のをトビアスSchittkowskiのmheymanの答えがお勧め:地図Entity Frameworkのコードのプロパティをデータベース列(SSpaceに関連するcspace)に

私はようやくこの答えに到達しました。これは、スカラープロパティとナビゲーションプロパティの両方をデータベースの列にマッピングし、属性で指定された特定のシーケンスで一意のインデックスを作成できます。このコードは、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

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という名前の一意のインデックスが作成されます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.