Entity Framework 코드의 고유 제약 조건


125

질문

유창한 구문이나 속성을 사용하여 속성에 대한 고유 제약 조건을 정의 할 수 있습니까? 그렇지 않은 경우 해결 방법은 무엇입니까?

기본 키가있는 사용자 클래스가 있지만 이메일 주소도 고유한지 확인하고 싶습니다. 데이터베이스를 직접 편집하지 않고도 가능합니까?

해결책 (Matt의 답변을 기반으로 함)

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

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

        return base.SaveChanges();
    }

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

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

1
이 작업을 수행하면 앱이 정확한 구문을 허용하는 데이터베이스 (이 경우 SQL Server)로만 제한됩니다. Oracle 공급자로 앱을 실행하면 실패합니다.
DamienG

1
이 상황에서는 새 Initializer 클래스 만 만들면되지만 유효한 지점입니다.
kim3er

3
이 게시물을 확인하십시오 : ValidationAttribute는 데이터베이스의 동료 행에 대해 고유 필드의 유효성을 검사하며 솔루션은 ObjectContext또는을 대상으로합니다 DbContext.
Shimmy Weitzhandler

예, 이제 EF 6.1부터 지원됩니다 .
Evandro Pomatti

답변:


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

23
귀하의 앱이 단일 사용자 가 아닌 한, 고유 제약 조건은 코드만으로는 시행 할 수없는 것 입니다. 코드를 호출하기 전에 고유성을 검사하여 코드 위반 가능성을 크게 줄일 수 SaveChanges()있지만 고유성 검사 시간과의 시간 사이에 다른 삽입 / 업데이트가 여전히 발생할 가능성이 SaveChanges()있습니다. 따라서 앱의 미션 크리티컬 방식과 고유성 위반 가능성에 따라 데이터베이스에 제약 조건을 추가하는 것이 가장 좋습니다.
devuxer

1
고유성 검사는 SaveChanges와 동일한 트랜잭션의 일부 여야합니다. 데이터베이스가 산을 준수한다고 가정하면 이러한 방식으로 고유성을 강제 할 수 있어야합니다. 이제 EF를 통해 트랜잭션 라이프 사이클을 올바르게 관리 할 수 ​​있는지 여부는 또 다른 질문입니다.
mattmc3

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를 지원합니다.
sotn

45

EF 6.1부터는 이제 가능합니다 :

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

엄밀히 말하면 고유 제약 조건 대신 고유 인덱스를 얻을 수 있습니다. 대부분의 실제적인 목적 은 동일 합니다.


5
@Dave : 각 속성 ( source ) 의 속성에 동일한 인덱스 이름을 사용하십시오 .
Mihkel Müür

이것은 고유 한 제약 조건이 아닌 고유 한 색인을 생성합니다 . 거의 동일하지만 완전히 동일하지는 않습니다 (유일한 제약이 FK의 대상으로 사용될 수 있음을 이해합니다). 제약 조건의 경우 SQL을 실행해야합니다.
Richard

(마지막 주석 다음) 다른 출처에서는 최신 SQL Server 버전에서이 제한이 제거되었다고 제안하지만 BOL이 완전히 일치하지는 않습니다.
Richard

@Richard : 속성 기반의 고유 제약 조건 도 가능합니다 ( 두 번째 답변 참조 ).
Mihkel Müür

1
@exSnake : SQL Server 2008부터 고유 인덱스는 기본적으로 열당 단일 NULL 값을 지원합니다. 여러 NULL을 지원해야하는 경우 필터링 된 인덱스가 필요합니다 ( 또 다른 질문 참조) .
Mihkel Müür 1

28

실제로 이와 관련이 없지만 경우에 따라 도움이 될 수 있습니다.

테이블의 제약 조건으로 작용하는 2 개의 열에 대해 고유 한 복합 인덱스를 작성하려는 경우 버전 4.3부터 새 마이그레이션 메커니즘을 사용하여이를 달성 할 수 있습니다.

기본적으로 마이그레이션 스크립트 중 하나에 다음과 같은 호출을 삽입해야합니다.

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

2
Down () 프로 시저에 DropIndex도 없어야합니까? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg

5

데이터베이스를 만들 때 SQL을 실행하기 위해 완전한 해킹을 수행합니다. 내 자신의 DatabaseInitializer를 만들고 제공된 초기화 프로그램 중 하나에서 상속합니다.

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

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를 사용하여 비주얼 베이직에서 손쉬운 방법

공공 수업 샘플

    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 #은 constrtaints에 여러 필드를 가질 수 있습니다.

이것을 사용하려면, 당신이 유일하게 원하는 필드에 [Unique]를 놓으십시오. 문자열의 경우 다음과 같은 작업을 수행해야합니다 (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 on DB 변경 사항을 다시 만듭니다 ...). 이 DatabaseInitializer에서 인덱스는 고유 속성을 기반으로 자동으로 작성됩니다.

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

유창한 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동일한 데이터베이스 트랜잭션 내에서 호출되지 않습니다. 따라서 데이터베이스의 다른 엔터티와 경쟁 조건이있을 수 있습니다. EF를 다소 해킹하여 SaveChanges(및 따라서 ValidateEntity) 거래를 강제해야합니다 . DBContext연결을 직접 열 수는 없지만 ObjectContext할 수는 있습니다.

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


0

이 질문을 읽은 후에 Mihkel Müür 's , Tobias Schittkowski 'smheyman의 답변 과 같이 고유 키로 속성을 지정하기 위해 속성을 구현하려고 시도하는 과정에서 내 자신의 질문이있었습니다 .Entity Framework 코드 속성을 데이터베이스 열에 매핑하십시오 (CSpace에서 SSpace로)

마침내이 답변에 도달하여 스칼라 및 탐색 속성을 데이터베이스 열에 매핑하고 속성에 지정된 특정 순서로 고유 인덱스를 만들 수 있습니다. 이 코드에서는 Sequence 속성을 사용하여 UniqueAttribute를 구현하고 엔터티의 고유 키 (기본 키 이외)를 나타내는 EF 엔터티 클래스 속성에 적용했다고 가정합니다.

참고 : 이 코드 EntityContainerMapping는 이전 버전에서는 사용할 수없는 EF 버전 6.1 이상을 사용합니다 .

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이라는 고유 인덱스가 생성됩니다.


0

답이 늦어서 미안하지만 나는 너와 함께하는 것이 좋다는 것을 알았다.

나는 이것에 대해 코드 프로젝트에 게시했다.

일반적으로 고유 색인을 생성하기 위해 클래스에 적용한 속성에 따라 다릅니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.