Restrição exclusiva no código da estrutura da entidade primeiro

125

Questão

É possível definir uma restrição exclusiva em uma propriedade usando a sintaxe fluente ou um atributo? Caso contrário, quais são as soluções alternativas?

Eu tenho uma classe de usuário com uma chave primária, mas gostaria de garantir que o endereço de email também seja exclusivo. Isso é possível sem editar o banco de dados diretamente?

Solução (com base na resposta de 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)");
            }
        }
    }
}
kim3er
fonte
1
Lembre-se de que isso limita seu aplicativo apenas aos bancos de dados que aceitam a sintaxe exata - neste caso, o SQL Server. Se você executar seu aplicativo com um provedor Oracle, ele falhará.
DamienG
1
Nessa situação, eu precisaria apenas criar uma nova classe Initializer, mas é um ponto válido.
Kim3er
3
Confira esta postagem: ValidationAttribute que valida um campo exclusivo em relação às suas linhas no banco de dados , a solução é direcionada para ObjectContextou DbContext.
Shimmy Weitzhandler
Sim, agora é suportado desde o EF 6.1 .
Evandro Pomatti

Respostas:

61

Até onde eu sei, não há como fazer isso com o Entity Framework no momento. No entanto, isso não é apenas um problema com restrições exclusivas ... você pode criar índices, verificar restrições e, possivelmente, gatilhos e outras construções também. Aqui está um padrão simples que você pode usar com sua configuração de primeiro código, embora seja certo que não é independente de banco de dados:

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

Outra opção é se o modelo de domínio for o único método de inserção / atualização de dados no banco de dados, você poderá implementar o requisito de exclusividade e deixar o banco de dados fora dele. Essa é uma solução mais portátil e obriga você a ter clareza sobre suas regras de negócios em seu código, mas deixa seu banco de dados aberto para que dados inválidos sejam recuperados.

mattmc3
fonte
Eu gosto que meu banco de dados seja tão rígido quanto um tambor, a lógica é replicada na camada de negócios. Sua resposta funciona apenas com CTP4, mas me colocou no caminho certo. Forneci uma solução compatível com CTP5 abaixo da minha pergunta original. Muito obrigado!
Kim3er
23
A menos que seu aplicativo seja de usuário único, acredito que uma restrição única é algo que você não pode aplicar apenas com o código. Você pode reduzir drasticamente a probabilidade de uma violação no código (verificando a exclusividade antes da chamada SaveChanges()), mas ainda há a possibilidade de outra inserção / atualização deslizar entre o horário da verificação de exclusividade e o horário de SaveChanges(). Portanto, dependendo da missão crítica do aplicativo e da probabilidade de uma violação de exclusividade, provavelmente é melhor adicionar a restrição ao banco de dados.
Devuxer
1
Você precisaria que sua verificação de exclusividade fizesse parte da mesma transação que suas SaveChanges. Supondo que seu banco de dados seja compatível com ácidos, você deve poder impor a exclusividade dessa maneira. Agora, se a EF permite que você gerencie adequadamente o ciclo de vida da transação dessa maneira é outra questão.
Mattmc3
1
@ mattmc3 Depende do seu nível de isolamento de transação. Somente o serializable isolation level(ou bloqueio de tabela personalizado, ugh) realmente permitiria garantir a exclusividade em seu código. Mas a maioria das pessoas não usa isso serializable isolation levelpor motivos de desempenho. O padrão no MS Sql Server é read committed. Veja a série de 4 partes que começa em: michaeljswart.com/2010/03/…
Nathan
3
O EntityFramework 6.1.0 agora tem suporte para IndexAttribute, que você pode basicamente adicioná-lo sobre as propriedades.
SOTN
45

A partir do EF 6.1, agora é possível:

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

Isso fornecerá a você um índice exclusivo, em vez de uma restrição exclusiva, a rigor. Para os propósitos mais práticos, eles são os mesmos .

Mihkel Müür
fonte
5
@ Dave: basta usar o mesmo nome de índice nos atributos das respectivas propriedades ( origem ).
Mihkel Müür
Observe que isso cria um índice exclusivo em vez de um contraint exclusivo . Embora quase o mesmo, eles não são exatamente o mesmo (pelo que entendi, restrições únicas podem ser usadas como alvo de um FK). Para uma restrição, você precisa executar o SQL.
Richard Richard
(Após o último comentário) Outras fontes sugerem que essa limitação foi removida nas versões mais recentes do SQL Server ... mas o BOL não é completamente consistente.
Richard Richard
@ Richard: restrições exclusivas baseadas em atributos também são possíveis (veja minha segunda resposta ), embora não estejam prontas para uso.
Mihkel Müür
1
@exSnake: Desde o SQL Server 2008, o índice exclusivo suporta um único valor NULL por coluna por padrão. Caso seja necessário o suporte para vários NULLs, seria necessário um índice filtrado, ver outra pergunta .
Mihkel Müür 02/04/19
28

