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
- 暂不支持在构造函数中使用导航属性
使用构造函数时,比较好玩的是支持依赖注入,我们可以在构造函数中注入DbContext
、IEntityType
、ILazyLoader
、Action<object, string>
这几个类型。
以上便是常用的构建模型的知识点,更多内容在用到时再进行学习。
01-EF Core笔记之创建模型的更多相关文章
- EF Core中避免贫血模型的三种行之有效的方法(翻译)
Paul Hiles: 3 ways to avoid an anemic domain model in EF Core 1.引言 在使用ORM中(比如Entity Framework)贫血领域模型 ...
- EF Core 快速上手——创建应用的DbContext
系列文章 EF Core 快速上手--EF Core 入门 EF Core 快速上手--EF Core的三种主要关系类型 本节导航 定义应用的DbContext 创建DbContext的一个实例 创建 ...
- EFcore笔记之创建模型
排除属性:NotMapped NotMapped:排除属性在CodeFirst的时候在数据库里不创建该属性 public class Blog { public int BlogId { get; ...
- ASP.NET Core 使用 SQLite 教程,EF SQLite教程,修改模型更新数据库,适合初学者看懂详细、简单教程
SQLIte 操作方便,简单小巧,这里笔者就不再过多介绍,感兴趣可以到以下博文 https://blog.csdn.net/qq_31930499/article/details/80420246 文 ...
- Azure Cosmos DB (三) EF Core 操作CURD
一,引言 接着上一篇使用 EF Core 操作 Azure CosmosDB 生成种子数据,今天我们完成通过 EF Core 实现CRUD一系列功能.EF Core 3.0 提供了CosmosDB 数 ...
- [翻译 EF Core in Action 1.9] 掀开EF Core的引擎盖看看EF Core内部是如何工作的
Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...
- EF Core 简单使用介绍
EF Core 是一个ORM(对象关系映射),它使 .NET 开发人员可以使用 .NET对象操作数据库,避免了像ADO.NET访问数据库的代码,开发者只需要编写对象即可. EF Core 支持多种数据 ...
- asp.net EF core 系列 作者:懒懒的程序员一枚
asp.net core 系列 19 EFCore介绍写作逻辑一 .概述1.1 比较EF Core 和EF61.2 EF Core数据库提供程序 1.3 引用程序添加数据库提供程序1.4 获取Enti ...
- 【asp.net core 系列】8 实战之 利用 EF Core 完成数据操作层的实现
0. 前言 通过前两篇,我们创建了一个项目,并规定了一个基本的数据层访问接口.这一篇,我们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中需要注意的地方. 1. 添加EF Core 先 ...
随机推荐
- 读取FANUC进给倍率
读取FANUC机床的倍率信息需要用到 FOCAS链接库. 根据FANUC的连接手册可以知道,进给倍率信号存在 Gn012寄存器中.在机床上更改倍率开关,发现G0012寄存器值变化,经验证G0012就是 ...
- Stream系列(七)distinct方法使用
EmployeeTestCase.java package com.example.demo; import lombok.Data; import lombok.ToString; import l ...
- vim的各项指令
lesson1 <ESC> 保证进入正常模式 :q!回车 退出编辑器 x 删除光标所在的字母 i 添加内容 A 自动追加内容到行尾 :wq 保存文件并退出 lesson2 dw 删除某 ...
- python网络爬虫之自动化测试工具selenium[二]
目录 前言 一.获取今日头条的评论信息(request请求获取json) 1.分析数据 2.获取数据 二.获取今日头条的评论信息(selenium请求获取) 1.分析数据 2.获取数据 房源案例(仅供 ...
- 【日常错误】spring-boot配置文件读取不到
最近在用spring-boot做项目时,遇到自定义的配置文件无法读取到的问题,通过在appcation.java类上定义@PropertySource(value = {"classpath ...
- AJAX与Django
AJAX 什么是AJAX? AJAX不是JavaScript的规范,它的缩写:Asynchronous JavaScript and XML,意思就是用JavaScript执行异步网络请求.提交任务之 ...
- exc_bad_instruction(code=EXC_I386_INVOP,subcode=0x0) 错误
对象存储异常 对象存储要遵守NSCoding协议 #import "EmotionModel.h" @interface EmotionModel()<NSCoding> ...
- gulp+webpack+angular1的一点小经验(第一部分gulp与webpack的整合)
时间匆匆如流水继上周熟悉了gulp的初步安装与环境配置以后,我的项目又进入了新的阶段! 这篇文章将把我这一周遇到的一些问题,以及解决的方式做一个小小的总结,不一定记的完整,但都是个人的一点经验,分享给 ...
- 华为云备案服务全面升级,EI助力带来极速体验
华为云备案"电子化核验"正式发布,备案更轻松.更快捷.自2019年9月12日起,华为云用户申请办理ICP备案可以通过华为云APP进行"ICP备案主体真实身份信息采集&qu ...
- 【开发者portal在线开发插件系列三】字符串 及 可变长度字符串
基础篇 基础场景见上面两个帖子,这里单独说明字符串和可变长度字符串的用法. 话不多说,开始今天的演(表)示(演) Profile和插件开发 添加一个string类型的属性: 在插件里添加一条数据上报消 ...