01-EF Core笔记之创建模型

使用EF Core的第一步是创建数据模型,模型建的好,下班走的早。EF Core本身已经设置了一系列约定来帮我们快速的创建模型,例如表名、主键字段等,毕竟约定大于配置嘛。如果你想改变默认值,很简单,EF Core提供了Fluent API或Data Annotations两种方式允许我们定制数据模型。

Fluent API 与 Data Annotations

FluentAPI方式和Data Annotations方式,FluentAPI是通过代码语句配置的,Data Annotations是通过特性标注配置的,FluentAPI的方式更加灵活,实现的功能也更多。优先级为:FluentAPI>Data Annotations>Conventions。

数据标注方式比较简单,在类或字段上添加特性标注即可,对实体类型有一定的入侵。

FluentAPI方式通过在OnModelCreating方法中添加代码逻辑来完成,也可以通过实现IEntityTypeConfiguration<T>类来完成,方式灵活,更能更加强大。

OnModelCreating方式:

modelBuilder.Entity<Role>()
    .Property(m => m.RoleName)
    .IsRequired();

IEntityTypeConfiguration<T>方式:

先定义IEntityTypeConfiguration<T>的实现:

public class BookConfigration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.HasKey(c => c.Id);

        builder.Property(c => c.Name)
            .HasMaxLength(100)
            .IsRequired();
    }
}

然后再OnModelCreating中添加调用:

//加载单个Configuration
modelBuilder.ApplyConfiguration(new BookConfigration());

//加载程序集中所有Configuration
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);

主键、备用键

主键与数据库概念相一致,表示作为数据行的唯一标识;备用键是与主键相对应的一个概念,备用键字段的值可以唯一标识一条数据,它对应数据库的唯一约束。

数据标识方式只能配置主键,使用Key特性,备用键只能通过FluentAPI进行配置。

FluentAPI方式配置的代码如下:

modelBuilder.Entity<Car>()
    .HasKey(c=>c.Id)    //主键
    .HasAlternateKey(c => c.LicensePlate);  //备用键

备用键可以是组合键,通过FluentAPI配置如下:

modelBuilder.Entity<Car>()
    .HasAlternateKey(c => new { c.State, c.LicensePlate });     //组合备用键

必填和选填

映射到数据库的必填和可空,在约定情况下,CLR中可为null的属性将被映射为数据库可空字段,不能为null的属性映射为数据库的必填字段。注意:如果CLR中属性不能为null,则无论如何配置都将为必填。

也就是说,如果能为null,则默认都是可空字段,因此在配置时,只需要配置是否为必填即可。

数据标注方式使用Required特性进行标注。

FluentAPI方式代码如下:

modelBuilder.Entity<Blog>()
    .Property(b => b.Url)
    .IsRequired();

最大长度

最大长度设置了数据库字段的长度,针对string类型、byte[]类型有效,默认情况下,EF将控制权交给数据库提供程序来决定。

数据标注方式使用MaxLength(length)特性进行标注

FluentAPI方式代码如下:

builder.Property(c => c.Name)
    .HasMaxLength(100)
    .IsRequired();

排除/包含属性或类型

默认情况下,如果你的类型中包含一个字段,那么EF Core都会将它映射到数据库中,导航属性亦是如此。如果不想映射到数据库,需要进行配置。

数据标注方式,使用NotMapped特性进行标注;

FluentAPI方式使用Ignore方法,代码如下:

//忽略类型
modelBuilder.Ignore<BlogMetadata>();

//忽略属性
modelBuilder.Entity<Blog>()
    .Ignore(b => b.LoadedFromDatabase);

如果一个属性或类型不在实体中,但是又想包含在数据库映射中时,我们只能通过Fluent API进行配置:

//包含类型
modelBuilder.Entity<AuditEntry>();      

//包含属性,又叫做阴影属性,它会被映射到数据库中
modelBuilder.Entity<Blog>()
    .Property<DateTime>("LastUpdated");

阴影属性

阴影属性指的是在实体中未定义的属性,而在EF Core中模型中为该实体类型定义的属性,这些类型只能通过变更跟踪器进行维护。

阴影属性的定义:

modelBuilder.Entity<Blog>().Property<DateTime>("LastUpdated");

为阴影属性赋值:

context.Entry(myBlog).Property("LastUpdated").CurrentValue = DateTime.Now;

查询时使用阴影属性:

var blogs = context.Blogs
    .OrderBy(b => EF.Property<DateTime>(b, "LastUpdated"));

