เฟรมเวิร์กส่วนที่เหลือ Django ใช้ serializers ที่แตกต่างกันใน ModelViewSet เดียวกัน


197

ฉันต้องการมอบ serializers ที่แตกต่างกันสองแบบและยังสามารถได้รับประโยชน์จากสิ่งอำนวยความสะดวกทั้งหมดของModelViewSet:

  • เมื่อดูรายการวัตถุฉันต้องการให้แต่ละวัตถุมี URL ที่เปลี่ยนเส้นทางไปยังรายละเอียดและความสัมพันธ์อื่น ๆ จะปรากฏขึ้นโดยใช้__unicode __โมเดลเป้าหมาย

ตัวอย่าง:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • เมื่อดูรายละเอียดของวัตถุฉันต้องการใช้ค่าเริ่มต้น HyperlinkedModelSerializer

ตัวอย่าง:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "http://127.0.0.1:8000/database/utenti/3/",
  "accesso": "CHI",
  "membri": [
    "http://127.0.0.1:8000/database/utenti/3/",
    "http://127.0.0.1:8000/database/utenti/4/",
    "http://127.0.0.1:8000/database/utenti/5/",
    "http://127.0.0.1:8000/database/utenti/6/",
    "http://127.0.0.1:8000/database/utenti/7/"
  ]
}

ฉันจัดการเพื่อให้งานนี้ทั้งหมดตามที่ฉันต้องการด้วยวิธีต่อไปนี้:

serializers.py

# serializer to use when showing a list
class ListaGruppi(serializers.HyperlinkedModelSerializer):
    membri = serializers.RelatedField(many = True)
    creatore = serializers.RelatedField(many = False)

    class Meta:
        model = models.Gruppi