Não está realmente relacionado a isso, mas pode ajudar em alguns casos.

Se você deseja criar um índice composto exclusivo, digamos 2 colunas que funcionarão como uma restrição para sua tabela, a partir da versão 4.3, você pode usar o novo mecanismo de migração para alcançá-lo:

Basicamente, você precisa inserir uma chamada como essa em um dos seus scripts de migração:

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

Algo parecido:

namespace Sample.Migrations
{
    using System;
    using System.Data.Entity.Migrations;

    public partial class TableName_SetUniqueCompositeIndex : DbMigration
    {
        public override void Up()
        {
            CreateIndex("TableName", new[] { "Column1", "Column2" }, true, "IX_UniqueColumn1AndColumn2");
        }

        public override void Down()
        {
            DropIndex("TableName", new[] { "Column1", "Column2" });
        }
    }
}
lnaie
fonte
É bom ver que a EF tem migrações no estilo Rails. Agora, se eu pudesse rodar em Mono.
Kim3er
2
Você também não deve ter um DropIndex no procedimento Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg 08/12/12
5

Eu faço um hack completo para executar o SQL quando o banco de dados está sendo criado. Crio meu próprio DatabaseInitializer e herdo de um dos inicializadores fornecidos.

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

Esse é o único lugar que eu poderia encontrar para inserir em minhas instruções SQL.

Isso é do CTP4. Não sei como funciona no CTP5.

Kelly Ethridge
fonte
Obrigado Kelly! Eu não estava ciente desse manipulador de eventos. Minha solução eventual coloca o SQL no método InitializeDatabase.
Kim3er
5

Apenas tentando descobrir se havia uma maneira de fazer isso, a única maneira que encontrei até agora era aplicá-la, criei um atributo a ser adicionado a cada classe em que você fornece o nome dos campos que precisa ser exclusivo:

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

Então, na minha turma, vou adicioná-lo:

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

Por fim, adicionarei um método no meu repositório, no método Add ou ao salvar alterações como esta:

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

Não é tão bom quanto precisamos confiar na reflexão, mas até agora é a abordagem que funciona para mim! = D

Rosendo
fonte
5

Também na 6.1, você pode usar a versão fluente da sintaxe da resposta de @ mihkelmuur da seguinte maneira:

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

O método fluente não é perfeito IMO, mas pelo menos é possível agora.

Mais informações sobre o blog Arthur Vickers http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

Não amado
fonte
4

Uma maneira fácil no visual basic usando as primeiras migrações de código EF5

Amostra de classe pública

    Public Property SampleId As Integer

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

    Public Property Code() As String

Classe final

O atributo MaxLength é muito importante para o índice exclusivo do tipo de sequência

Execute o cmd: update-database -verbose

depois de executar o cmd: add-migration 1

no arquivo gerado

Public Partial Class _1
    Inherits DbMigration

    Public Overrides Sub Up()
        CreateIndex("dbo.Sample", "Code", unique:=True, name:="IX_Sample_Code")
    End Sub

    Public Overrides Sub Down()
        'DropIndex if you need it
    End Sub

End Class
Despota
fonte
Esta é realmente uma resposta mais apropriada do que um inicializador de banco de dados personalizado.
Shaun Wilson
4

Semelhante à resposta de Tobias Schittkowski, mas em C # e tem a capacidade de ter vários campos nas restrições.

Para usar isso, basta colocar um [Único] em qualquer campo que você deseja que seja único. Para strings, você terá que fazer algo como (observe o atributo MaxLength):

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

porque o campo de sequência padrão é nvarchar (max) e isso não será permitido em uma chave.

Para vários campos na restrição, você pode:

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

Primeiro, o 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; }
}

Em seguida, inclua uma extensão útil para obter o nome da tabela do banco de dados de um tipo:

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

Em seguida, o inicializador do banco de dados:

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

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

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

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

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

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

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

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

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

Eu resolvi o problema pela reflexão (desculpe pessoal, VB.Net ...)

