1

我有一个 linq 查询问题,我试图从中获取某些信息,但根本找不到问题所在。我的 ef 核心数据库中有工具、任务和 MN ToolTask​​ 实体。此查询包括 2 个左外连接和最后的 group by。

即使我正在检查分组值是否不为空,我仍然会收到一条错误消息,例如“可为空的对象必须有一个值”。我在这里想念什么?

此外,似乎这个 linq 查询是在客户端评估的,我能做些什么来使其成为服务器端?当我由于某种原因在代码中没有“let maxDateV ...”行时,它一直是服务器端。

var max = (from t in _fabContext.Tools

join tt in _fabContext.ToolTask on t.ToolId equals tt.ToolId into tt1
from tt in tt1.DefaultIfEmpty()

join ts in _fabContext.Tasks on tt.TaskId equals ts.TaskId into ts1
from ts in ts1.DefaultIfEmpty()

group tt by tt.ToolId into g
let maxOrderV = g.Max(c => c != null ? c.Order : 0)
let maxDateV = g.Max(c => c != null ? c.Task.ExportDate : DateTime.MinValue)
select new
{
  ToolId = g.Key,
  MaxOrder = maxOrderV,
  MaxExportDate = maxDateV
}).ToDictionary(d => d.ToolId, d => 
    new OrderExportDate { 
        Order = d.MaxOrder, 
        ExportDate = d.MaxExportDate 
    });

更新 1(实体类):

任务

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace Main.DataLayer.EfClasses
{
    public class Task
    {
        public int TaskId { get; private set; }
        [Required]
        public string Name { get; private set; }
        [Required]
        public int ProfileId { get; private set; }
        [Required]
        public DateTime ExportDate { get; private set; }

        private HashSet<ToolTask> _toolTask;
        public IEnumerable<ToolTask> ToolTask => _toolTask?.ToList();

        private Task()
        {
        }

        public Task(
            string name,
            int profileId,
            DateTime exportDate)
        {
            Name = name;
            ProfileId = profileId;
            ExportDate = exportDate;
        }
    }
}

工具任务

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace Main.DataLayer.EfClasses
{
    public class ToolTask
    {
        public int ToolId
        {
            get; private set;
        }

        public Tool Tool
        {
            get; private set;
        }

        public int TaskId
        {
            get; private set;
        }

        public Task Task
        {
            get; private set;
        }

        [Required]
        public int SortOrder
        {
            get; private set;
        }

        private ToolTask() { }

        internal ToolTask(Tool tool, Task task, int sortOrder)
        {
            Tool = tool;
            ToolId = tool.ToolId;
            Task = task;
            TaskId = task.TaskId;
            SortOrder = sortOrder;
        }

        public void ChangeOrder(int order)
        {
            SortOrder = order;
        }
    }
}

工具

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Main.DataLayer.EfCode;

namespace Main.DataLayer.EfClasses
{
    public class Tool
    {
        public int ToolId
        {
            get; private set;
        }

        [Required]
        public string Name
        {
            get; private set;
        }

        [Required]
        public string Model
        {
            get; private set;
        }

        private HashSet<ToolTask> _toolTask;
        public IEnumerable<ToolTask> ToolTask => _toolTask?.ToList();

        internal Tool() { }

        public Tool(string name, string model)
        {
            Name = name;
            Model = model;

            _toolTask = new HashSet<ToolTask>();
        }

        public void AddTask(Task task, int sortOrder)
        {
            _toolTask?.Add(new ToolTask(this, task, sortOrder));
        }
    }
}

每个实体类都有自己的配置类,但除了设置主键和支持字段的访问模式之外,没有什么大不了的。

4

1 回答 1

3

该实现的问题是您分组 by tt.ToolId,其中tt来自左外连接的“右侧”,因此tt可以是null

您可以通过使用“左侧”侧的相应字段轻松解决此问题,例如group tt by t.ToolId into g.

但这不会使查询完全可翻译。为此,您需要(至少使用当前的 EF Core 查询转换器)在构建 LINQ 查询时遵循一些规则:

  1. 使用导航属性而不是手动连接。
  2. 特别是对于GroupBy带有聚合的查询,始终使用构造的{element}一部分group {element} by {key}来预选聚合方法中需要的所有表达式。
  3. 在 SQL(服务器端)查询中,null自然支持 s,即使数据类型不可为空并且来自连接的可选端。但不是在 C# 中,因此您必须使用强制转换指定显式。
  4. 始终使用Min和的可空重载Max。如果需要,将结果(不是操作数)转换为标记值。但最好保留它null和可为空的类型。避免DateTime.MinValue,因为它在 SqlServer 中没有表示形式。

将它们应用于您的查询:

var max =
    (from t in _fabContext.Tools
     from tt in t.ToolTask.DefaultIfEmpty()
     let ts = tt.Task
     group new
     {
         SortOrder = (int?)tt.SortOrder,
         ExportDate = (DateTime?)ts.ExportDate
     }
     by new { t.ToolId }
     into g
     select new
     {
         ToolId = g.Key.ToolId,
         MaxOrder = g.Max(e => e.SortOrder) ?? 0,
         MaxExportDate = g.Max(e => e.ExportDate)
     })
    .ToDictionary(d => d.ToolId, d => new OrderExportDate
    {
        Order = d.MaxOrder,
        ExportDate = d.MaxExportDate ?? DateTime.MinValue
    });

很好地转换为单个 SQL 查询

  SELECT [t].[ToolId], MAX([t.ToolTask].[SortOrder]) AS [MaxOrder], MAX([t.ToolTask.Task].[ExportDate]) AS [MaxExportDate]
  FROM [Tool] AS [t]
  LEFT JOIN [ToolTask] AS [t.ToolTask] ON [t].[ToolId] = [t.ToolTask].[ToolId]
  LEFT JOIN [Task] AS [t.ToolTask.Task] ON [t.ToolTask].[TaskId] = [t.ToolTask.Task].[TaskId]
  GROUP BY [t].[ToolId]

这正是具有 SQL 背景的人所期望的。

如果您不关心翻译后的 SQL 查询的形状,则可以避免 group by,而只需以简单的逻辑方式编写 LINQ 查询:

var max2 = _fabContext.Tools
    .Select(t => new
    {
        t.ToolId,
        MaxOrder = t.ToolTask.Max(tt => (int?)tt.SortOrder),
        MaxExportDate = t.ToolTask.Max(tt => (DateTime?)tt.Task.ExportDate)
    })
    .ToDictionary(d => d.ToolId, d => new OrderExportDate
    {
        Order = d.MaxOrder ?? 0,
        ExportDate = d.MaxExportDate ?? DateTime.MinValue
    });
于 2019-03-11T08:05:45.950 回答