前言

.net 环境近些年也算是稳步发展。在开发的过程中,与数据库打交道是必不可少的。早期的开发者都是DbHelper一撸到底,到现在的各种各样的ORM框架大行其道。孰优孰劣谁也说不清楚,文无第一武无第二说的就是这个理。没有什么最好的,只有最适合你的。

本人也是从DbHelper开始,期间用过SugarSql,再到EFCODE。本着学习分享的初衷分享本人工作中总结的一些小技巧,希望能帮助更多开发者,期望能达到共同进步。文中若有错误地方,欢迎大家不吝赐教。

1. DbContext配置

在asp.net中,通常情况下,通过在Startup类的ConfigureServices方法中,将ef服务注入。

示例代码如下:

services.AddDbContext<DemoDbContext>(opt=>opt.UseMySql("server=.;Database=demo;Uid=root;Pwd=123;Port=3306;"));

以上代码表示使用MySql数据库。如果使用SqlServer数据库,可以把UseMySql改为UseSqlServer,其他数据库的使用方式也是通过调用不同的方法进行选择。但需要安装对应的扩展方法的程序包,如 Microsoft.EntityFrameworkCore.SqlServer 或 Microsoft.EntityFrameworkCore.Sqlite。

另外,UseMySql方法还包含了一个可空的Action类型的参数,可以通过此参数进行一些个性化的配置,比如配置重试机制。如下所示:

services.AddDbContext<DemoDbContext>(opt => opt.UseMySql("server=.;Database=demo;Uid=root;Pwd=123456;Port=3306;",
provideropt => provideropt.EnableRetryOnFailure(3,TimeSpan.FromSeconds(10),new List<int>(){0} )));

这个重试机制在某些场景下还是比较有用的。比如,由于网络波动或访问量导致的一瞬间的连接超时。如果不设置重试机制,则会直接触发异常,设置了超时后,则会根据设置的时间间隔以及重试次数进行重试。EnableRetryOnFailure方法的最后一个参数是用来设置错误代码的,只有设置了错误代码的错误,才会触发重试。获取错误代码的方法有很多种,个人比较推荐的是,通过异常信息进行获取,比如,使用MySql数据时,触发的异常类型是MySqlException,此类的Number属性的值EnableRetryOnFailure方法所需要的Number

2. DbContext线程问题

efcore不支持在同一个DbContext实例上运行多个并行操作,这包括异步查询的并行执行以及从多个线程进行的任何显式并发使用。 因此,始终 await 异步调用,或对并行执行的操作使用单独的 DbContext 实例。

当 EF Core 检测到并行操作或多个线程同时尝试使用 DbContext 实例时,你将看到一条 InvalidOperationException,其中包含类似于下面的消息:

A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.

意思是,在上一个操作没有执行完毕之前,又启动了一个新的操作,所以不能保证线程是安全的。

下面是一段错误的,可以触发这个异常的示例代码:

所以,请始终await异步调用。如果在多个多个线程中使用DbContext,需保证每个线程的DbContext的实例是唯一的。

3. 数据库使用连接池

使用 services.AddDbContextPool比使用 services.AddDbContext吞吐量提升在10~20的百分点(非官方说法,对性能提高数据是本人测试后得到的结果)。

需要注意的是,连接池大小并不是越大越好。

4. 日志记录

在使用ef时,基本上绝大多数和数据库的交互都是通过linq实现的,然后ef将linq翻译成对应的sql语句,在排查问题的时候,在开发或者排查问题时,往往需要关注最终执行的sql脚本,所以就需要通过日志的方式查看。

efcore2.x的版本默认是注入日志服务,所以不需要额外的操作,就可以查看对应的sql脚本。但efcore3.x的版本默认移除了日志服务,具体原因参照:https://docs.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-3.0/breaking-changes#adddbc。

可通过自定义DbContext的方式注入日志任务,示例代码如下:

public static readonly ILoggerFactory MyLoggerFactory
= LoggerFactory.Create(builder => { builder.AddConsole(); });
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseLoggerFactory(MyLoggerFactory);
}

当执行ef代码时,可在控制台中查看相关的sql脚本,如下图所示:

5. 增

插入数据到数据库常用的场景有:普通单表单行插入,多表级联插入,批量插入。

普通单表单行插入比较简单,实例代码如下:

var student = new Student {CreateTime = DateTime.Now, Name = "zjjjjjj"};
await _context.Students.AddAsync(student);
await _context.SaveChangesAsync();

多表级联插入,需要在实体映射中配置属性导航。

比如Blog表和Post是的关系是1对多的关系。则在Blog的实体中,定义一个类型为List的属性。示例代码如下:

