Anotaciones de datos de Code First

Nota:

Solo EF4.1 y versiones posteriores: las características, las API, etc. que se tratan en esta página se han incluido a partir de Entity Framework 4.1. Si usa una versión anterior, no se aplica parte o la totalidad de la información.

El contenido de esta página está adaptado a partir de un artículo escrito originalmente por Julie Lerman (<http://thedatafarm.com>).

Code First de Entity Framework permite usar sus propias clases de dominio para representar el modelo en el que EF se basa para realizar consultas, seguimiento de cambios y actualizaciones de funciones. Code First aprovecha un patrón de programación denominado "convención sobre la configuración". Code First presupone que las clases siguen las convenciones de Entity Framework y, en ese caso, detectará automáticamente cómo realizar su trabajo. Sin embargo, si las clases no siguen esas convenciones, puede agregar configuraciones a las clases para proporcionar a EF la información necesaria.

Code First proporciona dos maneras de agregar estas configuraciones a las clases. Una usa atributos simples denominados DataAnnotations y la segunda usa la API fluida de Code First, que proporciona una manera de describir las configuraciones imperativamente en el código.

Este artículo se centrará en el uso de DataAnnotations (en el espacio de nombres System.ComponentModel.DataAnnotations) para configurar las clases y resaltar las configuraciones que más se suelen necesitar. DataAnnotations también se entiende por una serie de aplicaciones .NET, como ASP.NET MVC, que permite que estas aplicaciones aprovechen las mismas anotaciones para las validaciones del lado cliente.

El modelo de

Mostraré DataAnnotations de Code First con un par simple de clases: Blog y Post.

    public class Blog
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public DateTime DateCreated { get; set; }
        public string Content { get; set; }
        public int BlogId { get; set; }
        public ICollection<Comment> Comments { get; set; }
    }

De por sí, las clases Blog y Post siguen la convención de Code First y no requieren ajustes para habilitar la compatibilidad de EF. Sin embargo, también puede usar las anotaciones para proporcionar más información a EF sobre las clases y la base de datos a la que se asignan.

 

Clave

Entity Framework se basa en cada entidad que tiene un valor de clave que se usa para el seguimiento de entidades. Una convención de Code First son las propiedades de clave implícitas. Code First buscará una propiedad denominada “Id” o una combinación de nombre de clase e “Identificador”, como “BlogId”. Esta propiedad se asignará a una columna de clave principal de la base de datos.