Primeiro, defina um atributo UniqueAttribute:

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

End Class

Em seguida, aprimore seu modelo como

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

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

End Class

Por fim, crie um DatabaseInitializer personalizado (na minha versão, recrio as alterações de banco de dados no banco de dados somente se estiver no modo de depuração ...). Nesse DatabaseInitializer, os índices são criados automaticamente com base nos atributos exclusivos:

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

Talvez isso ajude ...

Tobias Schittkowski
fonte
1

Se você substituir o método ValidateEntity na sua classe DbContext, poderá colocar a lógica também. A vantagem aqui é que você terá acesso total a todos os seus DbSets. Aqui está um exemplo:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Data.Entity.Validation;
using System.Linq;

namespace MvcEfClient.Models
{
    public class Location
    {
        [Key]
        public int LocationId { get; set; }

        [Required]
        [StringLength(50)]
        public string Name { get; set; }
    }

    public class CommitteeMeetingContext : DbContext
    {
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }

        protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
        {
            List<DbValidationError> validationErrors = new List<DbValidationError>();

            // Check for duplicate location names

            if (entityEntry.Entity is Location)
            {
                Location location = entityEntry.Entity as Location;

                // Select the existing location

                var existingLocation = (from l in Locations
                                        where l.Name == location.Name && l.LocationId != location.LocationId
                                        select l).FirstOrDefault();

                // If there is an existing location, throw an error

                if (existingLocation != null)
                {
                    validationErrors.Add(new DbValidationError("Name", "There is already a location with the name '" + location.Name + "'"));
                    return new DbEntityValidationResult(entityEntry, validationErrors);
                }
            }

            return base.ValidateEntity(entityEntry, items);
        }

        public DbSet<Location> Locations { get; set; }
    }
}
Frank Hoffman
fonte
1

Se você estiver usando o EF5 e ainda tiver essa dúvida, a solução abaixo resolveu o problema para mim.

Estou usando a primeira abordagem de código, portanto, colocando:

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

no script de migração fez o trabalho bem. Também permite valores NULL!

FDIM
fonte
1

Com a abordagem EF Code First, é possível implementar o suporte a restrições exclusivas com base em atributos usando a seguinte técnica.

Crie um atributo de marcador

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

Marque as propriedades que você deseja que sejam únicas nas entidades, por exemplo

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

Crie um inicializador de banco de dados ou use um existente para criar restrições exclusivas

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

Defina o contexto do banco de dados para usar este inicializador no código de inicialização (por exemplo, em main()ou Application_Start())

Database.SetInitializer(new DbInitializer());

A solução é semelhante à do mheyman, com uma simplificação de não suporte a chaves compostas. Para ser usado com EF 5.0+.

Mihkel Müür
fonte
1

Solução Api fluente:

modelBuilder.Entity<User>(entity =>
{
    entity.HasIndex(e => e.UserId)
          .HasName("IX_User")
          .IsUnique();

    entity.HasAlternateKey(u => u.Email);

    entity.HasIndex(e => e.Email)
          .HasName("IX_Email")
          .IsUnique();
});
Pierre
fonte
0

Enfrentei esse problema hoje e finalmente consegui resolvê-lo. Não sei se é uma abordagem correta, mas pelo menos posso continuar:

public class Person : IValidatableObject
{
    public virtual int ID { get; set; }
    public virtual string Name { get; set; }


    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var field = new[] { "Name" }; // Must be the same as the property

        PFContext db = new PFContext();

        Person person = validationContext.ObjectInstance as Person;

        var existingPerson = db.Persons.FirstOrDefault(a => a.Name == person.Name);

        if (existingPerson != null)
        {
            yield return new ValidationResult("That name is already in the db", field);
        }
    }
}
Juan Carlos Puerto
fonte
0

Use um validador de propriedade exclusivo.

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

ValidateEntitynão é chamado na mesma transação do banco de dados. Portanto, pode haver condições de corrida com outras entidades no banco de dados. Você precisa invadir o EF para forçar uma transação em torno do SaveChanges(e, portanto, ValidateEntity). DBContextnão pode abrir a conexão diretamente, mas ObjectContextpode.

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

Depois de ler esta pergunta, tive minha própria pergunta no processo de tentar implementar um atributo para designar propriedades como chaves exclusivas, como as respostas de Mihkel Müür , Tobias Schittkowski e mheyman sugerem: Mapear propriedades de código do Entity Framework para colunas de banco de dados (CSpace para SSpace)

