1. Alerion

    21.04.2009

    0 ↑
    0 ↓
    Есть 3 модели:
    class Post(models.Model):
    name = models.CharField(max_length=255)
    tags = models.ManyToManyField('Tags')
    genres = models.ManyToManyField('Genre')

    class Tags(models.Model):
    name = models.CharField(max_length=255, unique=True)

    class Genre(models.Model):
    name = models.CharField('Название', max_length=255, unique=True)
    Select_related для ManyToManyField не работает. Как правильно выбрать Пост, все связаные Теги и Жанры одним запросом, что б при Post.tags.all() не посылался лишний запрос в базу. При выводе 20 статей будет до 40 запросов. Находил патчи для обратных связей в select_related, но как-то не хочется использовать такое решение. Raw sql не подходит - нужно иметь доступ к методам моделей Tags и Genres при работе с результатами запроса. Может можно как-то изменить объект query в менеджере?
  2. vit-ivanov

    21.04.2009

    0 ↑
    0 ↓
    Попробуй использовать эту функцию, она предназначена для ForeignKey, но я думаю что принцип тот же.
    def load_related_fk_reverse(object_list, related_list, fk_name, cache_name=None):
    """
    Вытягивает объекты связанный через ForeignKey одним запросом,
    корректно работает для ForeignKey с null=True
    """
    if not object_list or related_list == []:
    return object_list

    if isinstance(related_list, QuerySet):
    field = related_list.model._meta.get_field(fk_name)
    else:
    field = related_list[0]._meta.get_field(fk_name)

    attname = field.get_attname()
    if cache_name is None:
    cache_name = '%s_cache' % field.rel.related_name

    if isinstance(related_list, QuerySet):
    pks = list(set([obj.pk for obj in object_list]))
    related_list = related_list.filter(**{'%s__in' % field.name: pks})

    related_dict = {}
    if isinstance(field, models.OneToOneField) or isinstance(field, models.ForeignKey) and field.unique:
    for obj in related_list:
    related_dict[getattr(obj, attname)] = obj
    else:
    for obj in related_list:
    related_dict.setdefault(getattr(obj, attname), []).append(obj)

    for obj in object_list:
    try:
    setattr(obj, cache_name, related_dict[obj.pk])
    except KeyError:
    pass

    return object_list
  3. Alerion

    21.04.2009

    0 ↑
    0 ↓
    Для ManyToMany работать не будет потому что связаные обьекты не получим вот так:
    related_list.filter(**{'%s__in' % field.name: pks})

    При ForeignKey связаные обьекты получать через obj.cache_name?
  4. Ivan Sagalaev

    21.04.2009

    0 ↑
    0 ↓

    Вообще, я не очень представляю себе, как это в принципе SQL'ом делать. Очень неудобная задача для реляционной базы.

  5. vit-ivanov

    21.04.2009

    0 ↑
    0 ↓
    Для ManyToMany работать не будет потому что связаные обьекты не получим вот так:
    related_list.filter(**{'%s__in' % field.name: pks})
    Не подумал :)
    При ForeignKey связаные обьекты получать через obj.cache_name?
    Да.
  6. Достаточно часто возникающий вопрос на форуме. Как показала практика, при не катастрофических размерах M2M табличек, практичней вытягивать все связи с требуемыми объектами и потом уже на памяти их группировать как надо.

    В данном конкретном случае будет ещё одна задача - объединить два множества связей в одно, но я думаю этот challenge не очень сложный.

  7. Alerion

    22.04.2009

    0 ↑
    0 ↓
    Я представляю это вот так:
    SELECT p.id, p.name, t.id, t.name
    FROM post AS p
    LEFT JOIN post_tags AS pt ON p.id = pt.post_id
    LEFT JOIN tags AS t ON t.id = pt.tags_id
    Если вытягивать запросом, в результате получаем массив результатов, а не обьекты моделей. Как тогда получать доступ к методам моделей, например get_absolute_url? Для тегов конечно можно и обойтись, а если модель сложная и содержит много необходимых методов.

    Когда-то сталкивался с Doctrine, там существовал метод join в который передавалось имя связываемой таблици. Так как модели извесно через какое поле и промежуточную таблицу она связана с другой, он сам строил JOIN:
    LEFT JOIN post_tags AS pt ON p.id = pt.post_id
    LEFT JOIN tags AS t ON t.id = pt.tags_id
  8. Ivan Sagalaev

    22.04.2009

    0 ↑
    0 ↓
    SELECT p.id, p.name, t.id, t.name
    FROM post AS p
    LEFT JOIN post_tags AS pt ON p.id = pt.post_id
    LEFT JOIN tags AS t ON t.id = pt.tags_id
    

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

    Сырые данные вместо объектов — это тут самая мелкая проблема. На самом деле этот запрос даст совсем не то, что вы от него ожидаете. А именно, он выдаст гораздо больше строк постов, чем их существует: они будут дублированы на каждую комбинацию из одного своего тега и одного своего жанра (хотя тут нет жанров, но принцип понятен).

    То есть, если например у поста p1 есть теги t1, t2 и жанры g1, g2, то получится:

    p1 t1 g1
    p1 t1 g2
    p1 t2 g1
    p1 t2 g2
    

    А совсем не:

    p1 t1 t2 g1 g2
    

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

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

  9. Я представляю это вот так:

    Зачем вам SQL? Забудьте про него.

    Для начала вы должны сделать таблички связей явными:

    class Post(models.Model):
       #...
    
       tags = models.ManyToManyField('Tag', through='Tagging')
       genres = models.ManyToManyField('Genre', through='Classification')
    
    class Tag(models.Model):
       #...
    
    class Genre(models.Model):
       #...
    
    class Tagging(models.Model):
       post = models.ForeighKey(Post)
       tag = models.ForeignKey(Tag)
    
    class Classification(models.Model):
       post = models.ForeignKey(Post)
       genre = models.ForeignKey(Genre)
    

    Потом вытягиваете в 2 запроса нужные связи с объектами:

    lookup = {'post__...': ...}
    
    tagging = Tagging.objects.filter(**lookup).select_related()
    classification = Classification.objects.filter(**lookup).select_related()
    

    Потом сгруппировать:

    from django.utils.itercompat import group_by
    
    posts_with_tags = dict(group_by(tagging, lambda t: t.post))
    posts_with_genres = dict(group_by(classification, lambda g: g.post))
    

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

    Но учтите, что при большом количестве связей может просесть производительность из-за select_related. Но даже в этой ситуации код можно ещё больше оптимизировать.

    Кстати, модели лучше называть существительными в единственном числе - Tags -> Tag

  10. Alerion

    22.04.2009

    0 ↑
    0 ↓
    А именно, он выдаст гораздо больше строк постов, чем их существует
    Как работает LEFT JOIN я знаю и запрос этот проверял. Просто разве ORM не создан для того что бы преобразовывать данные из реляционной в обьектную модель.
    Для начала вы должны сделать таблички связей явными:
    Такая идея была. Просто думал сначала спросить перед тем как менять структуру.

    В общем ясно, что Django ORM сам на такое не способен. Можно нужный функционал моделей вынести в статические методы и вызывать их передавая необходимые данные.
  11. Ivan Sagalaev

    22.04.2009

    0 ↑
    0 ↓

    Просто разве ORM не создан для того что бы преобразовывать данные из реляционной в обьектную модель

    В такой постановке это некорректный вопрос :-). "ORM" — это же не продукт, а абстрактная концепция. Которая, как показывает практика, вообще никем нормально не реализована. Джанговский же ORM, вообще говоря, никогда не задумывался как "настоящий" ORM, обеспечивающий прозрачный маппинг объектов в реляционную базу. Его правильнее воспринимать как SQL-конструктор, который не пытается делать никакой особенной магии.

  12. Alerion

    22.04.2009

    0 ↑
    0 ↓
    Понятно :) Спасибо большое. Посмотрю на патч, который позволял в select_related передавать любые связаные модели и попытаюсь написать что-то вроде load_related_fk_m2m.
    Его правильнее воспринимать как SQL-конструктор, который не пытается делать никакой особенной магии.
    Магии он не пытается делать и не позволяет другим :)
  13. Alerion

    23.04.2009

    0 ↑
    0 ↓
    Вот написал подобное вышеупомянутой функции только для m2m. Всего один запрос на одну связаную таблицу.
    #models.py
    class Post(models.Model):
    name = models.CharField(max_length=255)
    tags = models.ManyToManyField('Tag', blank=True)
    genres = models.ManyToManyField('Genre', blank=True)

    class Tag(models.Model):
    name = models.CharField(max_length=255, unique=True)

    class Genre(models.Model):
    name = models.CharField(max_length=255, unique=True)

    #views.py
    def test(request):
    from lib import load_related_m2m
    from app.models import Post
    p = Post.objects.all()[:10]
    load_related_m2m(p, 'tags')
    load_related_m2m(p, 'genres')
    return {'posts': p}

    #test.html
    {% for item in posts %}
    <h3>{{ item.name }}</h3><br/>
    Теги:
    {% for tag in item.all_tags %}
    {{ tag.name }},
    {% endfor %}<br/>
    Жанры:
    {% for g in item.all_genres %}
    {{ g.name }},
    {% endfor %}<br/><br/>
    {% endfor %}

    #lib.py
    from django.db.models.sql.constants import LOOKUP_SEP
    from django.db.models import sql
    from django.db import connection

    def load_related_m2m(object_list, field):

    select_fields = ['pk']
    related_field = object_list.model._meta.get_field(field)
    related_model = related_field.rel.to
    cache_name = 'all_%s' % field

    for f in related_model._meta.local_fields:
    select_fields.append('%s%s%s' % (field, LOOKUP_SEP, f.column))

    query = sql.Query(object_list.model, connection)
    query.add_fields(select_fields)
    query.add_filter(('pk__in', [obj.pk for obj in object_list]))

    related_dict = {}
    for row in query.results_iter():
    if row[2]:
    related_dict.setdefault(row[0], []).append(related_model(*row[1:]))

    for obj in object_list:
    try:
    setattr(obj, cache_name, related_dict[obj.pk])
    except KeyError:
    setattr(obj, cache_name, [])

    return object_list
  14. DataGreed

    19.08.2010

    0 ↑
    0 ↓
    'DatabaseWrapper' object is not callable выдает :(

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