[Table("blog")]
public class Blog
{
[Column("id")]
public long Id { get; set; }
[Column("title")]
public string Title { get; set; }
public List<Post> Posts { get; set; }
[Column("create_date")]
public DateTime CreateDate { get; set; }
}

对应的插入语句如下所示:

var blog = new Blog
{
Title = "测试标题",
Posts = new List<Post>
{
new Post{Content = "评论1"},
new Post{Content = "评论2"},
new Post{Content = "评论3"},
}
};
await _context.Blog.AddAsync(blog);
await _context.SaveChangesAsync();

执行此代码,会生成如下的日志:



从日志中可以看出,通过这种方式实现了级联插入的效果。

批量插入实现方式有两种,一种是EF默认实现,适用于数据源较少的情况。另一种,我们基于EF开发一个大数据量批量插入的服务,适合于数据源大于1000的场景。在万级及以上的数据量上,较EF默认的批量插入性能上有非常明显的提升。具体参考:https://www.cnblogs.com/fulu/p/13370335.html

EF默认实现:

var list = new List<Student>();
for (int i = 0; i < num; i++)
{
list.Add(new Student { CreateTime = DateTime.Now, Name = "zjjjjjj" });
} await _context.Students.AddRangeAsync(list);
await _context.SaveChangesAsync();

ISqlBulk实现:

var list = new List<Student>();
for (int i = 0; i < 100000; i++)
{
list.Add(new Student { CreateTime = DateTime.Now, Name = "zjjjjjj" });
}
await _bulk.InsertAsync(list);

自增 OR GUID

int自增的优点:

1、需要很小的数据存储空间,仅仅需要4 byte 。

2、insert和update操作时使用INT的性能比GUID好,所以使用int将会提高应用程序的性能。

3、index和Join 操作,int的性能最好。

4、容易记忆。

int自增的缺点:

1、使用INT数据范围有限制。如果存在大量的数据,可能会超出INT的取值范围。

2、很难处理分布式存储的数据表。

GUID做主键的优点:

1、唯一性。

2、适合大量数据中的插入和更新操作。

3、跨服务器数据合并非常方便。

GUID做主键的缺点:

1、存储空间大(16 byte),因此它将会占用更多的磁盘大小。

2、很难记忆。join操作性能比int要低。

3、没有内置的函数获取最新产生的guid主键。

4、EF默认生成的GUID是无序的,会影响数据插入性能。

结论:

在数据量比较少的场景下,建议使用int自增,比如分类。对于大数据量,建议使用有序GUID。因为默认.net生成GUID是无序的,而数据库中主键默认是聚集索引,而聚集索引在物理上的存储是有序的,当插入数据时,如果插入的是无序的GUID,可能就会涉及到移动数据的情况,进而影响插入的性能,特别是百万级数据量的时候,性能影响则较为明显。参考资料:https://www.cnblogs.com/CameronWu/p/guids-as-fast-primary-keys-under-multiple-database.html

其他可选方案:

经过个人多番了解,目前市面上常用的分布式id生成算法和Twitter发布的雪花算法大同小异,个人也在项目中使用过雪花算法,有兴趣的朋友可以在博客园找下相关的内容。不过目前用.net封装的雪花算法普遍较基础,很难在docker或者k8s环境下简单的使用,所以在此预告下,本人根据雪花算法编写的可用于k8s环境的即将开源,敬请期待。

6. 查

EF使用Linq查询数据库中的数据,使用Linq可编写强类型的查询。当命令执行时,EF先将Linq表达式转换成sql脚本,然后再提交给数据库执行。可在日志中查看生成的sql脚本。

根据条件查询:
await _context.Blog.Where(x=>x.Id>0).ToListAsync();

上述代码执行时生成的sql脚本如下所示:

SELECT `x`.`id`, `x`.`create_date`, `x`.`title`
FROM `blog` AS `x`
WHERE `x`.`id` > 0
获取单个实体

可实现获取单个实体的方式有First,FirstOrDefault,Single,SingleOrDefault

其中First,FirstOrDefault执行时生成的sql脚本如下:

 SELECT `x`.`id`, `x`.`create_date`, `x`.`title`
FROM `blog` AS `x`
WHERE `x`.`id` > 10
LIMIT 1

Single,SingleOrDefault执行时生成的sql脚本如下:

 SELECT `x`.`id`, `x`.`create_date`, `x`.`title`
FROM `blog` AS `x`
WHERE `x`.`id` > 10
LIMIT 2

细心的你应该已经发现了两者的区别,Single需要查询2条数据,当返回的数据多余一条时,Single,SingleOrDefault方法就会报Source sequence contains more than one element.异常。所以Single方法仅适用于查询条件对应的数据只有一条的场景,比如查询主键的值。如下所示:

