Проецирование IEnumerable внутри Projected IQueryable с выполнением N запросов в базу данных - PullRequest
1 голос
/ 19 апреля 2019

Я делаю Asp.net Core Api и одно из действий контроллера, мне нужно вернуть IQueryable DTO, но одно из свойств является IEnumerable другого DTO в отношениях один ко многим в модель базы данных EF. Например:

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime Birthday { get; set; }
        public List<Order> Orders { get; set; }
    }

    public class Order
    {
        public int OrderNumber { get; set; }
        public Customer Customer { get; set; }
    }

И ДТО

    public class CustomerDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IEnumerable<OrderDTO> Orders { get; set; }
    }

    public class OrderDTO
    {
        public int OrderNumber { get; set; }
    }

Это всего лишь простой пример, потому что в моем приложении много полей в каждой таблице, и я не могу открыть все для внешнего интерфейса, поэтому я использую DTO.

Я использую Выбрать для проецирования каждого элемента в DTO, там нет никаких проблем, потому что я вижу на выходе ASP.NET Core Web Server, что система только делает один запрос к базе данных (чтобы получить Клиенты), но проблема возникает, когда я пытаюсь спроектировать OrdersDTO внутри CustomerDTO. Например, если у меня 100 клиентов, EF сделает 101 запрос к базе данных. (1 для получения клиентов и 100 для получения заказов для каждого клиента)

   [HttpGet]
   [EnableQuery]
   public IEnumerable<CustomerDTO> Get()
      {
        return context.Customer
        .Select(s => new CustomerDTO
        {
           Id = s.Id,
           Name = s.Name,
           Orders = s.Orders.Select(so => new OrderDTO
           {
              OrderNumber = so.OrderNumber
           })
         });
      }

Если я вызову ToList () перед тем, как спроецировать элементы с помощью Select, он сделает только один запрос к базе данных (как и предполагалось), но мне нужно вернуть IQueryable, потому что я использую OData, чтобы приложение внешнего интерфейса могло выполнять запросы непосредственно к базе данных, даже если это просто DTO

Я уже пробовал ставить вот так

     Orders = s.Orders.Any() ? s.Orders.Select(so => new OrderDTO
       {
         OrderNumber = so.OrderNumber
       }) : new List<OrderDTO>()

Это решило проблему частично, потому что, если из 100 клиентов только 50 имеют заказы, EF сделает только 50 запросов к базе данных.

Я хотел бы знать, есть ли решение этой проблемы, потому что я не хочу, чтобы приложение выполняло сотни запросов к базе данных каждый раз, когда какой-либо пользователь вызывает эту конечную точку API.

1 Ответ

2 голосов
/ 19 апреля 2019

Вам необходимо добавить ToList() при проецировании внутренней коллекции.

       Orders = s.Orders.Select(so => new OrderDTO
       {
          OrderNumber = so.OrderNumber
       }).ToList() // <--

Во-первых, потому что CustomerDTO.Orders тип свойства - List<OrderDTO>, поэтому код не компилируется без него.

Но даже этого не было (скажем, это IEnumerable<OrderDTO>), вам все еще нужно ToList, чтобы внедрить EF Core 2.1 Оптимизация коррелированных подзапросов

Мы улучшили перевод наших запросов, чтобы избежать выполнения запросов "N + 1" SQL во многих распространенных сценариях, в которых использование свойства навигации в проекции приводит к объединению данных из корневого запроса с данными из коррелированного подзапроса. Оптимизация требует буферизации результатов из подзапроса, и мы требуем, чтобы вы изменили запрос, чтобы включить новое поведение.

Обратите внимание на последнее предложение - «нам требуется , чтобы вы изменили запрос на opt-in новое поведение» . Затем документация содержит пример и продолжается:

Включив ToList() в нужном месте, вы указываете, что буферизация подходит для заказов, которые позволяют оптимизировать

...