Wie erstelle ich mit Django Rest Framework mehrere Modellinstanzen?

78

Ich möchte mehrere Instanzen mit dem Django Rest Framework mit einem API-Aufruf speichern und aktualisieren. Angenommen, ich habe ein "Klassenzimmer" -Modell, das mehrere "Lehrer" haben kann. Wenn ich mehrere Lehrer erstellen und später alle Klassenzahlen aktualisieren möchte, wie würde ich das tun? Muss ich für jeden Lehrer einen API-Aufruf durchführen?

Ich weiß, dass wir derzeit keine verschachtelten Modelle speichern können, aber ich würde gerne wissen, ob wir sie auf Lehrerebene speichern können. Vielen Dank!

Chaz
quelle
Hier ist eine ähnliche Frage mit einer Lösung, die für mich funktioniert hat: stackoverflow.com/questions/21439672/…
Marcin Rapacz

Antworten:

80

Ich weiß, dass dies vor einiger Zeit gefragt wurde, aber ich habe es gefunden, als ich versucht habe, dies selbst herauszufinden.

Wenn Sie many=Truebeim Instanziieren der Serializer-Klasse für ein Modell übergeben, können mehrere Objekte akzeptiert werden.

Dies wird hier in den Django Rest Framework-Dokumenten erwähnt

Für meinen Fall sah meine Ansicht folgendermaßen aus:

class ThingViewSet(viewsets.ModelViewSet):
    """This view provides list, detail, create, retrieve, update
    and destroy actions for Things."""
    model = Thing
    serializer_class = ThingSerializer

Ich wollte nicht wirklich eine Ladung Boilerplate schreiben, um die Instanziierung des Serializers direkt zu steuern und zu übergeben many=True, also überschreibe ich in meiner Serializer-Klasse __init__stattdessen Folgendes :

class ThingSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        many = kwargs.pop('many', True)
        super(ThingSerializer, self).__init__(many=many, *args, **kwargs)

    class Meta:
        model = Thing
        fields = ('loads', 'of', 'fields', )

Senden von Daten an die Listen-URL für diese Ansicht im folgenden Format:

[
    {'loads':'foo','of':'bar','fields':'buzz'},
    {'loads':'fizz','of':'bazz','fields':'errrrm'}
]

Erstellt zwei Ressourcen mit diesen Details. Welches war schön.

Tom Manterfield
quelle
Ha, das ist ein guter Fang. Ich habe meinen Code aktualisiert, um tatsächlich etwas mit dem jetzt standardmäßig vielen Wert zu tun. Es war ein Tippfehler meinerseits. Es stellt sich heraus, dass nur das Senden der Daten in dem gezeigten Format die Aufgabe über die veraltete Methode erledigt. Achtung, Änderungen sind nicht getestet.
Tom Manterfield
1
Wie sieht request.DATA in diesem Fall aus? Es kann kein Wörterbuch sein - oder stecken sie es irgendwie in das Diktat?
Akaphenom
@akaphenom Ich weiß nicht, ob Sie Ihre Antwort gefunden haben, aber es scheint, dass request.DATA entweder eine Liste mit einem Diktat oder ein Diktat mit einer Liste mit einem Diktat sein kann, je nachdem, wie Sie es serialisieren. Zumindest war das meine Erfahrung.
whoisearth
Es ist gut zu wissen. Ich bin von der Django-Arbeit weggegangen, also habe ich mich nicht konzentriert. Aber ich bin froh, dass diese Antwort etwas vollständiger ist.
Akaphenom
17
funktioniert bei mir nicht {"non_field_errors": ["Ungültige Daten. Erwartet ein Wörterbuch, hat aber eine Liste." ]}
rluts
53

Ich bin zu einem ähnlichen Ergebnis gekommen wie Daniel Albarral, aber hier ist eine prägnantere Lösung:

class CreateListModelMixin(object):

    def get_serializer(self, *args, **kwargs):
        """ if an array is passed, set serializer to many """
        if isinstance(kwargs.get('data', {}), list):
            kwargs['many'] = True
        return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
