一文学会使用Entity Framework Core

Posted JimCarter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文学会使用Entity Framework Core相关的知识,希望对你有一定的参考价值。

本文基于发稿时EF Core的最新版本5.0.

1. 基础篇

快速开始
如何根据实体类生成模型
实体关系的配置
值转换器(Value Conversion)

2. 原理篇

更改跟踪原理
数据查询原理
数据保存原理

3. 优化篇

日志、指标与拦截器
性能优化

4. 常见问题

4.1 怎么查看生成的sql

使用LogTo

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\\mssqllocaldb;Database=Blogging;Integrated Security=True")
        .LogTo(Console.WriteLine, LogLevel.Information);
}

4.2 怎么直接执行sql

context.Database.ExecuteSqlRaw("UPDATE [Employees] SET [Salary] = [Salary] + 1000");

//防止sql注入方式四,直接构造DbParameter
var user = new SqlParameter("user", "johndoe");
var blogs = context.Blogs
    .FromSqlRaw("EXECUTE dbo.GetMostPopularBlogsForUser @user", user)
    .ToList();

详见【原理篇-数据查询原理】

4.3 怎么执行事务

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();
try
{
    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context.SaveChanges();

    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    context.SaveChanges();

    var blogs = context.Blogs
        .OrderBy(b => b.Url)
        .ToList();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

事务失败会自动回滚,不需要手动操作。事务的更多用法,详见【原理篇-数据保存原理】

4.4 怎么使用数据库锁

4.4.1 乐观锁

实体上新增一个属性,标记为Timestamp:

[Timestamp]
public byte[] version{get;set;}

然后把SaveChanges用try-catch包裹起来 ,当有并发冲突时会抛出DbUpdateConcurrencyException异常,然后进行处理 即可。

详见【原理篇-数据保存原理3.2节】

4.4.2 悲观锁

查询之后就锁住,禁止其他查询和修改,容易死锁,性能低。efcore不支持,可以借助ADO.NET的事务实现:

static void Main(string[] args)
        {
           using (SqlConnection conn = new SqlConnection(connstr))
            {
                conn.Open();
                using (var tx = conn.BeginTransaction())
                {
                    try
                    {
                        using (var selectCmd = conn.CreateCommand())
                        {
                            selectCmd.Transaction = tx;
                            //xlock:排它锁,ROWLOCK行锁
                            selectCmd.CommandText = "select * from T_Girls with(xlock,ROWLOCK) where id=1";
                            using (var reader = selectCmd.ExecuteReader())
                            {
                                if (!reader.Read())
                                {
                                    Console.WriteLine("没有id为1的女孩");
                                    return;
                                }
                                string bf = null;
                                if (!reader.IsDBNull(reader.GetOrdinal("BF")))
                                {
                                    bf = reader.GetString(reader.GetOrdinal("BF"));
                                }
                                if (!string.IsNullOrEmpty(bf))//已经有男朋友
                                {
                                    if (bf == myname)
                                    {
                                        Console.WriteLine("早已经是我的人了");
                                    }
                                    else
                                    {
                                        Console.WriteLine("早已经被" + bf + "抢走了");
                                    }
                                    Console.ReadKey();
                                    return;
                                }
                                //如果bf==null,则继续向下抢
                            }
                            Console.WriteLine("查询完成,开始update");
                            using (var updateCmd = conn.CreateCommand())
                            {
                                updateCmd.Transaction = tx;
                                updateCmd.CommandText = "Update T_Girls set BF=@bf where id=1";
                                updateCmd.Parameters.Add(new SqlParameter("@bf", myname));
                                updateCmd.ExecuteNonQuery();
                            }
                            Console.WriteLine("结束Update");
                            Console.WriteLine("按任意键结束事务");
                            Console.ReadKey();
                        }
                        //事务提交之前你一直占有这行数据
                        tx.Commit();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex);
                        tx.Rollback();
                    }
                }
            }
            Console.ReadKey();
        }

4.5 导航属性的类型

如果是集合则可以为ICollection<T>List<T>HashSet<T>

4.6 主外键的默认命名规则

  1. 主键命名规则:符合IdIDClassnameIdClassnameID这些规则的属性自动认为是主键
  2. 外键规则:<导航属性名称><导航属性对应实体的主键属性名><导航属性对应实体的主键属性名>

4.7 EF Core怎么知道SaveChanges时哪些内容要提交到数据库?

通过Change tracking实现的。当数据从数据库读取出来之后,efcore会创建数据的快照,调用保存时会与快照进行对比。详见【原理篇-更改跟踪原理】。

4.8 禁止自动给主键赋值

[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int BlogId { get; set; }

5. 一些“坑”

“坑”不一定是真坑,所以加了引号。

5.1 跟踪查询引起的问题

ef默认使用的是跟踪查询,会把查询出来的实体实例信息记录在跟踪器中。当下次查询返回了一个跟踪器中已记录(主键相同)的实体时,则会把记录的这个实体作为查询结果直接返回(虽然还是会执行一遍sql),而且不会用数据库中的值覆盖现有实体的值。所以以下代码可能会出乎你的意料:

var blog = context.Blogs.Single(b => b.Name == "fish");
blog.Url = "aa";
var b = context.Blogs.Single(b => b.Name == "fish");
Console.WriteLine(b.Url);

此时blog的url还是aa,虽然生成的sql可以看到明显有两次查询:

info: 2021/6/2 15:40:01.856 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TOP(2) [b].[Id], [b].[Name], [b].[Url]
      FROM [Blogs] AS [b]
      WHERE [b].[Name] = N'fish'
info: 2021/6/2 15:40:01.862 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TOP(2) [b].[Id], [b].[Name], [b].[Url]
      FROM [Blogs] AS [b]
      WHERE [b].[Name] = N'fish'

针对where的一个操作:

var orders = context.Orders.Where(o => o.Id > 1000).ToList();
var filtered = context.Customers.Include(c => c.Orders.Where(o => o.Id > 5000)).ToList();

此时的查询结果:order的id大于1000的数据,而不是大于5000的数据。因为在跟踪查询的情况下,filter里的导航属性会被认为已经加载完成。

因为这个问题的存在,所以有时候你会发现明明自己没有调用Include,却也把子实体的数据给加载过来了。

以上问题产生的原因是:efcore只会维护同一实体的一个状态,如果同一个实体既是modified又是deleted,那么保存时就会出错(上面的原理篇-更改跟踪原理做了详细解释)。这个问题可以通过使用非跟踪查询AsNoTracking解决。

var blog = context.Blogs.Single(b => b.Name == "fish");
blog.Url = "aa";
var b = context.Blogs.AsNoTracking().Single(b => b.Name == "fish");
Console.WriteLine(b.Url);

以上是关于一文学会使用Entity Framework Core的主要内容,如果未能解决你的问题,请参考以下文章

必看一文学会使用Entity Framework Core

Entity Framework 4 ste删除外键关系

有没有办法以编程方式检查 Entity Framework Core 中的待定模型更改?

如何在 Entity Framework 6 中以编程方式创建与 MS SQL 的连接字符串?

Log4j 2再现新漏洞;缺乏资助不是开源软件安全的唯一问题;微软公布 Entity Framework 7.0 计划 | 开源日报

MVC5 Entity Framework学习之Entity Framework高级功能