efcore技巧贴-也许有你不知道的使用技巧

前言

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

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

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。sql

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

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脚本,以下图所示:
TIM截图20200618204603

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();

执行此代码,会生成以下的日志:
111
从日志中能够看出,经过这种方式实现了级联插入的效果。

批量插入实现方式有两种,一种是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自增的优势:

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

二、insert和update操做时使用INT的性能比GUID好,因此使用int将会提升应用程序的性能。

三、index和Join 操做,int的性能最好。

四、容易记忆。

int自增的缺点:

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

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

GUID作主键的优势:

一、惟一性。

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

三、跨服务器数据合并不是常方便。

GUID作主键的缺点:

一、存储空间大(16 byte),所以它将会占用更多的磁盘大小。

二、很难记忆。join操做性能比int要低。

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

四、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从新赋了值,但生成的脚本却将此记录的全部字段进行了更新,显然这不是咱们想要的结果。

20200828011210

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

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


福禄ICH.架构出品

做者:福尔斯

2020年8月

相关文章
相关标签/搜索