Как подсчитать связанные объекты, не извлекая их в Entity Framework

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

Представьте, что у меня есть обсуждение со связанным списком сообщений:

DiscussionCategory discussionCategory = _repository.GetDiscussionCategory(id);

discoveryCategory.Discussions — это список сущностей Discussion, которые в данный момент не загружены.

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

Когда я пробовал это раньше, мне приходилось загружать обсуждения и сообщения, чтобы я мог сделать что-то вроде этого:

discussionCategory.Discussions.Attach(Model.Discussions.CreateSourceQuery().Include("Messages").AsEnumerable());

foreach(Discussion discussion in discussionCategory.Discussions)
{

int messageCount = discussion.Messages.Count;

Console.WriteLine(messageCount);

}

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

Я видел несколько вопросов, затрагивающих эту тему, но, похоже, они не затрагивали ее напрямую.

Заранее спасибо за любые мысли, которые могут у вас возникнуть на эту тему.

Обновление. Еще немного кода по запросу:

public ActionResult Details(int id)
    {  
        Project project = _repository.GetProject(id);
        return View(project);
    }

Затем в представлении (просто для проверки):

Model.Discussions.Load();
var items = from d in Model.Discussions select new { Id = d.Id, Name = d.Name, MessageCount = d.Messages.Count() };

