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 先 ...
随机推荐
- 20190926-2 选题 Scrum立会报告+燃尽图05
此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/8678 一.小组情况组长:迟俊文组员:宋晓丽 梁梦瑶 韩昊 刘信鹏队名:扛 ...
- 超速入门AT指令集 | 我的物联网成长记
[摘要] 在物联网中,AT命令集可用于控制&调测设备.通信模块入网等.本文为您介绍NB-IoT常用的AT命令集及其调测工具. 什么是AT指令集 AT命令,用来控制TE(Terminal Equ ...
- Git及Github
目录 Git及Github的使用 Git的基本介绍 Git命令行操作 1.设置签名 2.创建本地库 3.仓库初始化 4.状态查看 5.添加文件 6.提交文件 7.历史记录 8.前进后退 9.删除文件 ...
- 【2018寒假集训 Day2】【动态规划】钱币兑换(exchange)(自己翻译的2333)
钱币兑换(exchange) 问题描述: Dave偶然获得了未来几天的美元(dollars)与马克(marks)之间的兑换率.例如Dave开始有100marks,请编写个程序帮助Dave找出最好的买卖 ...
- JavaScript笔记三
1.数据类型 - JS中一共分成六种数据类型 - String 字符串 - Number 数值 - Boolean 布尔值 - Null 空值 - Undefined 未定义 - Object 对象 ...
- day 27 网路编程 面向对象多继承
知识补充: 字符串转化为字节 string1 = input(“请输入你的名字”) string1.encode('utf-8') 字节转化为字符串 byte1 = b"alex" ...
- Selenium+Java(六)Selenium 强制等待、显式等待、隐实等待
前言 在实际测试过程中,由于网速或性能方面的原因,打开相应的网页后或在网页上做了相应的操作,网页上的元素可能不会马上加载出来,这个时候需要在定位元素前等待一下,等元素加载出来后再进行定位,根据实际使用 ...
- PHP安装扩展补充说明
上一篇文章中用到了,php的sodium扩展,那么如何安装PHP扩展呢?基于我之前踩过的一些坑,大致整理了几种安装php扩展的方法.已安装sodium为例 1.先做点准备工作,安装sodium依赖 r ...
- 动态规划算法(java)
一.动态规划算法 众所周知,递归算法时间复杂度很高为(2^n),而动态规划算法也能够解决此类问题,动态规划的算法的时间复杂度为(n^2).动态规划算法是以空间置换时间的解决方式,一开始理解起来可能比较 ...
- redis - redis数据结构与API
通用命令 keys:遍历所有的key[keys一般不再生产环境使用],时间复杂度O(n) keys * keys he* keys he[h-l]* keys ph? dbsize:计算key的总数, ...