Roger Collins
quelle
3
Das machte meinen Tag! Ich bestätige, dass dies gut funktioniert und sowohl Listen als auch Diktate akzeptiert.
Alekwisnia
1
Wie soll dies funktionieren, wenn request.data ein QueryDict und kein Diktat oder eine Liste ist? Aufgrund dieser Tatsache funktioniert es in Unit-Tests, aber nicht in der tatsächlichen Laufzeit (zumindest für mich).
Alexdlaird
kwargs.get ('data', {}) gibt ein QueryDict zurück und schlägt daher die Instanz fehl, so dass viele nicht auf True gesetzt werden.
Roger Collins
1
@RogerCollins Wenn eines der Listenelemente einen Validierungsfehler auslöst, schlägt die gesamte Anforderung fehl. Gibt es eine Möglichkeit, ungültige Elemente zu überspringen und die restlichen Instanzen zu erstellen?
pnhegde
@pnhegde Sie müssten diese Logik in Ihren Serializer aufnehmen. Sie hätten auch viel Arbeit, um sicherzustellen, dass Ihr Front-End Ihr Modell mit den Ergebnissen aktualisiert, da die Beziehungen nicht synchron wären.
Roger Collins
34

Hier ist eine weitere Lösung: Sie müssen Ihre Serializer- __init__Methode nicht überschreiben . Überschreiben Sie einfach die ModelViewSet- 'create'Methode Ihrer Ansicht . Hinweis many=isinstance(request.data,list). Hier, many=Truewenn Sie ein Array von Objekten zum Erstellen Falsesenden und wenn Sie nur das eine senden. Auf diese Weise können Sie sowohl einen Artikel als auch eine Liste speichern!

from rest_framework import status, viewsets
from rest_framework.response import Response

class ThingViewSet(viewsets.ModelViewSet):

"""This view snippet provides both list and item create functionality."""

    #I took the liberty to change the model to queryset
    queryset = Thing.objects.all()
    serializer_class = ThingSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data, many=isinstance(request.data,list))
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
waqmax
quelle
2
Diese Antwort scheint direkter zu sein und wie ich erwarten würde, dass diese Funktionalität implementiert wird.
Pitt
1
Dieser funktioniert und die am besten gewählte Antwort funktioniert bei mir nicht.
Darcyq
Sie können zusätzlich einen transaction.atomic()Block hinzufügen , um sicherzustellen, dass alle Elemente hinzugefügt werden
Felipe Buccioni
Dies verdient mehr Abstimmung, da dies das einzige war, das für mich funktioniert hat und es auch ziemlich einfach ist.
Steven
Ich bin auf den Expected a dictionary, but got list.Fehler : in der akzeptierten Antwort gestoßen und dieser hat ihn für mich behoben. Vielen Dank.
Lewis Menelaws
13

Ich konnte nicht genau herausfinden, ob die Anfrage.DATA von einem Wörterbuch in ein Array konvertiert werden sollte - was meine Fähigkeit, mit Tom Manterfields Lösung zu arbeiten, einschränkte. Hier ist meine Lösung:

class ThingSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        many = kwargs.pop('many', True)
        super(ThingSerializer, self).__init__(many=many, *args, **kwargs)

    class Meta:
        model = Thing
        fields = ('loads', 'of', 'fields', )

class ThingViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet ):
    queryset = myModels\
        .Thing\
        .objects\
        .all()
    serializer_class = ThingSerializer

    def create(self, request, *args, **kwargs):
        self.user = request.user
        listOfThings = request.DATA['things']

        serializer = self.get_serializer(data=listOfThings, files=request.FILES, many=True)
        if serializer.is_valid():
            serializer.save()
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED,
                            headers=headers)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Und dann führe ich das Äquivalent auf dem Client aus:

