27.02.2009 11:19

  1. Добрый день!
    Мне кажется подобный вопрос уже обсуждался, но не могу найти пост...
    Поэтому решил написать.

    Есть такая модель:

    ClientOrder <- Goods (К заказу от клиента привязаны товары)

    
    class ClientOrder(models.Model):
        ....
    
        def __init__(self, *args, **kwargs):
            super(ClientOrder, self).__init__(*args, **kwargs)
            self.goods = self.goods_set.all()
    
        def get_goodsnum(self):
            return self.goods.__len__()
        get_goodsnum.short_description = 'кол-во'
    
        def get_sellsumm(self):
            summ = float(0)
            goods = self.goods
            for item in goods:
                if item.get_sellsumm():
                    summ += float(item.get_sellsumm())
            return round(summ,2)
        get_sellsumm.short_description = 'Сумма счета'
    
    
    class Goods(models.Model):   
        clientorder = models.ForeignKey(ClientOrder, verbose_name='заказ от клиента')
        ....
    


    Если мне нужно получить список из 100 заказов, то для того, чтобы сосчитать get_goodsnum и get_sellsumm dlango приходится делать помимо основного запроса еще 100 запросов, чтобы подгрузить товары, прикрепленные к заказу.

    Можно ли как-то сократить число запросов?
    Спасибо!
  2. tony

    0 ↑
    0 ↓
    ага, было, решение называется денормализацией
    вот здесь довольно глубоко рассмотрена проблема, но основная идея практически в первом абзаце.
    http://webnewage.org/2008/9/26/krasivaya-kompozitsiya/
  3. В нынешней Джанге это делается агрегациями:

    from django.db.models import Sum, Count
    
    ClientOrder.objects.all().annotate(Count('goods'), Sum('goods__sellsum'))
    

    (Наверное sellsum у вас в каком-нибудь поле лежит.)

    P.S. Несколько антипитонизмов у вас в коде есть:

    1. Для узнавания длинны не вызывают __len__ явно:

    self.goods.__len__()
    

    Вместо этого пишут:

    len(self.goods)
    

    Хотя поскольку это queryset, а не просто произвольная коллекция, лучше бывает сделать count-запрос, вместо того, чтобы вынимать их все из базы, чтобы посчитать количество:

    self.goods.count()
    

    2. Вот этот весь код:

    summ = float(0)
    goods = self.goods
    for item in goods:
        if item.get_sellsumm():
            summ += float(item.get_sellsumm())
    

    Записывается проще:

    sum(g.get_sellsum() for g in self.goods)
    
  4. igorekk

    0 ↑
    0 ↓
    Иван, а мне кажется, что агрегации тут не подходят.
    Ведь
    
    get_sellsumm.short_description = 'Сумма счета'
    

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

  6. igorekk

    0 ↑
    0 ↓
    Есть и очень облегчили жизнь :)
  7. Огромное спасибо за такой подробный ответ!
    В общем это имено то, что нужно =)

    Единственное что, интересует можно ли сделать запрос Sum от результата умножения двух полей бд?
    SELECT SUM(`orders_goods`.`sellprice`*`orders_goods`.`cnt`) AS `sellsumm` ...


    2 Иван Салагаев:
    sum(g.get_sellsum() for g in self.goods) — просто шик! =)
    Единственное пришлось дописать небольшой хвостик, чтобы значения None не складывались в сумму:
    sum(g.get_sellsum() for g in self.goods if g.get_sellsum())
  8. Единственное что, интересует можно ли сделать запрос Sum от результата умножения двух полей бд?

    У меня не получилось... Я бы вместо этого просто хранил в Goods результат этого умножения отдельным автоматически обновляемым полем:

    class Good(models.Model):
        # ...
        sum = models.DecimalField(...)
    
        def save(self, **kwargs):
            self.sum = self.price * self.count
            super(Good, self).save(**kwargs)
    
  9. Добавил автоматически обновляемое поле.
    В результате всех изменений теперь для генерации страницы требуется сделать 9 запросов вместо 272.
    Результат на лицо =)
    Спасибо за помощь!

  10. Иван, а мне кажется, что агрегации тут не подходят.
    Ведь

    get_sellsumm.short_description = 'Сумма счета'

    указывает на Django-админку.


    Действительно, эта используется в Django-админке. Но агрегация вполне подошла =)
    Если переопределить менеджер модели вот таким образом, то все получится:

    
    class ClientOrderManager(models.Manager):
        def get_query_set(self):
            return super(ClientOrderManager, self).get_query_set().select_related('client').annotate(goodsnum=Count('goods'), sellsumm=Sum('goods__sellsumm'), act_tasks=Count('clientordertask'))
    
    class ClientOrder(models.Model):
        .....
        objects = ClientOrderManager()
    
  11. Вообще, делать дефолтный менеджер таким нагруженным плохая идея. Лучше в самой админке переопределить метод queryset, чтобы это только там действовало.

  12. Вообще, делать дефолтный менеджер таким нагруженным плохая идея. Лучше в самой админке переопределить метод queryset, чтобы это только там действовало.


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

    Я вот думаю... Если переопределить queryset для админки, то получается что функции внутри модели, которые завязаны теперь на агрегации, будут работать только в админе:
    
    class ClientOrderManager(models.Manager):
        def get_query_set(self):
            return super(ClientOrderManager, self).get_query_set().select_related('client').annotate(goodsnum=Count('goods'), sellsumm=Sum('goods__sellsumm'), act_tasks=Count('clientordertask'))
    
    class ClientOrder(models.Model):
        ....
        def get_sellsumm(self):
            if self.sellsumm:
                return self.sellsumm
            else:
                return Decimal(0)
        get_sellsumm.short_description = 'Сумма счета'
    


    А они используются еще в некоторых местах кроме админки.
  13. Нет, ну если оно реально нужно, значит нужно :-). Впрочем, я бы наверное сделал как-то так: определил у менеджера модели метод, который бы выдавал аннотированный queryset, и явно вызывал его, где надо (в админке ли, в коде ли). Но, впрочем, тут вам виднее с точки зрения всего приложения.

    P.S. Еще один антипитонизм, раз уж мы здесь... Вместо:

    if self.sellsumm:
        return self.sellsumm
    else:
        return Decimal(0)
    

    Проще выглядит:

    return self.sellsum or Decimal(0)
    

    И раз уже на то пошло, похоже, что этот метод используется только для того, чтобы придать sellsum'у правильное нулевое значение вместо null, так? Тогда стоит просто прописать default для этого поля, чтобы он просто никогда не был null'ом. И нужда в методе отпадет.

  14. для админки ж вроде есть опция, какие связи вытягивать через select_related

  15. P.S. Еще один антипитонизм, раз уж мы здесь... Вместо:
    
    if self.sellsumm:
        return self.sellsumm
    else:
        return Decimal(0)
    


    Проще выглядит:
    return self.sellsum or Decimal(0)



    Вот как можно оказывается.. удобный язык =)
    Спасибо за совет!

Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.