Las clases Blog y Post siguen esta convención. ¿Y si no lo hicieran? ¿Qué ocurre si Blog usó el nombre primaryTrackingKey en su lugar, o incluso foo? Si Code First no encuentra una propiedad que coincida con esta convención, producirá una excepción debido al requisito de Entity Framework de tener una propiedad de clave. Puede usar la anotación de clave para especificar qué propiedad se va a usar como EntityKey.

    public class Blog
    {
        [Key]
        public int PrimaryTrackingKey { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

Si usa la característica de generación de base de datos de Code First, la tabla Blog tendrá una columna de clave principal denominada PrimaryTrackingKey, que también se define como Identidad de forma predeterminada.

Blog table with primary key

Claves compuestas

Entity Framework admite claves compuestas: claves principales que constan de más de una propiedad. Por ejemplo, podría tener una clase Passport cuya clave principal es una combinación de PassportNumber y IssuingCountry.

    public class Passport
    {
        [Key]
        public int PassportNumber { get; set; }
        [Key]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

Si intenta usar la clase anterior en el modelo de EF, se producirá una InvalidOperationException:

No se puede determinar la ordenación de clave principal compuesta para el tipo "Passport". Use ColumnAttribute o el método HasKey para especificar un orden para las claves principales compuestas.

Para poder usar claves compuestas, Entity Framework requiere que defina un orden para las propiedades de clave. Puede hacerlo mediante la anotación Column para especificar un orden.

Nota:

El valor de orden es relativo (no basado en índices) por lo que se puede usar cualquier valor. Por ejemplo, 100 y 200 serían aceptables en lugar de 1 y 2.

    public class Passport
    {
        [Key]
        [Column(Order=1)]
        public int PassportNumber { get; set; }
        [Key]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

Si tiene entidades con claves externas compuestas, debe especificar el mismo orden de columnas que usó para las propiedades de clave principal correspondientes.

Solo el orden relativo dentro de las propiedades de clave externa debe ser el mismo, los valores exactos asignados a Order no necesitan coincidir. Por ejemplo, en la siguiente clase, se podrían usar 3 y 4 en lugar de 1 y 2.

    public class PassportStamp
    {
        [Key]
        public int StampId { get; set; }
        public DateTime Stamped { get; set; }
        public string StampingCountry { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 1)]
        public int PassportNumber { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }

        public Passport Passport { get; set; }
    }

Obligatorio

La anotación Required indica a EF que se requiere una propiedad determinada.

Al agregar Required a la propiedad Title se forzará EF (y MVC) para asegurarse de que la propiedad tiene datos en ella.

    [Required]
    public string Title { get; set; }

Sin cambios de marcado ni código adicionales en la aplicación, una aplicación MVC realizará la validación del lado cliente, incluso creando dinámicamente un mensaje mediante los nombres de propiedad y anotación.

Create page with Title is required error

El atributo Required también afectará a la base de datos generada haciendo que la propiedad asignada no acepte valores NULL. Observe que el campo Title ha cambiado a “no nulo”.

Nota:

Puede que en algunos casos no sea posible que la columna de la base de datos no acepte valores NULL, aunque se requiera la propiedad. Por ejemplo, al usar datos de estrategia de herencia de la tabla por jerarquía para varios tipos se almacena en una sola tabla. Si un tipo derivado incluye una propiedad necesaria, no se puede hacer que la columna no admita valores NULL, ya que no todos los tipos de la jerarquía tendrán esta propiedad.

 

Blogs table

 

MaxLength y MinLength

Los atributos MaxLength y MinLength permiten especificar validaciones de propiedades adicionales, como hizo con Required.

Este es el BloggerName con requisitos de longitud. En el ejemplo también se muestra cómo combinar atributos.

    [MaxLength(10),MinLength(5)]
    public string BloggerName { get; set; }

La anotación MaxLength afectará a la base de datos estableciendo la longitud de la propiedad en 10.

Blogs table showing max length on BloggerName column

Tanto la anotación del lado cliente de MVC como la anotación del lado servidor de EF 4.1 respetarán esta validación y, de nuevo, compilarán dinámicamente un mensaje de error: “El campo BloggerName debe ser una cadena o un tipo de matriz con una longitud máxima de '10'." Este mensaje es bastante largo. Muchas anotaciones permiten especificar un mensaje de error con el atributo ErrorMessage.

    [MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

También puede especificar ErrorMessage en la anotación Required.

Create page with custom error message

 

NotMapped

La convención Code First dicta que todas las propiedades que son de un tipo de datos admitido se representan en la base de datos. Pero esto no siempre es así en sus aplicaciones. Por ejemplo, podría tener una propiedad en la clase Blog que crea un código basado en los campos Title y BloggerName. Esa propiedad se puede crear dinámicamente y no es necesario almacenarla. Puede marcar cualquier propiedad que no se asigne a la base de datos con la anotación NotMapped, como esta propiedad BlogCode.

    [NotMapped]
    public string BlogCode
    {
        get
        {
            return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1);
        }
    }

 

ComplexType

No es raro describir las entidades de dominio en un conjunto de clases y, a continuación, colocar esas clases en capas para describir una entidad completa. Por ejemplo, puede agregar una clase denominada BlogDetails al modelo.

    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

Tenga en cuenta que BlogDetails no tiene ningún tipo de propiedad de clave. En el diseño controlado por dominios, BlogDetails se conoce como un objeto de valor. Entity Framework hace referencia a objetos de valor como tipos complejos.  No se puede realizar un seguimiento de los tipos complejos por sí solos.

Sin embargo, como una propiedad de la clase Blog se realizará un seguimiento de BlogDetails como parte de un objeto Blog. Para que Code First reconozca esto, debe marcar la clase BlogDetails como ComplexType.

    [ComplexType]
    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

Ahora puede agregar una propiedad en la clase Blog para representar BlogDetails de ese blog.

        public BlogDetails BlogDetail { get; set; }

En la base de datos, la tabla Blog contendrá todas las propiedades del blog, incluidas las propiedades contenidas en su propiedad BlogDetail. De forma predeterminada, cada una de ellas va precedida del nombre del tipo complejo "BlogDetail".

Blog table with complex type

ConcurrencyCheck

La anotación ConcurrencyCheck permite marcar una o varias propiedades para la comprobación de simultaneidad en la base de datos cuando un usuario edita o elimina una entidad. Si ha estado trabajando con EF Designer, esto se relaciona con establecer una propiedad ConcurrencyMode en Fixed.

Agreguemos ConcurrencyCheck a la propiedad BloggerName para ver cómo funciona.

    [ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

Cuando se llama a SaveChanges, debido a la anotación ConcurrencyCheck en el campo BloggerName, el valor original de esa propiedad se usará en la actualización. El comando intentará buscar la fila correcta filtrando no solo por el valor de clave, sino también por el valor original de BloggerName.  Estas son las partes críticas del comando UPDATE enviado a la base de datos. Puede ver que el comando actualizará la fila que tiene un PrimaryTrackingKey de 1 y un BloggerName de “Julie”, que era el valor original cuando ese blog se recuperó de la base de datos.

    where (([PrimaryTrackingKey] = @4) and ([BloggerName] = @5))
    @4=1,@5=N'Julie'

Si alguien ha cambiado el nombre de blog para ese blog mientras tanto, se producirá un error en esta actualización y obtendrá una DbUpdateConcurrencyException que debe controlar.

 

Marca de tiempo

Es más común usar campos rowversion o timestamp para la comprobación de simultaneidad. Pero en lugar de usar la anotación ConcurrencyCheck, puede usar la anotación de TimeStamp más específica siempre que el tipo de la propiedad sea matriz de bytes. Code First tratará propiedades de Timestamp igual que las propiedades ConcurrencyCheck, pero también garantizará que el campo de base de datos que Code First genere no acepte valores NULL. Solo puede tener una propiedad de marca de tiempo en una clase determinada.

Agregar la siguiente propiedad a la clase Blog:

    [Timestamp]
    public Byte[] TimeStamp { get; set; }

da como resultado que Code First cree una columna de marca de tiempo que no acepta valores NULL en la tabla de base de datos.

Blogs table with time stamp column

 

Tabla y columna

Si deja que Code First cree la base de datos, es posible que desee cambiar el nombre de las tablas y columnas que está creando. También puede usar Code First con una base de datos existente. Pero los nombres de las clases y propiedades del dominio no siempre coinciden con los nombres de las tablas y columnas de la base de datos.

Mi clase se denomina Blog y, por convención, Code First presupone que se asignará a una tabla denominada Blogs. Si no es así, puede especificar el nombre de la tabla con el atributo Table. Aquí, por ejemplo, la anotación especifica que el nombre de la tabla es InternalBlogs.

    [Table("InternalBlogs")]
    public class Blog

La anotación Column es mejor para especificar los atributos de una columna asignada. Puede estipular un nombre, un tipo de datos o incluso el orden en el que aparece una columna en la tabla. Este es un ejemplo del atributo Column.

    [Column("BlogDescription", TypeName="ntext")]
    public String Description {get;set;}

No confunda el atributo TypeName de columna con DataType DataAnnotation. DataType es una anotación que se usa para la interfaz de usuario y se omite mediante Code First.

A continuación, puede ver la tabla después de volver a generarla. El nombre de la tabla ha cambiado a InternalBlogs y la columna Description del tipo complejo es ahora BlogDescription. Dado que el nombre se especificó en la anotación, Code First no usará la convención de iniciar el nombre de columna con el nombre del tipo complejo.

Blogs table and column renamed

 

DatabaseGenerated

Una característica importante de la base de datos es la capacidad de tener propiedades calculadas. Si va a asignar las clases de Code First a las tablas que contienen columnas calculadas, es mejor que Entity Framework no intente actualizar esas columnas. Pero sí es deseable que EF devuelva esos valores de la base de datos después de insertar o actualizar los datos. Puede usar la anotación DatabaseGenerated para marcar esas propiedades en la clase junto con la enumeración Computed. Otras enumeraciones son None y Identity.

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime DateCreated { get; set; }

Puede usar la base de datos generada en columnas byte o marca de tiempo cuando Code First genere por primera vez la base de datos. Si no, solo debe usarla cuando señale a bases de datos existentes porque Code First no podrá determinar la fórmula de la columna calculada.

De forma predeterminada, una propiedad de clave que es un número entero se convertirá en una clave de identidad en la base de datos. Esto es lo mismo que establecer DatabaseGenerated en DatabaseGeneratedOption.Identity. Si no quiere que sea una clave de identidad, puede establecer el valor en DatabaseGeneratedOption.None.

 

Índice

Nota:

Solo de EF 6.1 en adelante: el atributo Index se introdujo en Entity Framework 6.1. Si usa una versión anterior, la información de esta sección no es aplicable.

Puede crear un índice en una o varias columnas mediante IndexAttribute. Agregar el atributo a una o varias propiedades hará que EF cree el índice correspondiente en la base de datos al crear la base de datos, o aplique scaffolding a las llamadas CreateIndex correspondientes si usa migraciones de Code First.

Por ejemplo, el código siguiente hará que se cree un índice en la columna Rating de la tabla Posts de la base de datos.

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index]
        public int Rating { get; set; }
        public int BlogId { get; set; }
    }

De forma predeterminada, el índice se denominará IX_<nombre de propiedad> (IX_Rating en el ejemplo anterior). Sin embargo, también puede especificar un nombre para el índice. En el ejemplo siguiente se especifica que el índice debe denominarse PostRatingIndex.

    [Index("PostRatingIndex")]
    public int Rating { get; set; }

De forma predeterminada, los índices no son únicos, pero puede usar el parámetro con nombre IsUnique para especificar que un índice debe ser único. En el ejemplo siguiente se presenta un índice único en un nombre de inicio de sesión de User.

    public class User
    {
        public int UserId { get; set; }

        [Index(IsUnique = true)]
        [StringLength(200)]
        public string Username { get; set; }

        public string DisplayName { get; set; }
    }

Índices de varias columnas

Los índices que abarcan varias columnas se especifican mediante el mismo nombre en varias anotaciones de índice para una tabla determinada. Al crear índices de varias columnas, debe especificar un orden para las columnas del índice. Por ejemplo, el código siguiente crea un índice de varias columnas en Rating y BlogId denominado IX_BlogIdAndRating. BlogId es la primera columna del índice y Rating es la segunda.

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index("IX_BlogIdAndRating", 2)]
        public int Rating { get; set; }
        [Index("IX_BlogIdAndRating", 1)]
        public int BlogId { get; set; }
    }

 

