EF Core 高阶操作

1.EF 内存查找

what?我们的ef不是直接连接数据库吗?我们查询的主体肯定是数据库啊,哪里来的内存呢?
1.所有的数据操作都有过程,并非操作直接会响应到数据库
2.并非所有的操作都每次提交,会存在缓存收集阶段,批量提交机制

描述下业务场景,我们存在一个业务,需要存储一张表,然后还需要对存储表数据做一些关联业务处理?我们可能会将方法拆分,首先处理数据保存,然后再根据数据去处理业务

直接看下代码

public static void Query_内存查询()
        {
            TestTable newTable = new TestTable();
            newTable.Id = 10;
            newTable.Name = "测试数据";
            using (MyDbContext dbContext = new MyDbContext())
            {
                dbContext.Add(newTable);
                Query_内存查询_关联业务处理(dbContext);
                dbContext.SaveChanges();
            }
        }

        private static void Query_内存查询_关联业务处理(MyDbContext dbContext)
        {
            var entity = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);
            //处理业务逻辑
            //...
        }

代码运行效果:
EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
发现并没有将数据查询出来,因为默认会查询数据库数据,此时数据还未提交,所以无法查询。但是也可以将实体数据传入到依赖方法啊,这样可以解决,但是如果关联实体多,来回传递麻烦,所以这不是最佳解

EF Core的缓存查询,前面文章已经提到,EF Core会将所有的改动存储到本地的缓存区,等待一起提交,并随即提供了基于缓存查询的方法,我们来验证下

public static void Query_内存查询()
        {
            TestTable newTable = new TestTable();
            newTable.Id = 10;
            newTable.Name = "测试数据";
            using (MyDbContext dbContext = new MyDbContext())
            {
                dbContext.Add(newTable);
                Query_内存查询_关联业务处理(dbContext);
            }
        }
        private static void Query_内存查询_关联业务处理(MyDbContext dbContext)
        {
            var entity = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);
            //处理业务逻辑
            //...
            var entity2 = dbContext.TestTables.Find(10);
            //处理业务逻辑
            //...
        }

代码运行效果:
EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
可以看到我们已经能够查询到未提交的数据了,但是也有必须的前提
1.必须使用ID查询,这点我们下面来分析
2.必须保证在同一上下文中,这点通过我们前面文章分析,缓存维护都是基于上下文维护,所以无法跨上下文来实现缓存数据查询

直接看源码,通过源码查看,分析得到通过Find()方法调用StateManager.FindIdentityMap(IKey key)方法

private IIdentityMap FindIdentityMap(IKey key)
        {
            if (_identityMap0 == null
                || key == null)
            {
                return null;
            }

            if (_identityMap0.Key == key)
            {
                return _identityMap0;
            }

            if (_identityMap1 == null)
            {
                return null;
            }

            if (_identityMap1.Key == key)
            {
                return _identityMap1;
            }

            return _identityMaps == null
                   || !_identityMaps.TryGetValue(key, out var identityMap)
                ? null
                : identityMap;
        }

这里就是对_identityMaps集合进行查找,那这个集合是什么时候有数据呢?为何新增的数据会在?看下DBContext.Add方法
DbContext.Add=>InternalEntityEntry.SetEntityState=> StateManager.StartTracking(this)=>StateManager.GetOrCreateIdentityMap
核心代码:

  if (!_identityMaps.TryGetValue(key, out var identityMap))
            {
                identityMap = key.GetIdentityMapFactory()(SensitiveLoggingEnabled);
                _identityMaps[key] = identityMap;
            }

会将当前实体放入集合中,如果集合中没有查询到,那就会执行数据库查询命令

2.导航属性

通过一个实体的属性成员,可以定位到与之有关联的实体,这就是导航的用途了
业务的发生永远不会堆积在单表业务上,可能会衍生多个关联业务表上,那在这种场景下,我们就需要导航属性,还是以示例入手

首先,我们需要两个关联实体,来看下实体

[Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int TestTableId { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }
    }

然后我们来测试下,实现关联数据的插入

public static void Insert_导航属性_数据准备()
        {
            TestTable table = new TestTable();
            table.Id = 10;
            table.Name = "主表数据10";
            TestTableDetail detail1 = new TestTableDetail();
            detail1.Id = 1;
            //detail1.PID = 10;
            detail1.Name = "主表数据10-从表数据1";
            TestTableDetail detail2 = new TestTableDetail();
            detail2.Id = 2;
            //detail2.PID = 10;
            detail2.Name = "主表数据10-从表数据2";
            table.TestTableDetails = new List<TestTableDetail>();
            table.TestTableDetails.Add(detail1);
            table.TestTableDetails.Add(detail2);
            using (MyDbContext db = new MyDbContext())
            {
                if (db.TestTables.FirstOrDefault(p => p.Id != 10) == null)
                    return;
                db.TestTables.Add(table);
                //db.TestTableDetails.Add(detail1);
                //db.TestTableDetails.Add(detail2);
                db.SaveChanges();
            }
        }

结果:
EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
实现了数据插入成功,这里第一个知识点。
如果要实现数据表的关联关系,一对多,必须有如下的约定
1.EFCore 默认导航属性,约定规则,主表包含从表数据集合,且从表包含主表表明+'Id'的字段
这样主,从表会被EFCore默认识别到,自动维护从表的外键信息
2.主实体包含从列表实体,以及从实体包含主实体,且从表包含从表导航属性名+主表主键名

 [Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }

        public int TestId { get; set; }
        public TestTable Test { get; set; }
    }