# serializer to use when showing the details
class DettaglioGruppi(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Gruppi

views.py

class DualSerializerViewSet(viewsets.ModelViewSet):
    """
    ViewSet providing different serializers for list and detail views.

    Use list_serializer and detail_serializer to provide them
    """
    def list(self, *args, **kwargs):
        self.serializer_class = self.list_serializer
        return viewsets.ModelViewSet.list(self, *args, **kwargs)

    def retrieve(self, *args, **kwargs):
        self.serializer_class = self.detail_serializer
        return viewsets.ModelViewSet.retrieve(self, *args, **kwargs)

class GruppiViewSet(DualSerializerViewSet):
    model = models.Gruppi
    list_serializer = serializers.ListaGruppi
    detail_serializer = serializers.DettaglioGruppi

    # etc.

โดยทั่วไปฉันตรวจพบเมื่อผู้ใช้ขอดูรายการหรือดูรายละเอียดและเปลี่ยนserializer_classให้เหมาะสมกับความต้องการของฉัน ฉันไม่พอใจกับรหัสนี้จริง ๆ ดูเหมือนว่าแฮ็คสกปรกและที่สำคัญที่สุดคือถ้าผู้ใช้สองคนขอรายการและรายละเอียดในเวลาเดียวกัน

มีวิธีที่ดีกว่าเพื่อให้บรรลุการใช้นี้ModelViewSetsหรือฉันต้องถอยกลับใช้GenericAPIView?

แก้ไข:
นี่คือวิธีการใช้ฐานที่กำหนดเองModelViewSet:

class MultiSerializerViewSet(viewsets.ModelViewSet):
    serializers = { 
        'default': None,
    }

    def get_serializer_class(self):
            return self.serializers.get(self.action,
                        self.serializers['default'])

class GruppiViewSet(MultiSerializerViewSet):
    model = models.Gruppi

    serializers = {
        'list':    serializers.ListaGruppi,
        'detail':  serializers.DettaglioGruppi,
        # etc.
    }

คุณใช้มันอย่างไรดี ใช้วิธีที่เสนอโดยผู้ใช้ 2734679 หรือใช้ GenericAPIView หรือไม่
andilabs

ตามที่แนะนำโดยผู้ใช้ 2734679; ฉันสร้าง ViewSet ทั่วไปเพื่อเพิ่มพจนานุกรมเพื่อระบุ serializer สำหรับแต่ละการกระทำและ serializer เริ่มต้นเมื่อไม่ได้ระบุ
BlackBear

ฉันมีปัญหาที่คล้ายกัน ( stackoverflow.com/questions/24809737/ … ) และสำหรับตอนนี้ก็จบลงด้วย ( gist.github.com/andilab/a23a6370bd118bf5e858 ) แต่ฉันไม่พอใจกับมันมาก
andilabs

1
สร้างแพคเกจขนาดเล็กนี้สำหรับสิ่งนี้ github.com/Darwesh27/drf-custom-viewsets
Adil Malik

1
วิธีการดึงข้อมูลแทนที่นั้นเป็นปกติ
gzerone

คำตอบ:


289

แทนที่get_serializer_classวิธีการของคุณ วิธีนี้ใช้ในโมเดลมิกซ์อินของคุณเพื่อดึงคลาส Serializer ที่เหมาะสม

โปรดทราบว่านอกจากนี้ยังมีget_serializerวิธีการที่ส่งคืนอินสแตนซ์ของ Serializer ที่ถูกต้อง

class DualSerializerViewSet(viewsets.ModelViewSet):
    def get_serializer_class(self):
        if self.action == 'list':
            return serializers.ListaGruppi
        if self.action == 'retrieve':
            return serializers.DettaglioGruppi
        return serializers.Default # I dont' know what you want for create/destroy/update.                

1
นี่มันเยี่ยมมากขอบคุณ! ฉันได้แทนที่ get_serializer_class แล้ว
BlackBear

15
คำเตือน: การวางท่า django พักผ่อนไม่ได้วางพารามิเตอร์ self.action ดังนั้นฟังก์ชั่นนี้จะทำให้เกิดข้อยกเว้น คุณอาจใช้คำตอบของ gonz หรือคุณอาจใช้if hasattr(self, 'action') and self.action == 'list'
Tom Leys

สร้างแพ็คเกจ pypi ขนาดเล็กสำหรับสิ่งนี้ github.com/Darwesh27/drf-custom-viewsets
Adil Malik

เราจะได้รับpkวัตถุที่ร้องขอได้อย่างไรหากการดำเนินการคือretrieveอะไร?
Pranjal Mittal

ปฏิกิริยาของฉันคือ มีคนบอกฉันทีว่าทำไม
Kakaji

86

คุณอาจพบว่ามิกซ์อินนี้มีประโยชน์แทนที่เมธอด get_serializer_class และช่วยให้คุณประกาศคำสั่งที่แม็พแอ็คชันและคลาส serializer หรือย้อนกลับไปยังพฤติกรรมปกติ

class MultiSerializerViewSetMixin(object):
    def get_serializer_class(self):
        """
        Look for serializer class in self.serializer_action_classes, which
        should be a dict mapping action name (key) to serializer class (value),
        i.e.:

        class MyViewSet(MultiSerializerViewSetMixin, ViewSet):
            serializer_class = MyDefaultSerializer
            serializer_action_classes = {
               'list': MyListSerializer,
               'my_action': MyActionSerializer,
            }

            @action
            def my_action:
                ...

        If there's no entry for that action then just fallback to the regular
        get_serializer_class lookup: self.serializer_class, DefaultSerializer.

        """
        try:
            return self.serializer_action_classes[self.action]
        except (KeyError, AttributeError):
            return super(MultiSerializerViewSetMixin, self).get_serializer_class()

สร้างแพคเกจขนาดเล็กนี้สำหรับสิ่งนี้ github.com/Darwesh27/drf-custom-viewsets
Adil Malik

15

คำตอบนี้เหมือนกับคำตอบที่ยอมรับ แต่ฉันชอบที่จะทำแบบนี้

มุมมองทั่วไป

get_serializer_class(self):

ส่งคืนคลาสที่ควรใช้สำหรับ serializer เริ่มต้นที่จะกลับserializer_classแอตทริบิวต์

อาจถูกเขียนทับเพื่อให้พฤติกรรมแบบไดนามิกเช่นการใช้ serializers ที่แตกต่างกันสำหรับการอ่านและเขียนการดำเนินงานหรือให้ serializers ที่แตกต่างกันให้กับผู้ใช้ประเภทต่างๆ แอตทริบิวต์ serializer_class

class DualSerializerViewSet(viewsets.ModelViewSet):
    # mapping serializer into the action
    serializer_classes = {
        'list': serializers.ListaGruppi,
        'retrieve': serializers.DettaglioGruppi,
        # ... other actions
    }
    default_serializer_class = DefaultSerializer # Your default serializer

    def get_serializer_class(self):
        return self.serializer_classes.get(self.action, self.default_serializer_class)

ใช้ไม่ได้เพราะมันบอกฉันว่ามุมมองของฉันไม่มีแอตทริบิวต์ "การกระทำ" ดูเหมือนว่า ProductIndex (generics.ListCreateAPIView) หมายความว่าคุณต้องผ่านการรับชมวิวเวอร์เป็นอาร์กิวเมนต์หรือมีวิธีการทำโดยใช้มุมมอง API ทั่วไป?
Seb

1
การตอบกลับล่าช้าในการแสดงความคิดเห็น @Seb คนอาจจะได้ประโยชน์จาก :) ตัวอย่างใช้ ViewSets ไม่ใช่ Views :)
fanny