Atributos de relación: InverseProperty y ForeignKey

Nota:

Esta página proporciona información sobre cómo establecer relaciones en su modelo Code First con anotaciones de datos. Para obtener información general sobre las relaciones en EF y sobre cómo acceder a los datos y manipularlos mediante relaciones, consulte Relaciones y propiedades de navegación.*

La convención de Code First se ocupará de las relaciones más comunes en el modelo, pero hay algunos casos en los que necesita ayuda.

Al cambiar el nombre de la propiedad key de la clase Blog, se creó un problema con su relación con Post

Al generar la base de datos, Code First ve la propiedad BlogId en la clase Post y la reconoce, mediante la convención que coincide con un nombre de clase más Id, como una clave externa para la clase Blog. Pero no hay ninguna propiedad BlogId en la clase de blog. La solución para esto consiste en crear una propiedad de navegación en Post y usar DataAnnotation de ForeignKey para ayudar a Code First a comprender cómo crear la relación entre las dos clases (mediante la propiedad Post.BlogId) así como las formas de especificar restricciones en la base de datos.

    public class Post
    {
            public int Id { get; set; }
            public string Title { get; set; }
            public DateTime DateCreated { get; set; }
            public string Content { get; set; }
            public int BlogId { get; set; }
            [ForeignKey("BlogId")]
            public Blog Blog { get; set; }
            public ICollection<Comment> Comments { get; set; }
    }

