使用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. 20190926-2 选题 Scrum立会报告+燃尽图05

    此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/8678 一.小组情况组长:迟俊文组员:宋晓丽 梁梦瑶 韩昊 刘信鹏队名:扛 ...

  2. 超速入门AT指令集 | 我的物联网成长记

    [摘要] 在物联网中,AT命令集可用于控制&调测设备.通信模块入网等.本文为您介绍NB-IoT常用的AT命令集及其调测工具. 什么是AT指令集 AT命令,用来控制TE(Terminal Equ ...

  3. Git及Github

    目录 Git及Github的使用 Git的基本介绍 Git命令行操作 1.设置签名 2.创建本地库 3.仓库初始化 4.状态查看 5.添加文件 6.提交文件 7.历史记录 8.前进后退 9.删除文件 ...

  4. 【2018寒假集训 Day2】【动态规划】钱币兑换(exchange)(自己翻译的2333)

    钱币兑换(exchange) 问题描述: Dave偶然获得了未来几天的美元(dollars)与马克(marks)之间的兑换率.例如Dave开始有100marks,请编写个程序帮助Dave找出最好的买卖 ...

  5. JavaScript笔记三

    1.数据类型 - JS中一共分成六种数据类型 - String 字符串 - Number 数值 - Boolean 布尔值 - Null 空值 - Undefined 未定义 - Object 对象 ...

  6. day 27 网路编程 面向对象多继承

    知识补充: 字符串转化为字节 string1  = input(“请输入你的名字”) string1.encode('utf-8') 字节转化为字符串 byte1 = b"alex" ...

  7. Selenium+Java(六)Selenium 强制等待、显式等待、隐实等待

    前言 在实际测试过程中,由于网速或性能方面的原因,打开相应的网页后或在网页上做了相应的操作,网页上的元素可能不会马上加载出来,这个时候需要在定位元素前等待一下,等元素加载出来后再进行定位,根据实际使用 ...

  8. PHP安装扩展补充说明

    上一篇文章中用到了,php的sodium扩展,那么如何安装PHP扩展呢?基于我之前踩过的一些坑,大致整理了几种安装php扩展的方法.已安装sodium为例 1.先做点准备工作,安装sodium依赖 r ...

  9. 动态规划算法(java)

    一.动态规划算法 众所周知,递归算法时间复杂度很高为(2^n),而动态规划算法也能够解决此类问题,动态规划的算法的时间复杂度为(n^2).动态规划算法是以空间置换时间的解决方式,一开始理解起来可能比较 ...

  10. redis - redis数据结构与API

    通用命令 keys:遍历所有的key[keys一般不再生产环境使用],时间复杂度O(n) keys * keys he* keys he[h-l]* keys ph? dbsize:计算key的总数, ...