Нулевое значение в группе после левого внешнего соединения в EF Core - PullRequest
0 голосов
/ 05 марта 2019

У меня проблема с запросом linq, когда я пытаюсь извлечь из него определенную информацию и просто не могу найти, где проблема.У меня есть инструменты, задачи и сущность MN ToolTask ​​в моей базе ef core.Этот запрос включает 2 левых внешних соединения и группу в конце.

Несмотря на то, что я проверяю, являются ли сгруппированные значения не равными NULL, я все еще получаю сообщение об ошибке типа «Обнуляемый объект должен иметь значение».Что мне здесь не хватает?

Также кажется, что этот запрос 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;
        }
    }
}

ToolTask ​​

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));
        }
    }
}

Каждый класс сущностей имеет свой собственный класс конфигурации, но в нем нет ничего огромного, кроме установки первичных ключейи режим доступа к резервным полям.

1 Ответ

0 голосов
/ 11 марта 2019

Проблема с этой реализацией состоит в том, что вы группируете по 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 поддерживаются естественным образом, даже когда тип данных не имеет значения NULL и идет с необязательной стороны объединений.Но не в 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, вы можете избежать группировки по группам и просто написать запрос 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
    });
...