La restricción de la base de datos muestra una relación entre InternalBlogs.PrimaryTrackingKey y Posts.BlogId

relationship between InternalBlogs.PrimaryTrackingKey and Posts.BlogId

InverseProperty se usa cuando tiene varias relaciones entre clases.

En la clase Post, es posible que desee realizar un seguimiento de quién escribió una entrada de blog, así como quién la editó. Estas son dos nuevas propiedades de navegación para la clase Post.

    public Person CreatedBy { get; set; }
    public Person UpdatedBy { get; set; }

También deberá agregar la clase Person a la que hacen referencia estas propiedades. La clase Person tiene propiedades de navegación que vuelven a Post, una para todas las publicaciones escritas por la persona y otra para todas las publicaciones actualizadas por esa persona.

    public class Person
    {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<Post> PostsWritten { get; set; }
            public List<Post> PostsUpdated { get; set; }
    }

Code First no puede hacer coincidir las propiedades de las dos clases por sí solo. La tabla de base de datos de Posts debe tener una clave externa para la persona CreatedBy y otra para la persona UpdatedBy, pero Code First creará cuatro propiedades de clave externa: Person_Id, Person_Id1, CreatedBy_Id y UpdatedBy_Id.

Posts table with extra foreign keys

Para solucionar estos problemas, puede usar la anotación InverseProperty para especificar la alineación de las propiedades.

    [InverseProperty("CreatedBy")]
    public List<Post> PostsWritten { get; set; }

    [InverseProperty("UpdatedBy")]
    public List<Post> PostsUpdated { get; set; }

Dado que la propiedad PostsWritten de Person sabe que esto hace referencia al tipo Post, creará la relación con Post.CreatedBy. Del mismo modo, PostsUpdated se conectará a Post.UpdatedBy. Y Code First no creará las claves externas adicionales.

Posts table without extra foreign keys

 

Resumen

DataAnnotations no solo permite describir la validación del cliente y del lado servidor en las clases de Code First, sino que también permite mejorar e incluso corregir las suposiciones que Code First realizará sobre las clases en función de sus convenciones. Con DataAnnotations no solo puede impulsar la generación de esquemas de base de datos, sino que también puede asignar las clases de Code First a una base de datos preexistente.

Aunque es muy flexible, tenga en cuenta que DataAnnotations proporciona solo los cambios de configuración más comunes necesarios que puede realizar en las clases de Code First. Para configurar las clases en otros casos excepcionales, debe buscar el mecanismo de configuración alternativo: la API fluida de Code First.