Я работаю над проектом IMDB Clone ради обучения. Он имеет две важные модели: Movie и Celebrity.
Модель фильма имеет три поля MTM, все из которых связаны с моделью Celebrity.
class Movie(models.Model):
# .. unrelated fields deleted for code brevity ..
directors = models.ManyToManyField(celeb_models.Celebrity, related_name='movies_as_director',
limit_choices_to=Q(duties__name__icontains='Director'))
writers = models.ManyToManyField(to=celeb_models.Celebrity, related_name='movies_as_writer',
limit_choices_to=Q(duties__name__icontains='Writer'))
casts = models.ManyToManyField(to=celeb_models.Celebrity, through='MovieCast')
Я хотел бы удалить все три поля и добавить только одну MTMfield.
class Movie(models.Model):
# one 'crews' field takes the place of three fields ('directors', 'writers', 'casts')
# but it shows bad query performance.
crews = models.ManyToManyField(celeb_models.Celebrity, through='MovieCrew', related_name='movies')
И создал промежуточную модель, которая имеет несколько методов и собственный менеджер (который должен творить чудеса).
class MovieCrewManager(models.Manager):
def get_queryset(self):
return super().get_queryset()
def get_directors(self):
qs = self.get_queryset()
return qs.filter(duty__name__icontains='Director').select_related('crew')
def get_writers(self):
qs = self.get_queryset()
return qs.filter(duty__name__icontains='Writer').select_related('crew')
def get_casts(self):
qs = self.get_queryset()
return qs.filter(duty__name__icontains='Cast').select_related('crew')
class MovieCrew(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_crews') #Movie Model
duty = models.ForeignKey(celeb_models.Duty, default=1, on_delete=models.CASCADE)
crew = models.ForeignKey(celeb_models.Celebrity, on_delete=models.CASCADE) # Celebrity Model
role = models.CharField(max_length=75, default='', blank=True,
help_text='e.g. short story, scrrenplay for writer, voice for cast')
screen_name = models.CharField(max_length=75, default='', blank=True,
help_text="crew's name on movie")
objects = MovieCrewManager()
def clean(self, *args, **kwargs):
if not self.duty in self.crew.duties.all():
raise ValidationError('crew duty and selected duty should match', code='invalid')
super(MovieCrew, self).clean(*args, **kwargs)
def save(self, *args, **kwargs):
self.full_clean()
super(MovieCrew, self).save(*args, **kwargs)
def __str__(self):
return self.crew.full_name
Причиной уменьшения числа полей былоожидание лучшей производительности. Потому что я называю три разных запроса для всех трех MTM, которые действительно относятся к одной и той же модели (Celebrity). Однако на данный момент я сохранил все четыре поля, поскольку не смог получить ожидаемую производительность запросов.
На четырех страницах (просмотрах) показаны фильмы с одним шаблоном. Также я создал своих аналогов. Итак, у меня есть куча плохих и куча хороших исполнительских страниц, чтобы увидеть разницу.
Одно из представлений, выполняющих ХОРОШО (три поля фильма: актеры, режиссеры, писатели):
class MovieListMixin2(ListView):
queryset = movie_model.objects.prefetch_related(
'writers', 'casts', 'directors', 'genres', 'comments')
template_name = 'movies/index2.html'
paginate_by = pagination
class IndexView2(MovieListMixin2):
ordering = ('-release_year', 'title')
def get_context_data(self, **kwargs):
context = super(IndexView2, self).get_context_data(**kwargs)
context['title'] = '(GQ) Latest movies'
context['title_suffix'] = 'by release date'
return context
и его шаблон (сокращенно для краткости):
{% for movie in object_list %}
[...]
<p class="small"><strong>Directors:</strong>
{% for director in movie.directors.all %}
<a href="{% url 'celebs:celeb_detail' director.id director.slug %}">{{ director.full_name }}</a>,
{% endfor %}
</p>
<p class="small"><strong>Writers:</strong>
{% for writer in movie.writers.all %}
<a href="{% url 'celebs:celeb_detail' writer.id writer.slug %}">{{ writer.full_name }}</a>,
{% endfor %}
</p>
<p class="small"><strong>Stars:</strong>
{% for cast in movie.casts.all %}
<a href="{% url 'celebs:celeb_detail' cast.id cast.slug %}">{{ cast.full_name }}</a>,
{% endfor %}
</p>
[...]
{% endfor %}
И аналог того же представления, но это выполняет ПЛОХО (одно поле Кино: экипажи):
class MovieListMixin(ListView):
queryset = movie_model.objects.prefetch_related('movie_crews', 'genres', 'comments')
template_name = 'movies/index.html'
paginate_by = pagination
class IndexView(MovieListMixin):
ordering = ('-release_year', 'title')
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
context['title'] = 'Latest movies'
context['title_suffix'] = 'by release date'
return context
и снова его шаблон (сокращенно для краткости):
{% for movie in object_list %}
[...]
<p class="small"><strong>Directors:</strong>
{% for director in movie.movie_crews.get_directors %}
<a href="{% url 'celebs:celeb_detail' director.crew.id director.crew.slug %}">{{ director.crew.full_name }}</a>,
{% endfor %}
</p>
<p class="small"><strong>Writers:</strong>
{% for writer in movie.movie_crews.get_writers %}
<a href="{% url 'celebs:celeb_detail' writer.crew.id writer.crew.slug %}">{{ writer.crew.full_name }}</a>,
{% endfor %}
</p>
<p class="small"><strong>Stars:</strong>
{% for cast in movie.movie_crews.get_casts %}
<a href="{% url 'celebs:celeb_detail' cast.crew.id cast.crew.slug %}">{{ cast.crew.full_name }}</a>,
{% endfor %}
</p>
[...]
{% endfor %}
Я не получаю никаких ошибок от обеих реализаций. С другой стороны,
ХОРОШАЯ страница выполнения имеет только 9 запросов , выполняющихся за 2,42 мс .
SELECT ••• FROM "movies_movie"
SELECT ••• FROM "django_session" WHERE ("django_session"."expire_date" > '''2019-10-03 02:20:13.197659''' AND "django_session"."session_key" = '''bftca58feksf1cbo17qzgc40l24eb893''')
SELECT ••• FROM "users_user" WHERE "users_user"."id" = '1'
SELECT ••• FROM "movies_movie" ORDER BY "movies_movie"."release_year" DESC, "movies_movie"."title" ASC LIMIT 5
SELECT ••• FROM "celebs_celebrity" INNER JOIN "movies_movie_writers" ON ("celebs_celebrity"."id" = "movies_movie_writers"."celebrity_id") WHERE "movies_movie_writers"."movie_id" IN ('8', '3', '7', '9', '6') ORDER BY "celebs_celebrity"."last_name" ASC, "celebs_celebrity"."first_name" ASC
SELECT ••• FROM "celebs_celebrity" INNER JOIN "movies_moviecast" ON ("celebs_celebrity"."id" = "movies_moviecast"."cast_id") WHERE "movies_moviecast"."movie_id" IN ('8', '3', '7', '9', '6') ORDER BY "celebs_celebrity"."last_name" ASC, "celebs_celebrity"."first_name" ASC
SELECT ••• FROM "celebs_celebrity" INNER JOIN "movies_movie_directors" ON ("celebs_celebrity"."id" = "movies_movie_directors"."celebrity_id") WHERE "movies_movie_directors"."movie_id" IN ('8', '3', '7', '9', '6') ORDER BY "celebs_celebrity"."last_name" ASC, "celebs_celebrity"."first_name" ASC
SELECT ••• FROM "movies_genre" INNER JOIN "movies_movie_genres" ON ("movies_genre"."id" = "movies_movie_genres"."genre_id") WHERE "movies_movie_genres"."movie_id" IN ('8', '3', '7', '9', '6') ORDER BY "movies_genre"."name" ASC
SELECT ••• FROM "reviews_moviecomment" WHERE "reviews_moviecomment"."movie_id" IN ('8', '3', '7', '9', '6')
Страница выполнения BAD имеет 22 запроса , выполняющихся за 5,65 мс .
SELECT ••• FROM "movies_movie"
SELECT ••• FROM "django_session" WHERE ("django_session"."expire_date" > '''2019-10-03 02:54:13.177499''' AND "django_session"."session_key" = '''bftca58feksf1cbo17qzgc40l24eb893''')
SELECT ••• FROM "users_user" WHERE "users_user"."id" = '1'
SELECT ••• FROM "movies_movie" ORDER BY "movies_movie"."release_year" DESC, "movies_movie"."title" ASC LIMIT 5
SELECT ••• FROM "movies_moviecrew" WHERE "movies_moviecrew"."movie_id" IN ('8', '3', '7', '9', '6')
SELECT ••• FROM "movies_genre" INNER JOIN "movies_movie_genres" ON ("movies_genre"."id" = "movies_movie_genres"."genre_id") WHERE "movies_movie_genres"."movie_id" IN ('8', '3', '7', '9', '6') ORDER BY "movies_genre"."name" ASC
SELECT ••• FROM "reviews_moviecomment" WHERE "reviews_moviecomment"."movie_id" IN ('8', '3', '7', '9', '6')
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '8' AND "celebs_duty"."name" LIKE '''%Director%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '8' AND "celebs_duty"."name" LIKE '''%Writer%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '8' AND "celebs_duty"."name" LIKE '''%Cast%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '3' AND "celebs_duty"."name" LIKE '''%Director%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '3' AND "celebs_duty"."name" LIKE '''%Writer%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '3' AND "celebs_duty"."name" LIKE '''%Cast%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '7' AND "celebs_duty"."name" LIKE '''%Director%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '7' AND "celebs_duty"."name" LIKE '''%Writer%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '7' AND "celebs_duty"."name" LIKE '''%Cast%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '9' AND "celebs_duty"."name" LIKE '''%Director%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '9' AND "celebs_duty"."name" LIKE '''%Writer%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '9' AND "celebs_duty"."name" LIKE '''%Cast%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '6' AND "celebs_duty"."name" LIKE '''%Director%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '6' AND "celebs_duty"."name" LIKE '''%Writer%''' ESCAPE '\')
15 similar queries.
SELECT ••• FROM "movies_moviecrew" INNER JOIN "celebs_duty" ON ("movies_moviecrew"."duty_id" = "celebs_duty"."id") INNER JOIN "celebs_celebrity" ON ("movies_moviecrew"."crew_id" = "celebs_celebrity"."id") WHERE ("movies_moviecrew"."movie_id" = '6' AND "celebs_duty"."name" LIKE '''%Cast%''' ESCAPE '\')
15 similar queries.
Итак, я хотел бы получить вашу помощь.
Хорошо лиуменьшить три поля MTM до одного поля MTM?
Если так, то почему он работает так плохо? И почему метод менеджера моделей создает дублированные запросы?
Спасибо.
PS: проект с открытым исходным кодом и размещен на github в качестве репозитория. Если вы хотите увидеть живой код, вы можете клонировать или загрузить его.
У него есть исходные данные для загрузки. Поэтому было бы легко заставить его работать за несколько минут. БД - sqlite3.
Если вы читаете этот пост в будущем и хотите увидеть код, я добавил его как ветку. Я надеюсь, что я не буду менять это. https://github.com/pydatageek/imdb-clone/tree/query_comparison_1