Finalmente, cheguei a essa resposta, que pode mapear propriedades escalares e de navegação até as colunas do banco de dados e criar um índice exclusivo em uma sequência específica designada no atributo. Esse código pressupõe que você implementou um UniqueAttribute com uma propriedade Sequence e o aplicou às propriedades da classe de entidade EF que devem representar a chave exclusiva da entidade (exceto a chave primária).

Nota: Este código depende da EF versão 6.1 (ou posterior), que EntityContainerMappingnão está disponível nas versões anteriores.

Public Sub InitializeDatabase(context As MyDB) Implements IDatabaseInitializer(Of MyDB).InitializeDatabase
    If context.Database.CreateIfNotExists Then
        Dim ws = DirectCast(context, System.Data.Entity.Infrastructure.IObjectContextAdapter).ObjectContext.MetadataWorkspace
        Dim oSpace = ws.GetItemCollection(Core.Metadata.Edm.DataSpace.OSpace)
        Dim entityTypes = oSpace.GetItems(Of EntityType)()
        Dim entityContainer = ws.GetItems(Of EntityContainer)(DataSpace.CSpace).Single()
        Dim entityMapping = ws.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).Single.EntitySetMappings
        Dim associations = ws.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).Single.AssociationSetMappings
        For Each setType In entityTypes
           Dim cSpaceEntitySet = entityContainer.EntitySets.SingleOrDefault( _
              Function(t) t.ElementType.Name = setType.Name)
           If cSpaceEntitySet Is Nothing Then Continue For ' Derived entities will be skipped
           Dim sSpaceEntitySet = entityMapping.Single(Function(t) t.EntitySet Is cSpaceEntitySet)
           Dim tableInfo As MappingFragment
           If sSpaceEntitySet.EntityTypeMappings.Count = 1 Then
              tableInfo = sSpaceEntitySet.EntityTypeMappings.Single.Fragments.Single
           Else
              ' Select only the mapping (esp. PropertyMappings) for the base class
              tableInfo = sSpaceEntitySet.EntityTypeMappings.Where(Function(m) m.IsOfEntityTypes.Count _
                 = 1 AndAlso m.IsOfEntityTypes.Single.Name Is setType.Name).Single().Fragments.Single
           End If
           Dim tableName = If(tableInfo.StoreEntitySet.Table, tableInfo.StoreEntitySet.Name)
           Dim schema = tableInfo.StoreEntitySet.Schema
           Dim clrType = Type.GetType(setType.FullName)
           Dim uniqueCols As IList(Of String) = Nothing
           For Each propMap In tableInfo.PropertyMappings.OfType(Of ScalarPropertyMapping)()
              Dim clrProp = clrType.GetProperty(propMap.Property.Name)
              If Attribute.GetCustomAttribute(clrProp, GetType(UniqueAttribute)) IsNot Nothing Then
                 If uniqueCols Is Nothing Then uniqueCols = New List(Of String)
                 uniqueCols.Add(propMap.Column.Name)
              End If
           Next
           For Each navProp In setType.NavigationProperties
              Dim clrProp = clrType.GetProperty(navProp.Name)
              If Attribute.GetCustomAttribute(clrProp, GetType(UniqueAttribute)) IsNot Nothing Then
                 Dim assocMap = associations.SingleOrDefault(Function(a) _
                    a.AssociationSet.ElementType.FullName = navProp.RelationshipType.FullName)
                 Dim sProp = assocMap.Conditions.Single
                 If uniqueCols Is Nothing Then uniqueCols = New List(Of String)
                 uniqueCols.Add(sProp.Column.Name)
              End If
           Next
           If uniqueCols IsNot Nothing Then
              Dim propList = uniqueCols.ToArray()
              context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IX_" & tableName & "_" & String.Join("_", propList) _
                 & " ON " & schema & "." & tableName & "(" & String.Join(",", propList) & ")")
           End If
        Next
    End If
End Sub
BlueMonkMN
fonte
0

Para aqueles que usam as primeiras configurações de código, você também pode usar o objeto IndexAttribute como um ColumnAnnotation e definir sua propriedade IsUnique como true.

No exemplo:

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

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

Isso criará um índice exclusivo chamado IX_name na coluna Nome.

Pascal Charbonneau
fonte
0

Desculpe pela resposta tardia, mas achei bom tê-lo com você

Eu postei sobre isso no projeto de código

Em geral, depende dos atributos que você coloca nas classes para gerar seus índices exclusivos

Wahid Bitar
fonte