Djangoテストコードセグメントでn+1個の問題を検索


ORMを使用するフレームワークでもN+1の問題が発生します.この記事では,テストコードでN+1問題を予測し修正する方法を共有する.
各フレームワークでn+1の問題を検出するパッケージは、優れた開発者によって共有されています.張高はhttps://github.com/jmcarp/nplusoneセットあり、利用しようとしています.
まずパッケージをインストールします.インストール方法はパッケージのreadmeを参照してください.
次の例示的なコードは、リソースに基づいて簡単なrest apiを記述するサンプルコードである. 
2つのモデルがあれば、Article▼Comment関係は1対多です.

Model

from django.db import models

# Create your models here.
class Article(models.Model):
    # user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=144)
    subtitle = models.CharField(max_length=144, blank=True)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return '[{}] {}'.format(self.title, self.subtitle)


class Comment(models.Model):
    article = models.ForeignKey(to=Article, related_name="comments", on_delete=models.CASCADE)
    content = models.TextField()
一対の多関係モデル.Article - Comment

View

from django.shortcuts import render

# Create your views here.
from requests import Response
from rest_framework import viewsets
from .serializers import ArticleSerializer, CommentSerializer
from .models import Article, Comment
from rest_framework import permissions

class ArticleView(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = ()

    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    def get_serializer(self, *args, **kwargs):
        return super().get_serializer(*args, **kwargs)

    def get_serializer_context(self):
        context = super(ArticleView, self).get_serializer_context()

        return context


class CommentView(viewsets.ModelViewSet):

    serializer_class = CommentSerializer
    permission_classes = ()

    def get_queryset(self):
        return Comment.objects.filter(article=self.kwargs['article_pk'])

    def perform_create(self, serializer):
        serializer.save()
リソースをrest形式で返します.

Serializer

from rest_framework import serializers
from .models import Article, Comment
from django.contrib.auth.models import User


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = (
            'id',
            'article',
            'content'
        )


class ArticleSerializer(serializers.ModelSerializer):
    comments = CommentSerializer(many=True, read_only=True)
    class Meta:
        model = Article
        fields = (
            'id',
            'title',
            'subtitle',
            'content',
            'created_at',
            'comments',
        )
        read_only_fields = ('created_at',)
ArticleSerializerからサブアノテーションモデルをインポート(重要、プリフェッチなし)

Urls

from django.urls import path, include
from rest_framework_nested import routers

from .views import ArticleView, CommentView

router = routers.SimpleRouter()
router.register(r'articles', ArticleView, basename='articles')

articles_router = routers.NestedSimpleRouter(router, r'articles', lookup='article')
articles_router.register(r'comments', CommentView, basename='article-comments')
urlpatterns = [

    path('', include(router.urls)),
    path('', include(articles_router.urls))
]

TestCode

from unittest import mock

import pytest
from django.conf import settings
from django.test import Client

from api.models import Article, Comment


@pytest.fixture
def logger(monkeypatch):
    mock_logger = mock.Mock()
    monkeypatch.setattr(settings, 'NPLUSONE_LOGGER', mock_logger)
    return mock_logger

def check_nplusone_problem(logger):
    if len(logger.log.call_args_list) != 0:  # 에러 문자가 포함되어 있다.
        args = logger.log.call_args[0]
        assert ("Potential n+1 query detected on" in args[1]) is False  # prefetch 가 필요한 경우
        assert ("Potential unnecessary eager load detected on" in args[1]) is False  # 쓸데없이 prefetch를 한 경우

    assert not logger.log.called  # 정상적인 경우 호출 하면 안 된다.



def create_mock_article() -> Article:
    """
    Article 목 데이터를 생성한다.
    :return:
    """
    # Given
    name: str = "TEST_title"
    description: str = "description"

    # When
    article: Article = Article()
    article.title = name
    article.subtitle = 'subtitle'
    article.content = description
    article.save()

    comment: Comment = Comment()
    comment.content = "AAA"
    comment.article = article
    comment.save()

    return article


@pytest.mark.django_db
def test_route_article_GET(logger) -> None:
    """
    AlbumLV 뷰의 url method GET 테스트 할 수 있다.
    :return:
    """

    # Given
    create_mock_article()
    create_mock_article()
    create_mock_article()

    client = Client()


    # When
    response = client.get('/api/articles/')


    # Then
    print(response.data)
    check_nplusone_problem(logger)
    assert response.status_code == 200

テストコードの実行に失敗しました



->失敗の原因は、ArticleSerializer()にサブモデルCommentがプリフェッチされていないためです.

失敗事例の修正


既存のArticleViewのクエリーセットにアノテーションモデルを追加します.
class ArticleView(viewsets.ModelViewSet):
    queryset = Article.objects.prefetch('comments').all()
    

テストコード実行結果成功~


=================================================================================== 1 passed in 7.85s ===================================================================================