【必看】一文学会使用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; }

4.9 在EF中如何使用ADO.NET?

其实只要能获取到connection就能使用:

var conn = _dbcontext.Database.GetDbConnection();
try
{    //打开数据库链接
    await conn.OpenAsync();
    //建立链接,因为非委托资源所以需要使用using进行内存资源的释放
    using (var command = conn.CreateCommand())
    {
        string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount   FROM Person  WHERE Discriminator = 'Student'  GROUP BY EnrollmentDate";
        command.CommandText = query; //赋值需要执行的sql命令
        DbDataReader reader = await command.ExecuteReaderAsync();//执行命令
        if (reader.HasRows)//判断是否有返回行
        {       //读取行数据 ,将返回值填充到视图模型中
            while (await reader.ReadAsync())
            {
                var row = new EnrollmentDateGroupDto
                {
                    EnrollmentDate = reader.GetDateTime(0),
                    StudentCount = reader.GetInt32(1)
                };
            }
        }
        //释放使用的所有的资源
        reader.Dispose();
    }
}
finally
{  //关闭数据库连接。
    conn.Close();
}

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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JimCarter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值