ดังนั้นเมื่อรวมกับโพสต์stackoverflow.com/questions/32589087/นี้ดูเหมือนว่า ViewSets จะเป็นหนทางในการควบคุมมุมมองที่แตกต่างกันมากขึ้นและสร้าง url โดยอัตโนมัติเพื่อให้มี API ที่สอดคล้องกันหรือไม่ ตอนแรกคิดว่า generics.ListeCreateAPIView นั้นมีประสิทธิภาพมากที่สุด แต่ก็พื้นฐานเกินไปใช่ไหม?
Seb

11

เกี่ยวกับการให้ serializers ที่แตกต่างกันทำไมไม่มีใครไปหาวิธีการตรวจสอบวิธี HTTP? IMO ชัดเจนยิ่งขึ้นและไม่ต้องการการตรวจสอบพิเศษ

def get_serializer_class(self):
    if self.request.method == 'POST':
        return NewRackItemSerializer
    return RackItemSerializer

เครดิต / แหล่งที่มา: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718


12
สำหรับกรณีที่เป็นปัญหาซึ่งเกี่ยวกับการใช้ serializer listและretrieveการกระทำที่แตกต่างกันคุณมีปัญหาที่ทั้งสองใช้GETวิธีการ นี่คือสาเหตุที่เฟรมเวิร์กส่วนที่เหลือ django ViewSets ใช้แนวคิดของการดำเนินการซึ่งคล้ายกัน แต่แตกต่างจากเมธอด http ที่สอดคล้องกันเล็กน้อย
Håken Lid

8

จาก @gonz และ @ user2734679 คำตอบฉันได้สร้างแพ็คเกจหลามขนาดเล็กนี้ที่ให้ฟังก์ชั่นนี้ในรูปแบบคลาสย่อยของ ModelViewset นี่คือวิธีการทำงาน

from drf_custom_viewsets.viewsets.CustomSerializerViewSet
from myapp.serializers import DefaltSerializer, CustomSerializer1, CustomSerializer2

class MyViewSet(CustomSerializerViewSet):
    serializer_class = DefaultSerializer
    custom_serializer_classes = {
        'create':  CustomSerializer1,
        'update': CustomSerializer2,
    }

6
ควรใช้ mixin แบบไหนดีกว่ากันมาก
iamsk

1

แม้ว่าจะมีการกำหนด Serializers หลายตัวล่วงหน้าไม่ว่าจะด้วยวิธีใดก็ตามดูเหมือนว่าจะเป็นเอกสารที่ชัดเจนที่สุด FWIW มีวิธีการทางเลือกที่ดึงรหัสเอกสารอื่น ๆ และที่ช่วยให้การส่งผ่านข้อโต้แย้งไปยังอนุกรมเมื่อมันเป็นอินสแตนซ์ ฉันคิดว่ามันอาจจะคุ้มค่ากว่าถ้าคุณต้องการสร้างตรรกะตามปัจจัยต่าง ๆ เช่นระดับผู้ดูแลระบบของผู้ใช้การกระทำที่ถูกเรียกหรือแม้แต่แอตทริบิวต์ของอินสแตนซ์

ชิ้นส่วนแรกของตัวต่อคือเอกสารในการปรับเปลี่ยน serializer แบบไดนามิก ณ จุดเริ่มต้นที่จุดของการเริ่ม เอกสารนั้นไม่ได้อธิบายวิธีเรียกรหัสนี้จาก viewset หรือวิธีการแก้ไขสถานะของฟิลด์แบบอ่านอย่างเดียวหลังจากที่พวกเขาได้รับการเริ่มต้น - แต่มันไม่ยากมาก