索引

索引是用来提高查询效率的,在EF Core中,索引的定义仅支持FluentAPI方式。

FluentAPI方式代码:

modelBuilder.Entity<Blog>()
    .HasIndex(b => b.Url);

可以配合唯一约束创建索引:

modelBuilder.Entity<Blog>()
    .HasIndex(b => b.Url)
    .IsUnique();

EF支持复合索引:

modelBuilder.Entity<Person>()
    .HasIndex(p => new { p.FirstName, p.LastName });

并发控制

EF Core支持乐观的并发控制,何谓乐观的并发控制呢?原理大致是数据库中每行数据包含一个并发令牌字段,对改行数据的更新都会出发令牌的改变,在发生并行更新时,系统会判断令牌是否匹配,如果不匹配则认为数据已发生变更,此时会抛出异常,造成更新失败。使用乐观的并发控制可提高数据库性能。

按照约定,EF Core不会设置任何并发控制的令牌字段,但是我们可以通过Fluent API或数据标注进行配置。

数据标注使用ConcurrencyCheck特性标注。除此之外,将数据库字段标记为Timestamp,则会被认为是RowVersion,也能起到并发控制的功能。

public class Blog
{
    public int BlogId { get; set; }

    [ConcurrencyCheck]
    public string Url { get; set; }
    
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

FluentAPI 方式代码如下:

//并发控制令牌
modelBuilder.Entity<Person>()
    .Property(p => p.LastName)
    .IsConcurrencyToken();

//行版本号
modelBuilder.Entity<Blog>()
    .Property(p => p.Timestamp)
    .IsRowVersion();

实体之间的关系

实体之间的关系,可以参照数据库设计的关系来理解。EF是实体框架,它的实体会映射到关系型数据库中。所以通过关系型数据库的表之间的关系更容易理解实体的关系。

在数据库中,数据表之间的关系可以分为一对一、一对多、多对多三种,在实体之间同样有这三种关系,但是EF Core仅支持一对一、一对多关系,如果要实现多对多关系,则需要通过关系实体进行关联。

一对一的关系

以下面的实体关系为例:

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
    public int BlogImageId { get; set; }
    public byte[] Image { get; set; }
    public string Caption { get; set; }

    public int BlogForeignKey { get; set; }
    public Blog Blog { get; set; }
}

每一个Blog对应一个BlogImage,通过Blog可以加载到对应的BlogImage对象,对应的数据库配置如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(p => p.BlogImage)
        .WithOne(i => i.Blog)
        .HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}

一对多的关系

以下面的实体对象为例:

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

每个Blog对应多个Post,而每个Post对应一个Blog,对应的数据库配置如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .IsRequired();
}

多对多的关系

多对多的关系需要我们定义一个关系表来完成。例如下面的实体对象:

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public List<PostTag> PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

Blog和Tag是多对多的关系,显然无论在Blog或Tag中定义外键都不合适,此时就需要一张关系表来进行关联,这张表就是BlogTag表。对应的关系配置如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<PostTag>()
        .HasKey(pt => new { pt.PostId, pt.TagId });

    modelBuilder.Entity<PostTag>()
        .HasOne(pt => pt.Post)
        .WithMany(p => p.PostTags)
        .HasForeignKey(pt => pt.PostId);

    modelBuilder.Entity<PostTag>()
        .HasOne(pt => pt.Tag)
        .WithMany(t => t.PostTags)
        .HasForeignKey(pt => pt.TagId);
}

生成的值

这个功能我没有试验成功。按照官方文档,定义如下实体:

public class Book
{
    [Key]
    public Guid Id { get; set; }

    [MaxLength(100)]
    public string Name { get; set; }

    public decimal Price { get; set; }

    public DateTime CreateTime { get; set; }
}

然后定义DateTime值生成器:

public class DateTimeGenerator : ValueGenerator<DateTime>
{
    public override bool GeneratesTemporaryValues => true;

    public override DateTime Next(EntityEntry entry) => DateTime.Now;
}

最后在FluentAPI中进行配置:

builder.Property(c => c.CreateTime)
    .HasValueGenerator<DateTimeGenerator>()
    .ValueGeneratedOnAddOrUpdate();

按照我的理解应该可以在添加和更新时设置CreateTime的值,并自动保存到数据库,但是值仅在Context中生成,无法保存到数据库中。或许是我理解的不对,后续再进行研究。

继承