TestTableDetail中包含了导航属性Test,主实体主键为ID,那就必须包含外键TestId,看下运行效果
EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
3.从实体包含导航属性,且包含 主表名称+主表主键 的外键字段

 [Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }

        public int TestTableId { get; set; }
        public TestTable Test { get; set; }
    }

三面三种方式来建立我们实体之间的主外键关系也还不错,但是往往业务中可能没有我们想象的简单,没法符合上面的三种规则,那我们就需要手动来设置导航属性
4.手动设置一,实体ForeignKey设置

public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }
        [ForeignKey("PID")]
        public TestTable Test { get; set; }
    }

运行结果,可以看到我们使用了自定义的外键PID
EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
5.手动设置二,Fluent API 设置
DbContext配置实体关系

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // 映射实体关系,一对多
            modelBuilder.Entity<TestTableDetail>()
                        .HasOne(p=>p.Test)
                        .WithMany(p=>p.TestTableDetails)
                        .HasForeignKey(p=>p.PID);
        }
 public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }
        public TestTable Test { get; set; }
    }

看下运行效果:
EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
导航属性的几种使用方式还是要结合真正的业务来选择,但是并非所有的场景都要使用,而且要结合性能来考虑,我们来看下导航属性的实现本质

public static void Query_导航属性()
        {
            MyDbContext dbContext = new MyDbContext();
            var test = dbContext.TestTables.Where(p=>p.Id==10).
                Include(c => c.TestTableDetails).FirstOrDefault();
        }

通过API Include方法,来执行导航属性查询,然后跟踪SQL如下

SELECT [t0].[Id], [t0].[Name], [t1].[Id], [t1].[Name], [t1].[PID]
FROM (
    SELECT TOP(1) [t].[Id], [t].[Name]
    FROM [TestTable] AS [t]
    WHERE [t].[Id] = 10
) AS [t0]
LEFT JOIN [TestTableDetail] AS [t1] ON [t0].[Id] = [t1].[PID]
ORDER BY [t0].[Id], [t1].[Id]

EF Core 三 、 骚操作 (导航属性,内存查询...)-LMLPHP
导航属性查询时,会将关联表进行Left Join,返回一张宽表,包含两张表的全部字段,主表数据量会呈现翻倍增长
例如:主表数据1条,二级从表3条,三级从表每个10条,那就是一张三十条数据的大宽表,从数据查询以及传输来看,对性能会照成比较大的影响,所以一定要慎用
有以下几个点:
1.在不需要关联表数据时,不需要使用Include,只会查询出主表数据

var test1 = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);

2.那如果可能需要关联表数据呢?能够有一种方法,在我需要关联数据的时候再去查询?
-- 2.1 分段查询,我们来看下具体效果

public static void Query_导航属性()
        {
            MyDbContext dbContext = new MyDbContext();
            //定义查询条件,并不会执行数据库查询
            var query = dbContext.TestTables.Where(p => p.Id == 10);
            //执行查询,但是只会查询主表数据
            var test4 = query.FirstOrDefault();
            //需要从表数据时,再触发查询
            query.SelectMany(p => p.TestTableDetails).Load();
        }

第一次查询

SELECT [t].[Id], [t].[Name]
FROM [TestTable] AS [t]
WHERE [t].[Id] = 10

第二次查询

SELECT [t0].[Id], [t0].[Name], [t0].[PID]
FROM [TestTable] AS [t]
INNER JOIN [TestTableDetail] AS [t0] ON [t].[Id] = [t0].[PID]
WHERE [t].[Id] = 10

第一次只会查询主表,第二次查询通过Inner Join,性能也远高于Left join,且只返回了TestTableDetail的数据

-- 2.2 Linq to SQL 或者 Lambda Join()
通过自主决定查询数据来优化查询方式,来提高查询效率,这也是决定Left join或者Inner join的一种方式
两种方式在特定场景下还是有比较大的性能差异

-- 2.3 延迟加载
1.使用 Proxies代理方式
引入Microsoft.EntityFrameworkCore.Proxies包
2.注册代理

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseLazyLoadingProxies();
            //写入连接字符串
            optionsBuilder.UseSqlServer("Data Source=.\\SQLSERVER;Initial Catalog=EfCore.Test;User ID=sa;Pwd=123");
        }

3.修改实体,导航属性增加 virtual 关键字

[Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }
        public virtual TestTable Test { get; set; }
    }

然后直接执行查询即可

var test1 = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);
var count = test1.TestTableDetails.Count();

观察SQL
第一次:

SELECT TOP(1) [t].[Id], [t].[Name]
FROM [TestTable] AS [t]
WHERE [t].[Id] = 10

第二次,访问TestTableDetails时触发

exec sp_executesql N'SELECT [t].[Id], [t].[Name], [t].[PID]
FROM [TestTableDetail] AS [t]
WHERE [t].[PID] = @__p_0',N'@__p_0 int',@__p_0=10

文本就先到这吧,要开始做饭了 😄 ...
EF Core在使用时还是要多了解,避免使用中带来的更多问题,后续一起继续学习

12-26 23:39