Home > front end >  Django - Too many similar queries
Django - Too many similar queries

Time:02-25

I'm creating a music rating app and I'm making a serializer for albums which has many relations and one aggregation method serializer which I think is causing all the trouble. The method gets average and count of reviews for every album. Is there any way I can decrease the number of queries for more performance?

Django Debug Toolbar View

All my models

class Artist(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    image = models.FileField(null=True, blank=True,
                             upload_to=rename_artist_image)
    background_image = models.FileField(
        null=True, blank=True, upload_to=rename_artist_bg_image)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self) -> str:
        return self.name


class Album(models.Model):

    RELEASE_TYPE_ALBUM_CHOICES = [
        ("LP", "LP"),
        ("EP", "EP"),
        ("Single", "Single"),
        ("Live", "Live"),
    ]

    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    release_date = models.DateField(null=True)
    artist_id = models.ForeignKey(
        Artist, on_delete=models.PROTECT, related_name="albums"
    )
    art_cover = models.FileField(
        null=True, blank=True, upload_to=rename_album_art_cover)
    release_type = models.CharField(max_length=10,
                                    choices=RELEASE_TYPE_ALBUM_CHOICES, default="LP")
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self) -> str:
        return self.title


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

    def __str__(self) -> str:
        return self.name


class AlbumGenre(models.Model):
    album_id = models.ForeignKey(
        Album, on_delete=models.PROTECT, related_name="album_genres")
    genre_id = models.ForeignKey(
        Genre,  on_delete=models.PROTECT, related_name="album_genres")

    def __str__(self) -> str:
        return self.genre_id.name


class AlbumLink(models.Model):
    SERVICE_NAME_CHOICES = [
        ("spotify", "Spotify"),
        ("tidal", "Tidal"),
        ("amazonMusic", "Amazon Music"),
        ("appleMusic", "Apple Music"),
    ]

    service_name = models.CharField(
        max_length=15, choices=SERVICE_NAME_CHOICES)
    url = models.CharField(max_length=255)
    album_id = models.ForeignKey(
        Album, on_delete=models.PROTECT, related_name="album_links")

    def __str__(self) -> str:
        return f"{self.service_name} - {self.url}"


class Track(models.Model):
    title = models.CharField(max_length=255)
    position = models.PositiveIntegerField()
    album_id = models.ForeignKey(
        Album, on_delete=models.PROTECT, related_name="tracks")
    duration = models.DurationField(null=True)

    def __str__(self) -> str:
        return f"{self.position}. {self.title} - {self.duration}"


class AlbumOfTheYear(models.Model):
    album_id = models.OneToOneField(
        Album, on_delete=models.PROTECT, related_name="aoty")
    position = models.IntegerField()

    def __str__(self) -> str:
        return str(self.position)


class Review(models.Model):
    reviewer_id = models.ForeignKey(Reviewer, on_delete=models.PROTECT)
    rating = models.IntegerField(
        validators=[MaxValueValidator(100), MinValueValidator(0)]
    )
    review_text = models.TextField(null=True)
    album_id = models.ForeignKey(
        Album, on_delete=models.PROTECT, related_name="reviews")
    created_at = models.DateTimeField(auto_now_add=True)

That's how album serializer looks.

"id": 2,
"title": "OK Computer",
"slug": "ok-computer",
"created_at": "2022-02-22T21:51:52.528148Z",
"artist": {
    "id": 13,
    "name": "Radiohead",
    "slug": "radiohead",
    "image": "http://127.0.0.1:8000/media/artist/images/radiohead.jpg",
    "background_image": "http://127.0.0.1:8000/media/artist/bg_images/radiohead.jpg",
    "created_at": "2022-02-22T00:00:00Z"
},
"art_cover": "http://127.0.0.1:8000/media/album/art_covers/ok-computer_cd5Vv6U.jpg",
"genres": [
    "Alternative Rock",
    "Art Rock"
],
"reviews": {
    "overall_score": null,
    "number_of_ratings": 0
},
"release_date": "1997-05-28",
"release_type": "LP",
"tracks": [
    {
        "position": 1,
        "title": "Airbag",
        "duration": "00:04:47"
    },
    {
        "position": 2,
        "title": "Paranoid Android",
        "duration": "00:06:27"
    }
],
"links": [
    {
        "service_name": "spotify",
        "url": "https://open.spotify.com/album/6dVIqQ8qmQ5GBnJ9shOYGE?si=L_VNH3HeSMmGBqfiqKiGWA"
    }
],
"aoty": null

Album serializer with method that gets average and count of reviews of album

class AlbumSerializer(serializers.ModelSerializer):
    tracks = TrackSerializer(many=True, read_only=True)
    genres = StringRelatedField(
        source="album_genres", many=True, read_only=True)
    aoty = StringRelatedField(read_only=True)
    links = AlbumLinkSerializer(
        source="album_links", many=True, read_only=True)
    artist = SimpleArtistSerializer(source="artist_id")

    def get_avg_and_count_of_reviews(self, album: Album):
        reviews = Review.objects.only("rating").filter(album_id=album.id).aggregate(
            overall_score=Avg(F("rating"), output_field=IntegerField()), number_of_ratings=Count(F("rating"), output_field=IntegerField()))
        return reviews

    reviews = serializers.SerializerMethodField(
        method_name="get_avg_and_count_of_reviews")

    class Meta:
        model = Album
        fields = ["id",
                  "title",
                  "slug",
                  "created_at",
                  "artist",
                  "art_cover",
                  "genres",
                  "reviews",
                  "release_date",
                  "release_type",
                  "tracks",
                  "links",
                  "aoty"]

    # Save slug
    def create(self, validated_data):
        slug = slugify(validated_data["title"])
        return Album.objects.create(slug=slug, **validated_data)

Here is a queryset in album Viewset

class AlbumViewSet(ModelViewSet):
    queryset = Album.objects.prefetch_related("tracks").prefetch_related("album_genres").prefetch_related(
     "album_links").prefetch_related("reviews").select_related("aoty").select_related("artist_id").all()

CodePudding user response:

First you need to change your aggregate that you call once for every Album to an annotation, this will remove all of those extra aggregation queries

class AlbumViewSet(ModelViewSet):
    queryset = Album.objects.prefetch_related(
        "tracks",
        "album_genres",
        "album_links",
        "reviews"
    ).select_related(
        "aoty",
        "artist_id"
    ).annotate(
        overall_score=Avg(F("reviews__rating"), output_field=IntegerField()),
        number_of_ratings=Count(F("reviews__rating"), output_field=IntegerField())
    )

Now you can replace your reviews field with two regular IntegerFields

class AlbumSerializer(serializers.ModelSerializer):
    ...
    overall_score = serializers.IntegerField(source="overall_score")
    number_of_ratings = serializers.IntegerField(source="number_of_ratings")

  • Related