关于继承关系如何在数据库中呈现,目前有三种常见的模式:

  • TPH(table-per-hierarchy):一张表存放基类和子类的所有列,使用discriminator列区分类型,目前EF Core仅支持该模式
  • TPT(table-per-type ):基类和子类不在同一个表中,子类对应的表中仅包含基类表的主键和基类扩展的字段,目前EF Core不支持该模式
  • TPC(table-per-concrete-type):基类和子类不在同一个表中,子类中包含基类的所有字段,目前EF Core不支持该模式

EF Core仅支持TPH模式,基类和子类数据将存储在同一个表中。当发现有继承关系时,EF Core会自动维护一个名为Discriminator的阴影属性,我们可以设置该字段的属性:

modelBuilder.Entity<Blog>()
    .Property("Discriminator")
    .HasMaxLength(200);

EF Core允许我们通过FluentAPI的方式自定义鉴别器的列名和每个类对应的值:

modelBuilder.Entity<Blog>()
    .HasDiscriminator<string>("blog_type")
    .HasValue<Blog>("blog_base")
    .HasValue<RssBlog>("blog_rss");

查询类型

查询类型很有用,EF Core不会对它进行跟踪,也不允许新增、修改和删除操作,但是在映射到视图、查询对象、Sql语句查询、只读库的表等情况下用到。

例如创建视图:

db.Database.ExecuteSqlCommand(
    @"CREATE VIEW View_BlogPostCounts AS 
        SELECT b.Name, Count(p.PostId) as PostCount 
        FROM Blogs b
        JOIN Posts p on p.BlogId = b.BlogId
        GROUP BY b.Name");

对应的查询视图:

public class BlogPostsCount
{
    public string BlogName { get; set; }
    public int PostCount { get; set; }
}

使用FluentAPI配置查询视图:

modelBuilder
    .Query<BlogPostsCount>().ToView("View_BlogPostCounts")
    .Property(v => v.BlogName).HasColumnName("Name");

值转换

值转换允许在写入或读取数据时,将数据进行转换(既可以是同类型转换,例如字符串加密解密,也可以是不同类型转换,例如枚举转换为int或string等)。

这里介绍两个概念

  • ModelClrType:模型实体的类型
  • ProviderClrType:数据库提供程序支持的类型

举个例子,string类型,对应数据库提供程序也是string类型,而枚举类型,对数据库提供程序来说没有与它对应的类型,则需要进行转换,至于如何转换、转换成什么类型,则有值转换器(Value Converter)进行处理。

值转换器包含两个Func表达式,用以提供ModelClrType和ProviderClrType的互相转换,例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}

该示例代码将的值转化器提供了枚举类型到字符串的互转。这里只是为了演示,真实场景中,EF Core已经提供了枚举到字符串的转换器,我们只需要直接使用即可。

除了使用Func表达式,我们还可以构造值转换器实例,例如:

var converter = new ValueConverter<EquineBeast, string>(
    v => v.ToString(),
    v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion(converter);

EF Core已经内置了常用的值转换器,例如字符串和枚举的转换器,我们可以直接使用:

var converter = new EnumToStringConverter<EquineBeast>();

modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion(converter);

所有内置的值转换器都是无状态(stateless)的,所以只需要实例化一次,并在多个模型中进行使用。

值转换器还有另外一个用法,即无需实例化转换器,只需要告诉EF Core需要使用的转换器类型即可,例如:

modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion<string>();

值转换器的一些限制:

  • null值无法进行转换
  • 到目前位置还不支持一个字段到多列的转换
  • 会影响构造查询参数,如果造成了影响将会生成警告日志

实体构造函数

EF Core支持实体具有有参的构造函数,默认情况下,EF Core使用无参构造函数来实例化实体对象,如果发现实体类型具有有参的构造函数,则优先使用有参的构造函数。

使用有参构造函数需要注意:

  • 参数名应与属性的名字、类型相匹配
  • 如果参数中不具有所有字段,则在调用构造函数完成后,对未包含字段进行赋值
  • 使用懒加载时,构造函数需要能够被代理类访问到,因此需要构造函数为public或protected
  • 暂不支持在构造函数中使用导航属性

使用构造函数时,比较好玩的是支持依赖注入,我们可以在构造函数中注入DbContextIEntityTypeILazyLoaderAction<object, string> 这几个类型。

以上便是常用的构建模型的知识点,更多内容在用到时再进行学习。

posted @ 2019-07-14 00:12 thz 阅读(...) 评论(...) 编辑 收藏