问题 使用ToList()和.AsQueryable()连接两个不同的DB Context有什么区别?


情况1: 我加入了两个不同的DB Context by ToList() 两种语境中的方法。

案例2: 并尝试加入第一个Db上下文 ToList() 第二个 AsQueryable()

两者都适合我。我想知道的是那些关于性能和功能的加入之间的区别。哪一个更好 ?

var users = (from usr in dbContext.User.AsNoTracking()
                  select new
                  {
                     usr.UserId,
                     usr.UserName
                  }).ToList();

 var logInfo= (from log in dbContext1.LogInfo.AsNoTracking()
               select new
               {
                   log.UserId,
                   log.LogInformation
               }).AsQueryable();

 var finalQuery= (from usr in users
                  join log in logInfo on usr.UserId equals log.UserId
                  select new
                  {
                     usr.UserName,
                     log.LogInformation
                  }.ToList();

3629
2017-11-23 08:00


起源

我认为这次加入将发生在“记忆中”,但我不确定。所以User和LogInfo将被读入内存并加入其中。 - Jehof
您应该在LinqPad中尝试相同的测试,这应该向您展示生成的SQL的差异。 - Aron


答案:


我将详细阐述Jehof在评论中给出的答案。确实,这个连接将在内存中执行。它有两个原因发生。

首先,无法在数据库中执行此连接,因为您正在加入内存中的对象(users)使用延迟查询(logInfo)。基于此,无法生成可以发送到数据库的查询。这意味着在执行实际连接之前,将执行延迟查询,并从数据库中检索所有日志。总而言之,在这种情况下,2个查询在数据库中执行,并且连接发生在内存中。 如果你使用它并不重要 ToList + AsQueryable 要么 ToList + ToList 在这种情况下。 

其次,在您的场景中,此连接只能在内存中执行。即使你使用 AsQueryable 对于第一个上下文和第二个上下文,它将无法工作。你会得到 System.NotSupportedException 消息的异常:

指定的LINQ表达式包含对与不同上下文关联的查询的引用。

我想知道你为什么要使用2个DB上下文。真的需要吗?正如我所解释的那样,你失去了充分利用延迟查询(懒惰评估功能)的可能性。

如果你真的必须使用2个DB上下文,我会考虑为负责从DB读取用户和日志的查询添加一些过滤器(WHERE条件)。为什么?对于少量记录,没有问题。但是,对于大量数据,在内存中执行连接效率不高。为此,创建了数据库。


10
2017-11-23 09:04



谢谢你的回复:)。我怀疑,在案例2中,加入后类型不会改变。它仍然是AsQueryable() - Karthik Arthik
@Arthik So?有一个完全没问题 IQueryable 在内存列表中。请注意,您可以使用 .ToList().AsQueryable() 同样容易 - 结果是列表中的可查询。但你仍然需要先获得整个列表:) - Luaan
@Arthik你是对的那种 LOGINFO 加入后不会改变。在C#中,您无法在声明变量后更改变量的类型。 LOGINFO 被“转换”为场景后面的列表,并且此操作的结果不会存储在代码中的任何变量中。 - Michał Komorowski
@Michal,幕后的意思是,那是在任何临时记忆中发生的吗?记忆保存多长时间? - Karthik Arthik
@Arthik它出现在应用程序使用的内存(RAM)中。当垃圾收集不再使用时,该列表将被垃圾收集从内存中删除,即没有引用指向此列表。 - Michał Komorowski


还没有解释为什么语句实际工作以及为什么EF不会抛出一个异常,你只能在LINQ语句中使用原始类型的序列。

如果你交换两个列表......

var finalQuery= (from log in logInfo
                 join usr in users on log.UserId equals usr.UserId
                 ...

EF将投掷

无法创建“用户”类型的常量值。在此上下文中仅支持原始类型或枚举类型。

那么为什么你的代码有效呢?

如果我们将您的语句转换为方法语法(运行时在引擎盖下),这将变得清晰:

users.Join(logInfo, usr => usr.UserId, log => log.UserId
            (usr,log) => new
                        {
                            usr.UserName,
                            log.LogInformation
                        }

以来 users 是一个 IEnumerable,扩展方法 Enumerable.Join 被解决为适当的方法。这个方法接受一个 IEnumerable 作为第二个加入的列表。因此, logInfo 隐含地投射到 IEnumerable,因此它在参与连接之前作为单独的SQL语句运行。

在版本中 from log in logInfo join usr ...Queryable.Join 用来。现在 usr 转换为 IQueryable。这会将整个语句转换为EF未成功尝试转换为一个SQL语句的表达式。

现在说几句话

哪一个更好?

最好的选择就是能让它发挥作用的选择。这意味着

  • 你可以删除 AsQueryable()因为 logInfo 已经是一个 IQueryable 它被投射到 IEnumerable 无论如何。
  • 你可以替换 ToList() 通过 AsEnumerable()因为 ToList() 构建冗余的中间结果,而 AsEnumerable() 只更改运行时类型 users,尚未触发其执行。

1
2017-11-23 15:15





ToList()

  • 立即执行查询
  • 你会得到 所有 在内存中准备好的元素

AsQueryable已()

  • 懒惰(稍后执行查询)
  • 参数: Expression<Func<TSource, bool>>
  • 将Expression转换为T-SQL(使用特定提供程序),远程查询并将结果加载到应用程序内存中。
  • 这就是DbSet(在Entity Framework中)也继承IQueryable以获得高效查询的原因。
  • 它不会加载每条记录。例如。如果Take(5),它将在后台生成select top 5 * SQL。

-1
2017-11-23 09:26



这不回答这个问题 - edc65