使用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> 这几个类型。

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

01-EF Core笔记之创建模型的更多相关文章

  1. EF Core中避免贫血模型的三种行之有效的方法(翻译)

    Paul Hiles: 3 ways to avoid an anemic domain model in EF Core 1.引言 在使用ORM中(比如Entity Framework)贫血领域模型 ...

  2. EF Core 快速上手——创建应用的DbContext

    系列文章 EF Core 快速上手--EF Core 入门 EF Core 快速上手--EF Core的三种主要关系类型 本节导航 定义应用的DbContext 创建DbContext的一个实例 创建 ...

  3. EFcore笔记之创建模型

    排除属性:NotMapped NotMapped:排除属性在CodeFirst的时候在数据库里不创建该属性   public class Blog { public int BlogId { get; ...

  4. ASP.NET Core 使用 SQLite 教程,EF SQLite教程,修改模型更新数据库,适合初学者看懂详细、简单教程

    SQLIte 操作方便,简单小巧,这里笔者就不再过多介绍,感兴趣可以到以下博文 https://blog.csdn.net/qq_31930499/article/details/80420246 文 ...

  5. Azure Cosmos DB (三) EF Core 操作CURD

    一,引言 接着上一篇使用 EF Core 操作 Azure CosmosDB 生成种子数据,今天我们完成通过 EF Core 实现CRUD一系列功能.EF Core 3.0 提供了CosmosDB 数 ...

  6. [翻译 EF Core in Action 1.9] 掀开EF Core的引擎盖看看EF Core内部是如何工作的

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

  7. EF Core 简单使用介绍

    EF Core 是一个ORM(对象关系映射),它使 .NET 开发人员可以使用 .NET对象操作数据库,避免了像ADO.NET访问数据库的代码,开发者只需要编写对象即可. EF Core 支持多种数据 ...

  8. asp.net EF core 系列 作者:懒懒的程序员一枚

    asp.net core 系列 19 EFCore介绍写作逻辑一 .概述1.1 比较EF Core 和EF61.2 EF Core数据库提供程序 1.3 引用程序添加数据库提供程序1.4 获取Enti ...

  9. 【asp.net core 系列】8 实战之 利用 EF Core 完成数据操作层的实现

    0. 前言 通过前两篇,我们创建了一个项目,并规定了一个基本的数据层访问接口.这一篇,我们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中需要注意的地方. 1. 添加EF Core 先 ...

随机推荐

  1. Oracle10g安装步骤(一)

    本例使用安装程序:10201_database_win32 首先将所有文件提取解压出来后,执行setup.exe 安装步骤如下:

  2. 【2018寒假集训Day 1】【位运算】生成字符串

    生成字符串(strs) [问题描述] 假设字符串只由字符“0”,“1”,“”组成,其中字符“”表示该字符可由 字符“0”或“1”替代. 现有一些字符串,根据这些字符串生成所有可生成的字符串.如: {1 ...

  3. Android 如何动态添加 View 并显示在指定位置。

    引子 最近,在做产品的需求的时候,遇到 PM 要求在某个按钮上添加一个新手引导动画,引导用户去点击.作为 RD,我哗啦啦的就写好相关逻辑了.自测完成后,提测,PM Review 效果. 看完后,PM ...

  4. Pycharm报错连接linux服务器报错:Could not verify `ssh-rsa` host key with fingerprint

    忘记了截图,后来解决了就懒得再去重新制造错误了.大概记得是通过ssh连接linux时,报错 Could not verify `ssh-rsa` host key with fingerprint . ...

  5. linux命令之less命令

    一.我查看日志特别喜欢用less命令来查看,下面给大家讲解下使用. less(选项)(参数) 选项如下: -e:文件内容显示完毕后,自动退出: -f:强制显示文件: -g:不加亮显示搜索到的所有关键词 ...

  6. 【Android - 进阶】之PopupWindow的使用

    创建一个类继承自PopupWindow,编写自定义的PopupWindow类.示例代码如下: import android.app.Activity; import android.graphics. ...

  7. JDK官方下载

    平时进行java开发时避免不了使用jdk,而现在jdk版本已经到1.9了,但是之前版本下载在官方网站就不好找了(主要还是因为网站是英文的): 进入官网下载jdk的前提是进入官网,直接百度搜jdk下载也 ...

  8. 共享共建会让中国的5G加速吗?

    9月9号,中国联通正式公告,已与中国电信签署<5G网络共建共享框架合作协议书>,将在全国范围内合作共建5G接入网络. 这则消息堪称爆炸性新闻,但却看不到什么深度分析,评论文章除了强调&qu ...

  9. Android Application 详细介绍

    一.先看看文档里怎么说 Base class for those who need to maintain global application state. You can provide your ...

  10. 蓝牙5.0芯片NRF52810和NRF52832可进行mesh组网

    提供智能化mesh照明解决方案,在现有传统灯具的基础上,插入NRF52832/52810的照明Mesh模块,可以迅速升级现有的传统灯具,配合手机APP和服务器系统,使每一盏灯成为物联网的一个智能节点, ...