await _context.Blog.SingleOrDefaultAsync(x => x.Id==100);

后缀带OrDefault和不带后缀的区别是,当sql脚本执行查询不到数据时,带后缀的会返回空值,而不带后缀的则会直接报异常。

判断数据库是否存在

可通过Any()和Count()方法实现是否存在数据。示例代码如下:

await _context.Blog.AnyAsync(x => x.Id > 100);

await _context.Blog.CountAsync(x => x.Id > 100)>0;

生成的sql脚本对应如下:

SELECT CASE
WHEN EXISTS (
SELECT 1
FROM `blog` AS `x`
WHERE `x`.`id` > 100)
THEN TRUE ELSE FALSE
END
SELECT COUNT(*)
FROM `blog` AS `x`
WHERE `x`.`id` > 100

乍一看,Any方法生成的脚本貌似更复杂些,但实际上,Any方法的性能在大数据量下比Count方法高了很多。所以在判断是否存在时,请使用Any方法。

连接查询

连接查询是关系数据库中最主要的查询,主要包括内连接、外连接(左连接、外连接)和交叉连接等。通过连接运算符可以实现多个表查询。本文主要讲解下常用的内连接和左连接。

内连接的示例代码如下:

var query = from post in _context.Post
join blog in _context.Blog on post.BlogId equals blog.Id
where blog.Id > 0
select new {blog, post};

左连接的示例代码如下:

var query = from post in _context.Post
join blog in _context.Blog on post.BlogId equals blog.Id
into pbs
from pb in pbs.DefaultIfEmpty()
where pb.Id>0 && post.Content.Contains("1")
select new {post,pb.Title};
级联查询

在很多场景中,可能会涉及到查询与父表关联的子表数据,在这样的场景中,会有一部分人先查出主表数据,然后根据主表的主键再去查询子表的数据,笔者在使用ef初期也是这种处理方式的。但借助Include的方法可以让我们更方便的解决父子表级联查询的问题。示例代码如下:

var result = await _context.Blog.Include(b => b.Posts) .SingleOrDefaultAsync(x=>x.Id==157);

如果有更多的层级,可以借助ThenInclude进行查询。

有的时候,还有这样的场景:我们不是简单的查询子表的数据,而是需要查询满足指定条件的数据,那就要求咱们在调用Include的方法时传入参数,示例代码如下:

 var filteredBlogs = await _context.Blogs
.Include(blog => blog.Posts
.Where(post => post.BlogId == 1)
.OrderByDescending(post => post.Title)
.Take(5))
.ToListAsync();

注:以上方法仅在.net5中支持。所以,efcore也是在一个发展的过程中,随着时间与版本的更新,功能也会渐渐趋于完善。相关内容请参考:https://docs.microsoft.com/zh-cn/ef/core/querying/related-data

7. 改

