ข้อ จำกัด เฉพาะในรหัสกรอบงานเอนทิตีก่อน


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(หรือการล็อกตารางแบบกำหนดเองฮึ) เท่านั้นที่จะช่วยให้คุณสามารถรับประกันความเป็นเอกลักษณ์ในรหัสของคุณได้ แต่คนส่วนใหญ่ไม่ใช้serializable isolation levelเนื่องจากเหตุผลด้านประสิทธิภาพ เริ่มต้นใน MS SQL Server read committedคือ ดูซีรีส์ภาค 4 เริ่มที่: michaeljswart.com/2010/03/…
นาธา

3
EntityFramework 6.1.0 รองรับ IndexAttribute ซึ่งโดยพื้นฐานแล้วคุณสามารถเพิ่มได้ที่ด้านบนของคุณสมบัติ
sotn

45

เริ่มต้นด้วย EF 6.1 ตอนนี้เป็นไปได้:

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

สิ่งนี้จะทำให้คุณได้รับดัชนีที่ไม่เหมือนใครแทนที่จะเป็นข้อ จำกัด เฉพาะพูดอย่างเคร่งครัด เพื่อวัตถุประสงค์ในการปฏิบัติมากที่สุดพวกเขาเป็นเดียวกัน


5
@Dave: ใช้ชื่อดัชนีเดียวกันกับคุณสมบัติของคุณสมบัติที่เกี่ยวข้อง ( แหล่งที่มา )
Mihkel Mür

หมายเหตุนี้จะสร้างดัชนีที่ไม่ซ้ำมากกว่าที่ไม่ซ้ำกันcontraint ในขณะที่เกือบจะเหมือนกัน แต่ก็ไม่เหมือนกัน (อย่างที่ฉันเข้าใจว่าข้อ จำกัด เฉพาะสามารถใช้เป็นเป้าหมายของ FK ได้) สำหรับข้อ จำกัด คุณต้องดำเนินการ SQL
Richard

(ทำตามความคิดเห็นสุดท้าย) แหล่งข้อมูลอื่นแนะนำว่าข้อ จำกัด นี้ถูกลบออกใน SQL Server เวอร์ชันล่าสุด ... แต่ BOL ไม่สอดคล้องกันอย่างสมบูรณ์
Richard

@ ริชาร์ด: ข้อ จำกัดเฉพาะตามแอตทริบิวต์ยังเป็นไปได้ (ดูคำตอบที่สองของฉัน ) แม้ว่าจะไม่อยู่นอกกรอบก็ตาม
Mihkel Mür

1
@exSnake: ตั้งแต่ SQL Server 2008 ดัชนีเฉพาะรองรับค่า NULL เดียวต่อคอลัมน์ตามค่าเริ่มต้น ในการสนับสนุนกรณี NULLs หลายจำเป็นต้องมีดัชนีกรองจะต้องดูคำถามอื่น
Mihkel Mür

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 ตอนนี้ถ้ามีเพียงฉันสามารถรันบน Mono
kim3er

2
คุณไม่ควรมี DropIndex ในขั้นตอน Down () หรือไม่? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg

5

ฉันทำการแฮ็คโดยสมบูรณ์เพื่อให้ SQL ดำเนินการเมื่อกำลังสร้างฐานข้อมูล ฉันสร้าง DatabaseInitializer ของตัวเองและสืบทอดจาก initializers ที่มีให้

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

สุดท้ายฉันจะเพิ่มวิธีการในที่เก็บของฉันในวิธีการเพิ่มหรือเมื่อบันทึกการเปลี่ยนแปลงเช่นนี้:

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 ที่สมบูรณ์แบบ แต่อย่างน้อยก็เป็นไปได้ในตอนนี้

deets เพิ่มเติมในบล็อกของ Arthur Vickers http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/


4

วิธีง่ายๆใน Visual Basic โดยใช้ 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 (สูงสุด) และจะไม่ได้รับอนุญาตในคีย์

สำหรับหลายช่องในข้อ จำกัด คุณสามารถทำได้:

[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 แบบกำหนดเอง (ในเวอร์ชันของฉันฉันสร้างฐานข้อมูลใหม่บนฐานข้อมูลเฉพาะในกรณีที่อยู่ในโหมดดีบัก ... ) ใน 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

หากคุณแทนที่เมธอด ValidateEntity ในคลาส DbContext ของคุณคุณสามารถใส่ตรรกะที่นั่นได้เช่นกัน ข้อดีคือคุณจะสามารถเข้าถึง DbSets ทั้งหมดของคุณได้อย่างเต็มที่ นี่คือตัวอย่าง:

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ไม่ถูกเรียกภายในธุรกรรมฐานข้อมูลเดียวกัน ดังนั้นจึงอาจมีเงื่อนไขการแข่งขันกับเอนทิตีอื่นในฐานข้อมูล คุณต้องแฮ็ค 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 Muur ของ , โทเบียส Schittkowski ของและmheyman ของคำตอบที่แนะนำ: แผนที่ Entity Framework คุณสมบัติรหัสคอลัมน์ฐานข้อมูล (CSpace เพื่อ SSpace)

ในที่สุดฉันก็มาถึงคำตอบนี้ซึ่งสามารถแมปทั้งคุณสมบัติสเกลาร์และการนำทางลงไปที่คอลัมน์ฐานข้อมูลและสร้างดัชนีเฉพาะในลำดับเฉพาะที่กำหนดไว้ในแอตทริบิวต์ รหัสนี้ถือว่าคุณได้ใช้ UniqueAttribute กับคุณสมบัติ Sequence และนำไปใช้กับคุณสมบัติคลาสเอนทิตี 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 บนคอลัมน์ชื่อ


0

ขออภัยสำหรับคำตอบที่ล่าช้า แต่ฉันคิดว่ามันเป็นการดีที่จะแบ่งปันกับคุณ

ฉันได้โพสต์เกี่ยวกับเรื่องนี้ที่โครงการรหัส

โดยทั่วไปจะขึ้นอยู่กับแอตทริบิวต์ที่คุณใส่ในคลาสเพื่อสร้างดัชนีเฉพาะของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.