Code Copied

EF Code First系列–02. 属性特性的约定和配置

1. 理解领域模型(Domain Model)

在我们开始编写实体类之前,请先大致理解一下领域模型。

从概念上讲,领域模型并不是我们所创建的VS工程中的包含实体类的工程,这样的工程只能称之为对象模型。
领域模型是针对某个特定的问题领域设计的,力图对领域中的实体与关系设计的流程和数据进行抽象。
这里我们需要区分对象模型和领域模型,对象模型仅仅是一系列的对象,而领域模型则是用于实现一系列需求的对象模型。
对象模型是相对孤立的,而领域模型依赖于业务需求

更深层次地,领域模型涉及到的含义比以上这些文字复杂地多(这里我们只是介绍领域模型,而不是为了深入研究DDD相关的东西)。

2. Code First中的属性特性

2.1 Length(长度配置)

e30qt5ka

在上一节的示例中,我们编写了一个非常干净的Product实体类,EF生成的映射表Products如下:

image

Name和SerialNumber的长度是max的,实际上只需要几十个字符就能满足它们。
使用nvarchar(max)会浪费数据库空间,而且损失性能。

Length能够约定字段的最大长度,Length属于System.ComponentModel.DataAnnotations空间。

public class Product
{
    public int Id { get; set; }
    [MinLength(5)]
    [MaxLength(100)]
    public string Name { get; set; }
    [MaxLength(36)]
    public string SerialNumber { get; set; }
}

再次运行程序后,打开Products表将得到下面的结果。

image

MaxLength特性能够约定属性的长度,通过Entity Framework映射成数据表的字段长度。
MinLength在数据表中没有起到作用,是因为数据表里面没有字段最小长度的设定,而实体则有属性值最小长度的约束。
StringLength不仅能够约定数据长度,而且能够对数据长度进行验证,只要指定MaxLength或MinLenght对应的ErrorMessage即可,StringLength通常运用于ASP.NET MVC或Dynamic Data中。

配置Length也能通过EF的Fluent API方式实现(推荐使用Fluent API去,尽量不要使用Data Annotation方式,Data Annotation方式会让代码看起来不干净)
Fluent API方式:

public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(p => p.Name).HasMaxLength(100);
        Property(p => p.SerialNumber).HasMaxLength(36);
    }
}

2.2 DataType(数据类型)

bopps3yv

Product表中添加一个Picture,将其数据库对应字段类型指定为image

Data Annotation方式:

public class Product
{
    public int Id { get; set; }
    [MinLength(5)]
    [MaxLength(100)]
    public string Name { get; set; }
    [MaxLength(36)]
    public string SerialNumber { get; set; }

    [Column(TypeName = "image")]
    public byte[] Picture { get; set; }
}

image

Fluent API方式:

public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(p => p.Name).HasMaxLength(100);
        Property(p => p.SerialNumber).HasMaxLength(36);

        Property(p => p.Picture).HasColumnType("image");
    }
}

2.3 Nullability and the Required Configuration(空和非空配置)

lxqvikiy

实体属性映射为数据库字段时,默认是可空的。当要求一些属性非空时,EF提供了Required方式。

例如:Product实体中,要求Name属性是非空的,那么映射成数据库字段时则为not null的。

Data Annotation方式:

public class Product
{
    public int Id { get; set; }
    [MinLength(5)]
    [MaxLength(100)]
    [Required]
    public string Name { get; set; }
    [MaxLength(36)]
    public string SerialNumber { get; set; }

    [Column(TypeName = "image")]
    public byte[] Picture { get; set; }
}

Fluent API方式:

public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(p => p.Name).HasMaxLength(100).IsRequired();
        Property(p => p.SerialNumber).HasMaxLength(36);

        Property(p => p.Picture).HasColumnType("image");
    }
}

2.4 Map Keys(映射主键)

l3sv2jxz

默认情况下EF会将实体中的Id属性或者[TypeName] + Id属性映射为数据库的主键。
如果在Product实体中指定了Id列,那么Id列为主键;如果指定了ProductId列,那么ProductId列为主键。

当然有些情况下,实体中并不包含这种规则的属性,例如下面的Customer实体(CustomerNo为主键属性)

public class Customer
{
    public int CustomerNo { get; set; }
    public string CustomerName { get; set; }
}

EF约定了每个实体必须拥有主键属性,在Customer实体类中,CustomerNo作为主键列,但它不符合EF的主键命名约定,所以运行时会出现下面的异常EntityType ‘Customer‘ has no key defined.
image

需要为非约定性命名主键属性指定Key才能避开这个异常

Data Annotation方式:

public class Customer
{
    [Key]
    public int CustomerNo { get; set; }
    public string CustomerName { get; set; }
}

Fluent API方式:

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.CustomerNo);
    }
}

运行程序后,CustomerNo就自动映射为Customers表的主键了。

image

2.5 Configuring Database-Generated Properties(属性值由数据库生成)

szcpcqx2

在EF中,当实体的主键属性是int类型时,映射到数据库表的主键列是自增长的

image

static void Main(string[] args)
{
    Database.SetInitializer(new DropCreateDatabaseIfModelChanges<PmsContext>());

    using (var context = new PmsContext())
    {
        var product = new Product
        {
            Name = "iPad Air",
            SerialNumber = "00000001"
        };

        var customer = new Customer
        {
            CustomerName = "John"
        };

        context.Products.Add(product);
        context.Customers.Add(customer);
        context.SaveChanges();
    }

    Console.WriteLine("Good Job!");
    Console.ReadKey();
}