使用过EF的应该都了解查询的跟踪与非跟踪的概念吧(纳尼?你没听说过,老衲给您指条明路吧:https://docs.microsoft.com/zh-cn/ef/core/querying/tracking)。

通常来讲,更新的流程大概是这样:查询出数据,修改某些字段的值,调用Update方法,然后调用SaveChange方法。看上去毫无破绽,但如果你仔细观察过生成的sql脚本的话,或许你就应该有更好的方法,咱们先来看看示例代码:

var school = await _context.Schools.FirstAsync(x => x.Id > 0);
school.Name = "6666";
_context.Schools.Update(school);
await _context.SaveChangesAsync();

如下图所示的是执行以上代码生成的update的sql语句,我们发现明明代码中只对Name重新赋了值,但生成的脚本却将此记录的所有字段进行了更新,显然这不是我们想要的结果。

其实,如果实体是通过跟踪查询得到的,则可直接调用SaveChage方法,而不用多余调用Update方法,此时,EF内部会自动判断哪些字段进行了更新,从而只生成值改变了的sql语句。

结论:当要更新的实体开启了跟踪,则更新时,无需调用Update方法, 直接调用SaveChange方法,此时之后更新值发生改变的字段。 如果先调用Update则SaveChange,则不管实体的字段有没有更新,生成的sql脚本依旧会更新所有的字段,牺牲了性能。假如你的实体不是通过数据库的跟踪查询获取的,则在调用时才需要调用Update方法。


福禄ICH.架构出品

作者:福尔斯

2020年8月

efcore技巧贴-也许有你不知道的使用技巧的更多相关文章

  1. 26个你不知道的Python技巧

    Python是目前世界上最流行的编程语言之一.因为: 1.它容易学习 2.它用途超广 3.它有非常多的开源支持(大量的模块和库) 不好意思,优达菌又啰嗦了. 本文作者 Peter Gleeson 是一 ...

  2. (转载) Android Studio你不知道的调试技巧

    Android Studio你不知道的调试技巧 标签: android studio 2015-12-29 16:05 2514人阅读 评论(0) 收藏 举报  分类: android(74)    ...

  3. 三个你不知道的CSS技巧

    各种浏览器之间的竞争的白热化意味着越来越多的人现在开始使用那些支持最新.最先进的W3C Web标准的设备,以一种更具交互性的方式来访问互联网.这意味着我们终于能够利用更强大更灵活的CSS来创造更简洁, ...

  4. Playground 你不知道的小技巧, CoreData 的使用

    Playground 的出现无疑是大大的提高了开发效率,可以节省大量的编译时间. 这里介绍在 Playground 中使用 CoreData 的小技巧. 我们新建一个工程 iOS 项目工程. 点击Fi ...

  5. 读《SQL优化核心思想》:你不知道的优化技巧

    SQL性能问题已经逐步发展成为数据库性能的首要问题,80%的数据库性能问题都是因SQL而导致. 1.1 基数(CARDINALITY) 某个列唯一键(Distinct_Keys)的数量叫作基数.比如性 ...

  6. Android Studio你不知道的调试技巧

    写代码不可避免有Bug,通常情况下除了日志最直接的调试手段就是debug:那么你的调试技术停留在哪一阶段呢?仅仅是下个断点单步执行吗?或者你知道 Evaluate Expression, 知道条件断点 ...

  7. iOS 10 个实用小技巧(总有你不知道的和你会用到的)

    在开发过程中我们总会遇到各种各样的小问题,有些小问题并不是十分容易解决.在此我就总结一下,我在开发中遇到的各种小问题,以及我的解决方法.比较普遍的我就不再提了,这里主要讲一些你可能不知道的(当然,也有 ...

  8. 你不知道的 flex 技巧

    一.使用 Auto Margins 对齐 不需要给图片使用任何的 flex,也不需要给父容器设置 space-between,只需要给 ' BUY-BUY-BUY' 按钮设置 margin-left: ...

  9. Extjs 项目中常用的小技巧,也许你用得着(3)

    几天没写了,接着继续, 1.怎么获取表单是否验证通过: form.isValid()//通过验证为true 2.怎样隐藏列,并可勾选: hidden: true, 如果是动态隐藏的话: grid.ge ...

随机推荐

  1. abs,all,any函数的使用

    ''' abs函数:如果参数为实数,则返回绝对值 如果参数为复数,则返回复数的模 ''' a = 6 b = -6 c = 0 # print("a = {0} , b = {1} , c ...

  2. MySQL的undo/redo日志和binlog日志,以及2PC

    发现自己的知识点有点散,今天就把它们连接起来,好好总结一下. 一.undo log.redo log.binlog的定义和对比   定义和作用                       所在架构层级 ...

  3. CentOS7系统管理与运维实战

    CentOS7系统管理与运维实战 下载地址 https://pan.baidu.com/s/1KFHVI-XjGaLMrh39WuhyCw 扫码下面二维码关注公众号回复100007 获取分享码 本书目 ...

  4. 文件权限和访问控制列表ACL (1)

    背景知识: 文件的权限主要针对三类对象进行定义 Owner: 属主u Group: 属组g Other: 其他o 每个文件针对每一类的访问者都设定了三种权限 r: Readable 读 w: Writ ...

  5. 008_go语言中的Arrays数组

    代码演示 package main import "fmt" func main() { var a [5]int fmt.Println("emp:", a) ...

  6. Docker 快速搭建 MySQL8 开发环境

    使用 Docker 快速搭建一个 MySQL8 开发环境 步骤 获取镜像 docker pull mysql:8 启动容器,密码 123456,映射 3306 端口 docker run --name ...

  7. 社区观点 | 理解比原链MOV链上交换协议

    去中心化交换协议的发展 从Bitshare,Stellar到以太坊上的Etherdelta,Bancor,0x协议,去中心化交换协议也经过了好几代发展和很多模式的探索,每一代都通过前面的协议的痛点来进 ...

  8. 微信小程序多列选择器

    wxml <picker mode="multiSelector" bindchange="bindMultiPickerChange2" bindcol ...

  9. JDK 1.8 中文 API CHM

    链接: https://pan.baidu.com/s/1AiJn6RM1KoEL1n_96qoQhQ 提取码: n2ya

  10. C#LeetCode刷题之#876-链表的中间结点(Middle of the Linked List)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3836 访问. 给定一个带有头结点 head 的非空单链表,返回链 ...