Как правильно выполнять django-запросы полей ManyToMany - PullRequest
1 голос
/ 03 октября 2019

Я работаю над проектом 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.

Итак, я хотел бы получить вашу помощь.

  1. Хорошо лиуменьшить три поля MTM до одного поля MTM?

  2. Если так, то почему он работает так плохо? И почему метод менеджера моделей создает дублированные запросы?

Спасибо.

PS: проект с открытым исходным кодом и размещен на github в качестве репозитория. Если вы хотите увидеть живой код, вы можете клонировать или загрузить его.

У него есть исходные данные для загрузки. Поэтому было бы легко заставить его работать за несколько минут. БД - sqlite3.

Если вы читаете этот пост в будущем и хотите увидеть код, я добавил его как ветку. Я надеюсь, что я не буду менять это. https://github.com/pydatageek/imdb-clone/tree/query_comparison_1

1 Ответ

1 голос
/ 03 октября 2019

Проблема здесь в том, что каждый раз, когда вы делаете filter, как вы делаете во всех методах MovieCrewManager, это всегда возврат к базе данных - в обход оптимизации prefetch_related.

Я бы подошел к этому по-другому. Поскольку вы хотите получить все данные и извлекать их заранее, вы можете написать методы в Movie, чтобы использовать этот кеш, если он существует, и выполнять фильтрацию в Python. Что-то вроде:

class Movie(models.Model):       
    def _get_crew(self, duty_name):
        if hasattr(self, '_prefetched_objects_cache') and 'movie_crews' in self._prefetched_objects_cache:
            return [c for c in self._prefetched_objects_cache['movie_crews'] if c.duty.name == duty_name]
        else:
            return self.movie_crews.filter(duty__name=duty_name)

    @property
    def directors(self):
      return self._get_crew('Director')

    @property
    def writers(self):
      return self._get_crew('Writer')

    @property
    def cast(self):
      return self._get_crew('Cast')

Тогда ваш набор запросов представления может быть:

queryset = movie_model.objects.prefetch_related('movie_crews__duty', 'movie_crews__crew, 'genres', 'comments')  

, и ваш шаблон станет:

{% for director in movie.directors %}
    <a href="{% url 'celebs:celeb_detail' director.crew.id director.crew.slug %}">{{ director.crew.full_name }}</a>, 
{% endfor %}
...

В моем тесте это сокращает запросывсего 7.

...