foreach (var item in items) {
//etc

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


person Oligarchia    schedule 06.01.2010    source источник


Ответы (6)


Легкий; просто проецируйте на тип POCO (или анонимный):

var q = from d in Model.Discussions
        select new DiscussionPresentation
        {
            Subject = d.Subject,
            MessageCount = d.Messages.Count(),
        };

Когда вы посмотрите на сгенерированный SQL, вы увидите, что Count() выполняется сервером БД.

Обратите внимание, что это работает как в EF 1, так и в EF 4.

person Craig Stuntz    schedule 06.01.2010
comment
Привет, спасибо за ответ. К сожалению, я пробовал это раньше и обнаружил, что свойство count анонимного типа возвращает ноль для всех обсуждений. Я попробовал этот метод еще раз, увидев ваш ответ, но с тем же результатом. Возможно, я что-то неправильно понимаю в структуре, поскольку речь идет о том, чтобы объекты были «подключены» к хранилищу данных. Может ли кто-нибудь еще подтвердить, что вышеуказанный метод должен работать? - person Oligarchia; 06.01.2010
comment
Покажите свой код; ты делаешь что-то не так. Мы используем эту функцию широко в наших приложениях для доставки. Я подозреваю, что вы пытаетесь сделать это в свойстве ассоциации EF (как в вашем вопросе), а не в запросе L2E (как в моем ответе). Это совершенно другое; первый — LINQ to Objects; последний — LINQ to Entities. - person Craig Stuntz; 06.01.2010
comment
Код, который я использовал, был идентичен по форме тому, что вы предоставили. Возможно, дальше по цепочке что-то идет не так. Я посмотрю, смогу ли я отредактировать свой исходный пост, чтобы предоставить дополнительный код, если вы считаете, что это поможет. Еще раз спасибо за вашу помощь. Я смотрю на сообщение в блоге, на которое вы ссылаетесь прямо сейчас. - person Oligarchia; 06.01.2010
comment
После прочтения сообщения в блоге, которое вы предоставили, кажется, что моя проблема может заключаться в понимании того, как использовать репозиторий для предоставления той же функциональности, которую вы демонстрируете, напрямую используя контекст. - person Oligarchia; 06.01.2010
comment
Это бы объяснило. Обратите внимание, что пока ваш репозиторий возвращает IQueryable<>, вы можете делать такие проекции даже за пределами репозитория, даже если это IQueryable<SomePoco> вместо IQueryable<SomeEntity>. Пока вы остаетесь в домене IQueryable, EF всегда будет возвращать Count() обратно в SQL. - person Craig Stuntz; 06.01.2010
comment
Спасибо за подсказку, пойду попробую. - person Oligarchia; 06.01.2010
comment
Только что попытался использовать этот пример, но понял, что d.Messages.Count() выполняется для каждого элемента в Model.Discussions. - person Bruno Santos; 10.02.2019

Если вы используете Entity Framework 4.1 или более позднюю версию, вы можете использовать:

var discussion = _repository.GetDiscussionCategory(id);

// Count how many messages the discussion has 
var messageCount = context.Entry(discussion)
                      .Collection(d => d.Messages)
                      .Query()
                      .Count();

Источник: http://msdn.microsoft.com/en-US/data/jj574232.

person Ricardo    schedule 27.09.2012

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

Я предполагаю прямые модели POCO и Code First, как в шаблонах и примерах. Хотя решение SQL View удобно с точки зрения администратора баз данных, оно вновь ставит задачу параллельной поддержки как кода, так и структур базы данных. Для простых агрегированных SQL-запросов вы не увидите большого прироста скорости от представления. Чего вам действительно нужно избегать, так это множественных (n+1) запросов к базе данных, как в приведенных выше примерах. Если у вас есть 5000 родительских объектов и вы считаете дочерние объекты (например, сообщения на обсуждение), это 5001 SQL-запрос.

Вы можете вернуть все эти счетчики в одном SQL-запросе. Вот как.

  1. Добавьте свойство-заполнитель в модель класса, используя аннотацию данных [NotMapped] из пространства имен System.ComponentModel.DataAnnotations.Schema. Это дает вам место для хранения рассчитанных данных без фактического добавления столбца в вашу базу данных или проецирования на ненужные модели представления.

    ...
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace MyProject.Models
    {
        public class Discussion
        {
            [Key]
            public int ID { get; set; }
    
            ...
    
            [NotMapped]
            public int MessageCount { get; set; }
    
            public virtual ICollection<Message> Messages { get; set; }
        }
    }
    
  2. В вашем контроллере получите список родительских объектов.

    var discussions = db.Discussions.ToList();
    
  3. Запишите количество в словарь. При этом создается один запрос SQL GROUP BY со всеми идентификаторами родительских объектов и количеством дочерних объектов. (Предположим, что DiscussionID — это FK в Messages.)

    var _counts = db.Messages.GroupBy(m => m.DiscussionID).ToDictionary(d => d.Key, d => d.Count());
    
  4. Прокрутите родительские объекты, найдите количество в словаре и сохраните в свойстве-заполнителе.

    foreach (var d in discussions)
        {
            d.MessageCount = (_counts.ContainsKey(d.ID)) ? _counts[d.ID] : 0;
        }
    
  5. Верните свой список обсуждений.

    return View(discussions);
    
  6. Ссылка на свойство MessageCount в представлении.

    @foreach (var item in Model) {
        ...
        @item.MessageCount
        ...
    }
    

Да, вы можете просто вставить этот словарь в ViewBag и выполнить поиск непосредственно в представлении, но это запутает ваше представление кодом, который не должен там быть.

В конце концов, я бы хотел, чтобы у EF был способ «ленивого подсчета». Проблема как с ленивой, так и с явной загрузкой заключается в том, что вы загружаете объекты. И если вам нужно загрузить, чтобы считать, это потенциальная проблема с производительностью. Ленивый подсчет не решит проблему n+1 в списковых представлениях, но было бы неплохо иметь возможность просто вызывать @item.Messages.Count из представления, не беспокоясь о потенциальной загрузке тонн нежелательных данных объекта.

Надеюсь это поможет.

person Neil Laslett    schedule 14.05.2015

У меня нет прямого ответа, но я могу только указать вам на следующее сравнение между NHibernate и EF 4.0, которое, кажется, предполагает, что даже в EF 4.0 нет готовой поддержки для получения количества связанной коллекции сущностей без извлечения коллекция.

http://ayende.com/Blog/archive/2010/01/05/nhibernate-vs.-entity-framework-4.0.aspx

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

person Brian Hasden    schedule 06.01.2010
comment
Это нормально. Как я уже сказал, у меня нет прямого ответа на его вопрос, но в Интернете есть много материалов, говорящих о том, что подсчет либо сложен, либо невозможен. Я не склонен верить кому-то, когда они говорят, что что-то невозможно, но я хотел представить это спрашивающему и начать дискуссию. - person Brian Hasden; 06.01.2010

Я столкнулся с той же проблемой при работе с несколькими преобразователями, включая EF и DevExpress XPO (которые даже не позволяют одному объекту сопоставляться с несколькими таблицами). То, что я нашел лучшим решением, - это в основном использовать шаблоны EDMX и T4 для создания обновляемых представлений в SQL Server (с вместо триггеров), и таким образом у вас есть низкоуровневый контроль над sql, поэтому вы можете выполнять подзапросы в select предложение, использовать все виды сложных соединений для ввода данных и так далее.

person Sheldmandu    schedule 18.09.2013

Если это не единичный случай, и вам нужно подсчитать несколько различных связанных сущностей, представление базы данных может быть более простым (и, возможно, более подходящим) выбором:

  1. Создайте представление базы данных.

    Предполагая, что вам нужны все исходные свойства объекта плюс связанное количество сообщений:

    CREATE VIEW DiscussionCategoryWithStats AS
    SELECT dc.*,
          (SELECT count(1) FROM Messages m WHERE m.DiscussionCategoryId = dc.Id)
              AS MessageCount
    FROM DiscussionCategory dc
    

    (Если вы используете Entity Framework Code First Migrations, см. этот ответ SO о том, как создать представление.)

  2. В EF просто используйте представление вместо исходного объекта:

    // You'll need to implement this!
    DiscussionCategoryWithStats dcs = _repository.GetDiscussionCategoryWithStats(id);
    
    int i = dcs.MessageCount;
    ...
    
person Dunc    schedule 14.01.2015