-
Есть 3 модели:
Select_related для ManyToManyField не работает. Как правильно выбрать Пост, все связаные Теги и Жанры одним запросом, что б при Post.tags.all() не посылался лишний запрос в базу. При выводе 20 статей будет до 40 запросов. Находил патчи для обратных связей в select_related, но как-то не хочется использовать такое решение. Raw sql не подходит - нужно иметь доступ к методам моделей Tags и Genres при работе с результатами запроса. Может можно как-то изменить объект query в менеджере?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) -
Попробуй использовать эту функцию, она предназначена для 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 -
Для ManyToMany работать не будет потому что связаные обьекты не получим вот так:
related_list.filter(**{'%s__in' % field.name: pks})
При ForeignKey связаные обьекты получать через obj.cache_name? -
Вообще, я не очень представляю себе, как это в принципе SQL'ом делать. Очень неудобная задача для реляционной базы.
-
Для ManyToMany работать не будет потому что связаные обьекты не получим вот так:
related_list.filter(**{'%s__in' % field.name: pks})Не подумал :)
Да.При ForeignKey связаные обьекты получать через obj.cache_name? -
Достаточно часто возникающий вопрос на форуме. Как показала практика, при не катастрофических размерах M2M табличек, практичней вытягивать все связи с требуемыми объектами и потом уже на памяти их группировать как надо.
В данном конкретном случае будет ещё одна задача - объединить два множества связей в одно, но я думаю этот challenge не очень сложный.
-
Я представляю это вот так:
Если вытягивать запросом, в результате получаем массив результатов, а не обьекты моделей. Как тогда получать доступ к методам моделей, например get_absolute_url? Для тегов конечно можно и обойтись, а если модель сложная и содержит много необходимых методов.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
Когда-то сталкивался с 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 -
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А если представить, что у разных постов разный набор тегов и жанров, то у результатов должно быть разное число колонок, а так не бывает. Именно это я имел в виду, когда говорил, что для реляционной базы это очень неудобная задача.
Соответственно, как сказал выше Саша, чтобы показывать список постов со всеми тегами и жанрами, надо будет вытащить из базы все жанры и теги и составлять их с постами уже в питоньем коде.
-
Я представляю это вот так:
Зачем вам 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
-
А именно, он выдаст гораздо больше строк постов, чем их существуетКак работает LEFT JOIN я знаю и запрос этот проверял. Просто разве ORM не создан для того что бы преобразовывать данные из реляционной в обьектную модель.
Такая идея была. Просто думал сначала спросить перед тем как менять структуру.Для начала вы должны сделать таблички связей явными:
В общем ясно, что Django ORM сам на такое не способен. Можно нужный функционал моделей вынести в статические методы и вызывать их передавая необходимые данные. -
Просто разве ORM не создан для того что бы преобразовывать данные из реляционной в обьектную модель
В такой постановке это некорректный вопрос :-). "ORM" — это же не продукт, а абстрактная концепция. Которая, как показывает практика, вообще никем нормально не реализована. Джанговский же ORM, вообще говоря, никогда не задумывался как "настоящий" ORM, обеспечивающий прозрачный маппинг объектов в реляционную базу. Его правильнее воспринимать как SQL-конструктор, который не пытается делать никакой особенной магии.
-
Понятно :) Спасибо большое. Посмотрю на патч, который позволял в select_related передавать любые связаные модели и попытаюсь написать что-то вроде load_related_fk_m2m.
Магии он не пытается делать и не позволяет другим :)Его правильнее воспринимать как SQL-конструктор, который не пытается делать никакой особенной магии. -
Вот написал подобное вышеупомянутой функции только для 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 -
'DatabaseWrapper' object is not callable выдает :(
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.