image

假如现在将CustomerNo属性的数据类型更改为GUID,运行程序后CustomerNo字段生成的是无效的GUID值。

public class Customer
{
    [Key]
    public Guid CustomerNo { get; set; }
    public string CustomerName { get; set; }
}

image

再重复运行一次程序,会产生主键冲突的异常。
DatabaseGenerated特性能解决这一问题。

Data Annotation方式:

public class Customer
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid CustomerNo { get; set; }
    public string CustomerName { get; set; }
}

image

Fluent API方式:

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.CustomerNo);
        Property(c => c.CustomerNo).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}

2.6 Configuring TimeStamp/RowVersion Fields for Optimistic Concurrency(乐观并发配置时间戳或行版本)

ahysg3cb

2.6.1 理解RowVersion和TimeStamp(行版本和时间戳)

每个数据库都有一个计数器,当对数据库中包含rowversion列的表执行插入或者更新操作时,该计数器值就会增加。此计数器是数据库就是行版本。
RowVersion通常用作给表行加版本戳的机制,存储大小为8个字节。rowversion数据类型只是递增的数字,不保留日期或时间。

TimeStamp的和RowVersion拥有相同的功能,但在创建时用法稍微不同。

2.6.2 如何使用RowVersion和TimeStamp

使用TimeStamp

在 CREATE TABLE 或 ALTER TABLE 语句中,不必为 timestamp 数据类型指定列名,例如:

image

使用RowVersion
如果不指定列名,则SQL Server数据库引擎将生成timestamp列名。
但RowVersion却没有这样的行为,在使用RowVersion时,必须指定列名,例如:

image

2.6.3 在EF中使用RowVersion和TimeStamp

Data Annotation方式:

public class Customer
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid CustomerNo { get; set; }
    public string CustomerName { get; set; }
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

Fluent API方式:

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.CustomerNo)
            .Property(c => c.CustomerNo).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        Property(c => c.RowVersion).IsRowVersion();
    }
}

image

2.7 Configuring Non-Timestamp Fields for Concurrency(配置非时间戳的并发字段)

25gnqbfj

有些数据库可能并不像SQL Server一样提供了RowVersion或者TimeStamp数据类型,这种情况下无法配置RowVersion和TimeStamp字段,但我们仍然需要对数据进行并发控制。
EF提供了为非RowVersion或TimeStamp数据类型的列进行并发检查。

Data Annotation方式:

public class Product
{
    public int Id { get; set; }
    [MinLength(5)]
    [MaxLength(100)]
    [Required]
    public string Name { get; set; }
    [MaxLength(36)]
    [ConcurrencyCheck]
    public string SerialNumber { get; set; }

    [Column(TypeName = "image")]
    public byte[] Picture { get; set; }
}

Fluent API方式:

public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(p => p.Name).HasMaxLength(100).IsRequired();
        Property(p => p.SerialNumber).HasMaxLength(36)
            .IsConcurrencyToken();

        Property(p => p.Picture).HasColumnType("image");
    }
}

2.8 Mapping to Non-Unicode Database Types(映射非Unicode的数据类型)

koiuielc

EF为string类型的属性生成nvarchar的数据类型,这对于那些包含双字节字符的字段是OK的;
但是对于那些仅包含单字节数据的字段就显得冗余,而且会损失性能,给这样的字段指定varchar类型是最好的方式。

Fluent API提供了IsUnicode方法来设置是否使用varchar还是nvarchar(Data Annotation没有相应的方法)。

public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(p => p.Name).HasMaxLength(100).IsRequired();
        Property(p => p.SerialNumber).HasMaxLength(36)
            .IsUnicode(false)
            .IsConcurrencyToken();

        Property(p => p.Picture).HasColumnType("image");
    }
}

image

2.9 Affecting the Precision and Scale of Decimals(设置decimal数据类型的长度和精度)

0c0cofq5

当实体中包含decimal数据类型的属性时,EF会自动将其映射成为为decimal(18,2)数据类型的字段。

public class Product
{
    public int Id { get; set; }
    [MinLength(5)]
    [MaxLength(100)]
    [Required]
    public string Name { get; set; }
    [MaxLength(36)]
    [ConcurrencyCheck]
    public string SerialNumber { get; set; }

    public decimal Price { get; set; }

    [Column(TypeName = "image")]
    public byte[] Picture { get; set; }
}
image
要改变decimal的长度和精度可以通过Fluent API的HasPrecision方法(Data Annotation没有对应的方式)。
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(p => p.Name).HasMaxLength(100).IsRequired();
        Property(p => p.SerialNumber).HasMaxLength(36)
            .IsUnicode(false)
            .IsConcurrencyToken();

        Property(p => p.Price).HasPrecision(10, 4);

        Property(p => p.Picture).HasColumnType("image");
    }
}

image

3. 参考引用和源码

3.1 参考引用

关于TimeStamp和RowVersion: http://technet.microsoft.com/zh-cn/library/ms182776.aspx
引用书籍:file_extension_pdfProgramming Entity Framework Code First

3.2 本文源代码下载

下载链接:http://blog.64cm.com/source/EFConsoleApplication-02.zip