使用具有外键的模型的序列化程序在 Django Rest Framework 中对父 table 执行 CRUD

Use serializer of model having foreign key to do CRUD on parent table in Django Rest Framework

在我的API中,我有两个模型QuestionOption如下图

class Question(models.Model):
    body = models.TextField()


class Options(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    option = models.CharField(max_length=100)
    is_correct = models.SmallIntegerField()

在创建问题的同时创建选项会更好。并且不应创建已经存在的问题,但如果选项与以前的选项不同,则可以更改选项。
我正在使用 ModelSerializerModelViewSet。我为 QuestionOption 使用不同的 url 和视图。

serializers.py

class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = '__all__'


class OptionReadSerializer(serializers.ModelSerializer):
    question = QuestionSerializer(read_only=True)

    class Meta:
        model = Option
        fields = ('question', 'option', 'is_correct')


class OptionWriteSerializer(serializer.ModelSerializer):
    class Meta:
        model = Option
        fields = ('question', 'option', 'is_correct')

views.py

class QuestionViewSet(ModelViewSet):
    seriaizer_class = QuestionSerializer
    queryset = Question.objects.all()


class OptionViewSet(ModelViewSet):
    queryset = Option.objects.all()

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return OptionWriteSerializer
        return OptionReadSerializer

urls.py

from django.urls import include
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('api/question', QuestionViewset, base_name='question')
router.register('api/option', OptionViewSet, base_name='option')

urlpatterns = [
    path('', include(router.urls))
]

这样一来,我总是必须先创建问题,然后才能为该问题单独添加选项。我认为这可能不是一个实用的方法。
如果问题和选项可以同时添加并且类似于所有 CRUD 操作,那就更好了。

JSON格式的预期结果和发布数据如下所示:

{
    "body": "Which country won the FIFA world cup 2018",
    "options": [
        {
            "option": "England",
            "is_correct": 0
        },
        {
            "option": "Germany",
            "is_correct": 0
        },
        {
            "option": "France",
            "is_correct": 1
        }
    ]
}

我们可以使用PrimaryKeyRelatedField.

tldr;

我相信一个 Question 可以附加多个 Options。而不是将 Option 挂接到 Question.

像这样:

class Question(models.Model):
    body = models.TextField()
    options = models.ManyToManyField(Option)

class Options(models.Model):
    text = models.CharField(max_length=100)
    is_correct = models.BooleanField()

然后我们可以使用PrimaryKeyRelatedField这样的东西:

class QuestionSerializer(serializers.ModelSerializer):
    options = serializers.PrimaryKeyRelatedField(queryset=Options.objects.all(), many=True, read_only=False)
    class Meta:
        model = Question
        fields = '__all__'

参考:https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield

models 中,我在 Option 模型的外键字段中添加了 related_name='options'

models.py

class Question(models.Model):
    body = models.TextField()


class Options(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options')
    option = models.CharField(max_length=100)
    is_correct = models.SmallIntegerField()

QuestionWriteSerializer 中,我覆盖了 update()create() 方法。创建和更新逻辑是从 QuestionWriteSerialzer 处理的。

serializers.py

class OptionSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField(required=False)

    class Meta:
        model = Option
        fields = ('id', 'question', 'option', 'is_correct')


class QuestionReadSerializer(serializers.ModelSerializer):
    options = OptionSerializer(many=True, read_only=True)

    class Meta:
        model = Question
        fields = ('id', 'body', 'options')


class QuestionWriteSerializers(serializers.ModelSerializer):
    options = OptionSerializer(many=True)

    class Meta:
        model = Question
        fields = ('id', 'body', 'options')

    def create(self, validated_data):
        options_data = validated_data.pop('options')
        question_created = Questions.objects.update_or_create(**validated_data)

        option_query = Options.objects.filter(question=question_created[0])
        if len(option_query) > 1:
            for existeding_option in option_query:
                option_query.delete()

        for option_data in options_data:
            Options.objects.create(question=question_created[0], **option_data)

        return question_created[0]

    def update(self, instance, validated_data):
        options = validated_data.pop('options')
        instance.body = validated_data.get('body', instance.body)
        instance.save()

        keep_options = []
        for option_data in options:
            if 'id' in option_data.keys():
                if Options.objects.filter(id=option_data['id'], question_id=instance.id).exists():
                    o = Options.objects.get(id=option_data['id'])
                    o.option = option_data.get('option', o.option)
                    o.is_correct = option_data.get('is_correct', o.is_correct)
                    o.save()
                    keep_options.append(o.id)
                else:
                    continue
            else:
                o = Options.objects.create(**option_data, question=instance)
                keep_options.append(o.id)

        for option_data in instance.options.all():
            if option_data.id not in keep_options:
                Options.objects.filter(id=option_data.id).delete()

        return instance

QuestionViewSet几乎一样,我去掉了OptionViewSet,控制了QuestionViewSet

的所有东西

views.py

class QuestionViewSet(ModelViewSet):
    queryset = Question.objects.all()

    def get_serializer_class(self) or self.request.method == 'PUT' or self.request.method == 'PATCH':
        if self.request.method == 'POST':
            return QuestionWriteSerializer
        return QuestionReadSerializer

    def create(self, request, *args, **kwargs):
        """
        Overriding create() method to change response format
        """
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            self.perform_create(serializer)
            headers = self.get_success_headers(serializer.data)
            return Response({
                'message': 'Successfully created question',
                'data': serializer.data,
                'status': 'HTTP_201_CREATED',
            }, status=status.HTTP_201_CREATED, headers=headers)
        else:
            return Response({
                'message': 'Can not create',
                'data': serializer.errors,
                'status': 'HT',
            }, status=status.HTTP_400_BAD_REQUEST)