Understanding django drf's query or create

65 阅读2分钟

What we want to do?

I have a feature to record user's reading history. So the relation about User, Book, ReadingHistory is :

class ReadingHistory(TimeStampedModel, models.Model):
    epubCFI = models.CharField(max_length=100, null=True, blank=True)
    readingTime = models.IntegerField(default=0)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)

    def __str__(self):
        return self.epubCFI

image.png

But in fact user will need to fetch readingHistory which his current reading book which means we need filter target reading history using bookID.

Our ReadingHistoryViewSet doesn't support this feature now.

class ReadingHistoryViewSet(viewsets.ModelViewSet):
    queryset = ReadingHistory.objects.all()
    serializer_class = ReadingHistorySerializer
    permission_classes = [permissions.IsAuthenticated]

We want to pass some filter condition like /reading_histories/?book_id=1.So we need to extract args from query params.

class ReadingHistoryViewSet(viewsets.ModelViewSet):
    serializer_class = ReadingHistorySerializer

    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        """
        This view should return a list of all the purchases for
        the book as determined by the book_id portion of the URL.
        """
        book_id = self.kwargs['book_id']
        return ReadingHistory.objects.filter(book_id=book_id)

But a error raised:

image.png

Yes. Our router is very easy and never specify the basename. Why it not raise a error before?

router = routers.DefaultRouter()
router.register('groups', GroupViewSet)
router.register('users', UserViewSet)
router.register('books', BookViewSet)
router.register('reading_histories', ReadingHistoryViewSet)

Official document tell us the answer:

  • basename - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the queryset attribute of the viewset, if it has one. Note that if the viewset does not include a queryset attribute then you must set basename when registering the viewset.

We can understand this from sourcecode:

image.png

image.png

So we change our routers and viewset:

router.register('reading_histories', ReadingHistoryViewSet, basename='reading_histories')

class ReadingHistoryViewSet(viewsets.ModelViewSet):
    serializer_class = ReadingHistorySerializer

    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        """
        This view should return a list of all the purchases for
        the book as determined by the book_id portion of the URL.
        """
        book_id = self.kwargs.get('book_id')
        return ReadingHistory.objects.filter(book_id=book_id, user=self.request.user)

Be careful None should be exclude from filter. THis is what we will meet:

image.png

It have other question.We know For each book, each user will have a maximum of ONE reading history. But know it will return a pagination result.

And We want if can not find this record, then create it and return.

Both feature are not convenient and not comfortable when we follow the RESTFUL api design. So we create another endpoint to do this.


# looks useless for now
router.register('reading_histories', ReadingHistoryViewSet) 

path('query_or_create_reading_histories/', handle_query_or_create_reading_history),

class ReadingHistoryViewSet(viewsets.ModelViewSet):
    serializer_class = ReadingHistorySerializer
    queryset = ReadingHistory.objects.all()
    permission_classes = [permissions.IsAuthenticated]


@api_view(['POST'])
def handle_query_or_create_reading_history(request):
    book_id = request.data.get('book_id')
    user = request.user
    if book_id is None or user is None:
        return Response(status=status.HTTP_400_BAD_REQUEST)
    else:
        try:
            record = ReadingHistory.objects.get(book_id=book_id, user=user)
            serializer = ReadingHistorySerializer(record)
            return Response(serializer.data)
        except ReadingHistory.DoesNotExist:
            data = {
                "book": book_id, "user": user.id
            }
            serializer = ReadingHistorySerializer(data=data)
            if serializer.is_valid(raise_exception=True):
                serializer.save()
                return Response(serializer.data)

What we have learn

  • REASTFUL not a sliver bullet.
  • Choose what we need , not copy and adapt rigidly

Test Case

class ReadingHistoryTestCase(TransactionTestCase):
    databases = {'default'}

    def setUp(self):
        from django.core.management import call_command
        call_command('migrate', verbosity=0, interactive=False, database='default')

        # make sure we have a default user.
        self.defaultUserName = 'bookCreator'
        self.defaultPassword = "defaultUserPassword"
        self.defaultUser = User.objects.create_user(username=self.defaultUserName, password=self.defaultPassword)

        self.defaultBookTitle = 'book_1'
        self.defaultBook = Book.objects.create(title="book_1", owner_id=self.defaultUser.id)

    def test_create_a_reading_history_if_not_exist(self):
        # At first, it isn't exist
        count = ReadingHistory.objects.filter(user=self.defaultUser, book=self.defaultBook.id).count()
        self.assertEqual(count, 0)

        token, created = Token.objects.get_or_create(user=self.defaultUser)
        client = APIClient()
        client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

        response = client.post('/query_or_create_reading_history/', {'book_id': self.defaultBook.id})
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # After created, it should exist.
        count = ReadingHistory.objects.filter(user=self.defaultUser, book=self.defaultBook.id).count()
        self.assertEqual(count, 1)

        response = client.post("/query_or_create_reading_history/", {'book_id': self.defaultBook.id})
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # It should not create second record.
        count = ReadingHistory.objects.filter(user=self.defaultUser, book=self.defaultBook.id).count()
        self.assertEqual(count, 1)

Thanks for reading!