var things = {    
    "things":[
        {'loads':'foo','of':'bar','fields':'buzz'},
        {'loads':'fizz','of':'bazz','fields':'errrrm'}]
}
thingClientResource.post(things)
Akaphenom
quelle
1
+1 Danke für das Beispiel. Beachten Sie, dass ich init in meinem Serializer nicht überschreiben musste, sondern nur die Erstellungsmethode in meiner Ansichtsklasse
Fiver
Ich habe nicht daran gedacht, es ohne Init zu versuchen, ich habe am vorherigen Beispiel gearbeitet. Ich werde Ihre Änderung auf jeden Fall ausprobieren und meine Antwort bis zu diesem Experiment aktualisieren. Danke für die Warnung".
Akaphenom
3
Ich denke, der Schlüssel ist die Aufnahme many=Truein den get_serializerAnruf
Fiver
Es ist über ein Jahr her, seit ich meine Antwort geschrieben habe, und ich habe Mühe, mich daran zu erinnern, was ich zum Frühstück hatte. Nehmen Sie dies also als das, was es wert ist: Ich erinnere mich an den einzigen Grund, warum ich meine Init überschreiben musste, um die vielen Flaggen hinzuzufügen, war, weil ich Ich wollte die Serializer-Klasse aus irgendeinem Grund nicht direkt instanziieren (hoffentlich war es eine gute, aber im Moment entgeht sie mir). Also ja, das Vergehen von vielen = Wahr ist hier der Schlüssel. Der überschriebene Init kann gelöscht werden.
Tom Manterfield
Ich auch war es ein Schmerz
Akaphenom
8

Ich denke, der beste Weg, um die vorgeschlagene Architektur des Frameworks zu respektieren, wird darin bestehen, ein Mixin wie dieses zu erstellen:

class CreateListModelMixin(object):

    def create(self, request, *args, **kwargs):
        """
            Create a list of model instances if a list is provides or a
            single model instance otherwise.
        """
        data = request.data
        if isinstance(data, list):
            serializer = self.get_serializer(data=request.data, many=True)
        else:
            serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED,
                    headers=headers)

Anschließend können Sie das CreateModelMixin von ModelViewSet folgendermaßen überschreiben:

class <MyModel>ViewSet(CreateListModelMixin, viewsets.ModelViewSet):
    ...
    ...

Jetzt können Sie im Client folgendermaßen arbeiten:

var things = [    
    {'loads':'foo','of':'bar','fields':'buzz'},
    {'loads':'fizz','of':'bazz','fields':'errrrm'}
]
thingClientResource.post(things)

oder

var thing = {
    'loads':'foo','of':'bar','fields':'buzz'
}

thingClientResource.post(thing)

BEARBEITEN:

Wie Roger Collins in ihrer Antwort vorschlägt, ist es klüger, die Methode get_serializer zu überschreiben als die Methode 'create'.

Daniel Albarral
quelle
1
kam zu einem ähnlichen Ergebnis wie Daniel Albarral, aber hier ist eine prägnantere Lösung: Klasse CreateListModelMixin (Objekt): def get_serializer (self, * args, ** kwargs): "" Wenn ein Array übergeben wird, setzen Sie den Serializer auf viele "" "if isinstance (kwargs.get ('data', {}), list): kwargs ['many'] = True return super (CreateListModelMixin, self) .get_serializer (* args, ** kwargs)
Daniel Albarral
Habe das Gefühl, Roger ist ein Typ
Josh
8

Sie können einfach die überschreiben get_serializerMethode in Ihrer APIView und passieren many=Truein get_serializeretwa so die Basisansicht:

class SomeAPIView(CreateAPIView):
    queryset = SomeModel.objects.all()
    serializer_class = SomeSerializer

    def get_serializer(self, instance=None, data=None, many=False, partial=False):
        return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)
TehQuila
quelle
1
Wenn Sie diese Methode implementieren, erhalten Sie möglicherweise "AssertionError". Wenn einem Serializer ein dataSchlüsselwortargument übergeben wird, müssen Sie ihn aufrufen, .is_valid()bevor Sie versuchen, auf die serialisierte .dataDarstellung zuzugreifen . Sie sollten entweder .is_valid()zuerst anrufen oder .initial_datastattdessen zugreifen .
Philip Mutua
Versuchen Sie das nächste:from rest_framework.fields import empty def get_serializer(self, instance=None, data=empty, many=False, partial=False): return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)
Guillermo Hernandez
3