ส่วนที่สอง - วิธี get_serializerมีการบันทึกไว้ด้วย - (เพียงเล็กน้อยลงจากหน้า get_serializer_class ภายใต้ 'วิธีอื่น') ดังนั้นจึงควรวางใจได้ (และแหล่งที่มานั้นง่ายมากซึ่งหวังว่าจะมีโอกาสน้อยที่ไม่ได้ตั้งใจ ผลข้างเคียงที่เกิดจากการดัดแปลง) ตรวจสอบแหล่งที่มาภายใต้ GenericAPIView (ModelViewSet - และคลาสอื่น ๆ ที่สร้างขึ้นในคลาสของวิวเวอร์ที่ดูเหมือน - สืบทอดมาจาก GenericAPIView ซึ่งกำหนด get_serializer

การรวมสองสิ่งเข้าด้วยกันคุณสามารถทำสิ่งนี้:

ในไฟล์ serializers (สำหรับฉัน base_serializers.py):

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""

def __init__(self, *args, **kwargs):
    # Don't pass the 'fields' arg up to the superclass
    fields = kwargs.pop('fields', None)

    # Adding this next line to the documented example
    read_only_fields = kwargs.pop('read_only_fields', None)

    # Instantiate the superclass normally
    super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

    if fields is not None:
        # Drop any fields that are not specified in the `fields` argument.
        allowed = set(fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

    # another bit we're adding to documented example, to take care of readonly fields 
    if read_only_fields is not None:
        for f in read_only_fields:
            try:
                self.fields[f].read_only = True
            exceptKeyError:
                #not in fields anyway
                pass

จากนั้นใน Viewset ของคุณคุณอาจทำสิ่งนี้:

class MyViewSet(viewsets.ModelViewSet):
    # ...permissions and all that stuff

    def get_serializer(self, *args, **kwargs):

        # the next line is taken from the source
        kwargs['context'] = self.get_serializer_context()

        # ... then whatever logic you want for this class e.g:
        if self.action == "list":
            rofs = ('field_a', 'field_b')
            fs = ('field_a', 'field_c')
        if self.action == retrieve”:
            rofs = ('field_a', 'field_c’, ‘field_d’)
            fs = ('field_a', 'field_b’)
        #  add all your further elses, elifs, drawing on info re the actions, 
        # the user, the instance, anything passed to the method to define your read only fields and fields ...
        #  and finally instantiate the specific class you want (or you could just
        # use get_serializer_class if you've defined it).  
        # Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
        kwargs['read_only_fields'] = rofs
        kwargs['fields'] = fs
        return MyDynamicSerializer(*args, **kwargs)

และควรจะเป็น! การใช้ MyViewSet ควรทำให้ MyDynamicSerializer สร้างอินสแตนซ์ของคุณด้วยอาร์กิวเมนต์ที่คุณต้องการ - และสมมติว่า serializer ของคุณสืบทอดมาจาก DynamicFieldsModelSerializer ของคุณคุณควรรู้ว่าต้องทำอย่างไร

บางทีมันควรค่าแก่การกล่าวถึงว่ามันเหมาะสมเป็นพิเศษถ้าคุณต้องการที่จะปรับ serializer ด้วยวิธีอื่น ... เช่นการทำสิ่งต่าง ๆ เช่นใช้ในรายการ read_only_exceptions และใช้มันในรายการที่อนุญาตมากกว่าช่องรายการที่ไม่ปลอดภัย ฉันยังพบว่ามีประโยชน์ในการตั้งค่าเขตข้อมูลเป็น tuple ที่ว่างเปล่าหากยังไม่ผ่านและเพียงแค่ลบการตรวจสอบไม่มี ... และฉันตั้งค่าคำจำกัดความเขตข้อมูลของฉันในการสืบทอด Serializers เป็น ' ทั้งหมด ' ซึ่งหมายความว่าไม่มีเขตข้อมูลที่ไม่ผ่านเมื่อ instantiating อยู่รอดโดยบังเอิญและฉันยังไม่ต้องเปรียบเทียบการร้องขอ serializer กับคำนิยามclassizer serializer สืบทอดเพื่อทราบว่ามีอะไรรวมอยู่ ... เช่นภายในinitของ DynamicFieldsModelSerializer:

# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....

NBถ้าฉันต้องการคลาสสองหรือสามคลาสที่แมปกับการกระทำที่แตกต่างและ / หรือฉันไม่ต้องการพฤติกรรมซีเรียลไลเซอร์แบบพิเศษใด ๆ ฉันอาจใช้วิธีการอย่างใดอย่างหนึ่งที่ผู้อื่นกล่าวถึงที่นี่ แต่ฉันคิดว่าสิ่งนี้คุ้มค่า โดยเฉพาะอย่างยิ่งเมื่อได้รับประโยชน์อื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.