Die Generic Views Seite in Django REST - Framework in der Dokumentation besagt , dass die ListCreateAPIView generische Ansicht " " "für Lese- / Schreibendpunkte verwendet wird, um eine Sammlung von Modellinstanzen darzustellen".

Dort würde ich anfangen zu suchen (und ich werde es tatsächlich tun, da wir diese Funktionalität bald auch in unserem Projekt benötigen werden).

Beachten Sie auch, dass die Beispiele auf der Seite "Allgemeine Ansichten" zufällig verwendet werden ListCreateAPIView.

Akaihola
quelle
Das habe ich gesehen; Im Tutorial gab es jedoch keine Beispiele, die zeigten, wie mehrere Elemente erstellt / aktualisiert werden können. Fragen wie Verschachteln Sie die Ressourcen in einem JSON-Objekt, sollte es flach sein, was passiert, wenn nur eine Teilmenge von Elementen nicht validiert wird usw. werden nicht dokumentiert. Im Moment habe ich eine etwas unelegante Problemumgehung durchgeführt, bei der ich ein Lehrer-JSON-Objekt durchlaufe und einen Lehrer-Serializer zum Validieren und Speichern verwende. Bitte lassen Sie mich wissen, wenn Sie eine bessere Lösung finden. Danke
Chaz
Ja, es sieht so aus, als wären es individuelle Funktionen zum Erstellen und zur Liste. Ich glaube nicht, dass die Lösung zum Aktualisieren / Erstellen mehrerer Datensätze darin besteht.
Akaphenom
3 von 4 Links sind jetzt defekt
e4c5
3

Ich habe mir ein einfaches Beispiel ausgedacht post

Serializers.py

from rest_framework import serializers
from movie.models import Movie

class MovieSerializer(serializers.ModelSerializer):

    class Meta:
        model = Movie
        fields = [
            'popularity',
            'director',
            'genre',
            'imdb_score',
            'name',
        ]  

Views.py

from rest_framework.response import Response
from rest_framework import generics
from .serializers import MovieSerializer
from movie.models import Movie
from rest_framework import status
from rest_framework.permissions import IsAuthenticated

class MovieList(generics.ListCreateAPIView):
    queryset = Movie.objects.all().order_by('-id')[:10]
    serializer_class = MovieSerializer
    permission_classes = (IsAuthenticated,)

    def list(self, request):
        queryset = self.get_queryset()
        serializer = MovieSerializer(queryset, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        data = request.data
        if isinstance(data, list):  # <- is the main logic
            serializer = self.get_serializer(data=request.data, many=True)
        else:
            serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Diese Zeile ist die eigentliche Logik von Multiple Instance -

data = request.data
if isinstance(data, list):  # <- is the main logic
      serializer = self.get_serializer(data=request.data, many=True)
else:
      serializer = self.get_serializer(data=request.data)

Wenn Sie mit many = True verwechselt werden, sehen Sie dies

Wenn wir Daten senden, werden diese in listetwa so sein -

[
    {
        "popularity": 84.0,
        "director": "Stanley Kubrick",
        "genre": [
            1,
            6,
            10
        ],
        "imdb_score": 8.4,
        "name": "2001 : A Space Odyssey"
    },
    {
        "popularity": 84.0,
        "director": "Stanley Kubrick",
        "genre": [
            1,
            6,
            10
        ],
        "imdb_score": 8.4,
        "name": "2001 : A Space Odyssey"
    }
]
Huzaif Sayyed
quelle
1

Die einfachste Methode, die mir begegnet ist:

    def post(self, request, *args, **kwargs):
        serializer = ThatSerializer(data=request.data, many=isinstance(request.data, list))
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
popen
quelle