0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Django REST framework tutorialやってみた

Last updated at Posted at 2021-03-15

django tutorialの続き
https://qiita.com/uturned0/items/af8646f612b8d8c941ae

Quickstart

project tree

/kc/tutorial$ django-admin startproject tutorial .

$ tree -I env
.
├── manage.py
└── tutorial
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

これは django そのもの

それから startapp すると

@local:/kc/tutorial/tutorial$ django-admin startapp quickstart
@local:/kc/tutorial/tutorial$ cd ..
@local:/kc/tutorial$ tree -I env
.
├── manage.py
└── tutorial
    ├── __init__.py
    ├── asgi.py
    ├── quickstart
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

manage.pyはroot
settings.py は tutotrialの中
models/viewsはquickstartの中

serializerとかはさくっとコピペさせるんだな。中身は後か。

Notice that we're using hyperlinked relations in this case with HyperlinkedModelSerializer. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.

なんのこっちゃ

viewはviewsetsを使う。

Rather than write multiple views we're grouping together all the common behavior into classes called ViewSets.
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.

viewが複数まとまるとviewsetsになるようだ。

url

Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.

viewsじゃなくてviewsetsを使う。似て非なるもの。

routerは DRFが持ってるurlのパターン制御ルータみたいだ。

settings.py

言われたとおりappを足して

python manage.py runserver

おお!あのDRFの画面が出た。

GET /
-----------------------------------
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "users": "http://127.0.0.1:8000/users/",
    "groups": "http://127.0.0.1:8000/groups/"
}
    path('', include(router.urls)),

ここですべてのアクセスをrouter.register=DRFにルーティングしてるようだ。
DefaultRouterはrouter.urlsを渡されると、内包しているendpointのlistを返してくれるみたい、かな。

これだと 403 - "detail": "Authentication credentials were not provided."

なるほどいつの間にかauthが必要になっている。 application/json で投げるとbasic authがかかるようだ。
これはDRFの基本仕様なのか、特殊なdjango admin userのmodelを使っているからなのか。

$ curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/users/
{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "url": "http://127.0.0.1:8000/users/1/",
            "username": "admin",
            "email": "admin@example.com",
            "groups": []
        }
    ]

tutorialだと users getしてrecordが帰ってきてるけどこっちにはデータがない様子。あ、loginしてるこいつ、

ここでadminで入ったらUIからrecord insertできる。getもできた。

    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))

これは login 画面という意味なのか? わからなくねー???

groupを作ってuserに紐付けたら不思議なrelationが入った。

GET /users/2/

{
"url": "http://127.0.0.1:8000/users/2/",
"username": "tom",
"email": "tom@example.com",
"groups": [
"http://127.0.0.1:8000/groups/1/"
]
}

ぬお、これが Hyperlinked なやつか。どういう仕組なんだ。urlを通じて groupの pk=1 に紐付いてる。ほう。

Tutorial 1: Serialization

$ tree -I env
.
├── manage.py
├── snippets
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── tutorial
    ├── __init__.py
    ├── __pycache__
    │   ├── __init__.cpython-37.pyc
    │   └── settings.cpython-37.pyc
    ├── settings.py
    ├── urls.py
    └── wsgi.py

class SnippetsConfig(AppConfig):
name = 'snippets'

なんだこいつは。

serializers

本題。

初期データを作る

snippet = Snippet(code='foo = "bar"\n')
snippet.save()

DB からデータを取る場合

データを取る

serializer = SnippetSerializer(snippet)
serializer.data

複数データの場合は many=True

serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data

Jsonにして出力

content = JSONRenderer().render(serializer.data)
content

DB にデータを入れる場合

この逆。jsonできたものをDBに入れる場合

import io

stream = io.BytesIO(content)         # このcontentにjson文字列が入っているとする
data = JSONParser().parse(stream)

serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()

なるほど is_valid は、jsonがmodelに即した内容になってるかをチェックしてるのか。つまりデシリアライズする時に使うのか。

modelに入ってたやつはvalidに決まってるからis validする必要がない。DBから取り出すときは不要。
DBに入れるときは is validする。

明日はここから Using ModelSerializers

formとModelFormのように、ってそこすっ飛ばしたから意味がわからなかった。

この2つは意味が同じらしい。


class SnippetSerializer(serializers.Serializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']

    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(required=False, allow_blank=True, max_length=100)
    code = serializers.CharField(style={'base_template': 'textarea.html'})
    linenos = serializers.BooleanField(required=False)
    language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
    style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')



class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']

serializers.Serializer と serializers.ModelSerializer が違う。

大事なとこだなぁ。tutorialやり直すか。

Writing your first Django app, part 4¶

django formを学び直す。

view.pyでtemplateを呼ぶとき、render()には results.html を書くが
redirectするときは こうして reverse() を使ってviewの関数を呼ぶようにすると、URL依存にならない。

    return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

POSTの処理でupdateするとき、こうすると race conditionを引き起こす。
二人が同時に .get() して、それが 42だとすると、ふたりとも 43 をsave()する。

    selected_choice = question.choice_set.get(pk=request.POST['choice'])
    selected_choice.votes += 1
    selected_choice.save()

これを防ぐのはF()がヒントらしい。今度見る。
https://docs.djangoproject.com/en/3.1/ref/models/expressions/#avoiding-race-conditions-using-f
どうやら F() で取ったfieldはDBの値をstaticに変数に入れるんじゃなく、dynamicな、DBへのaliasのような感じで扱えるようだ。
ただ、それでもlockしない限りどうなん?と思うけど。F()をcallするとlockされるのかな?
なんにせよ大学で race conditionの用語に慣れておいてよかった。

formを短く書く

url.py

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]




urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

int:question_id が pk に 短くなった

views.detail という file.関数 の指定が
views.DetailView.as_view() という謎に。これ謎だよなー

views.py

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})



from django.views import generic

class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

これはヤバい・・・・なんもreturnしてないぞ

The DetailView generic view expects the primary key value captured from the URL to be called "pk", so we’ve changed question_id to pk for the generic views.

これ大事。 pk じゃないと、呼び出せないんだ。

By default, the DetailView generic view uses a template called /_detail.html. In our case, it would use the template "polls/question_detail.html". The template_name attribute is

ここも大事。 デフォルトでtmeplateのfile nameがキマってる。
template_name 変数は、それを上書きできる。ただ宣言してるだけで使われているように見えないけど、大事。

Similarly, the ListView generic view uses a default template called /_list.html; we use template_name to tell ListView to use our existing "polls/index.html" template.

ここも大事。結局 ModelViewというか、generic.DetailViewとgeneric.ListViewってのはコードを短くするためのショートハンドで、決められたtemplate名、変数名で動くように鳴っている。これはdjangoのみの仕様だから、そのまま覚えるしかない。


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

context_object_name は元のモデルが Qeustionだから、デフォルトでは question_list になる。これは決まりごと。
だからそこをcomment outした場合、 template を

{% for question in question_list %}

にすれば動く。

で、context_object_nameには get_queryset() の値が入る仕組みらしい。

全部決まりごと。辛いな。

よしわかった。そういう決まりなんだ。それがdjangoでいう return render(template-file) と return不要のgeneric.DetailViewみたいな関係なんだ。

よし。DRFに戻る。

Tutorial 2: Requests and Responses

明日はここから

今更だけど、apiのpost受け取ってDB触ってjson返す、そのすべてをviews.py でやるんだな。
それ全部viewなんだな。
まあ、api serverならそうか。そうだよな。

viewをsimpleにするのがtutorial 2.

いまいち

# Create your views here.
@csrf_exempt
def snippet_list(request):
    """
    List all code snippets, or create a new snippet.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)



これがよい

@api_view(['GET', 'POST'])
def snippet_list(request):
    """
    List all code snippets, or create a new snippet.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = SnippetSerializer(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)

returnが JsonResponse から Response だけで、こいつがいい感じに勝手に解釈してくれるらしい。

POSTを受け取るところの

        data = JSONParser().parse(request)

これがなくなった。serializerに data=request.data でぶっこめばいいらしい。
is_valid() でerror処理できるので、try/exceptしなくていい。キレイに書けるな。

POST の方はこう。


@csrf_exempt
def snippet_detail(request, pk):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(snippet, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)

    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)



@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

エラーのときはわざわざJsonじゃなくて return HttpResponse(status=404) してたのが、Response()だけでよくなってる。
みんな同じで良いってことね。違いはそれくらいかな。HTTP_204_NO_CONTENT こんなのあったのか。

url suffix

.json とか .html で返せるようにする機能

そういえばさ、urls.py ってなんもしてないよね。


urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]

これ list 宣言してるだけやんそういえば。tutorial/urls.pyで


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('snippets.urls')),     <------------- ここからincludeされてる
]

なるほど tutorial/settings.py にこれがあった

ROOT_URLCONF = 'tutorial.urls'

ふむ

で、そこに format_suffix_patterns() を足すと

urlpatterns = format_suffix_patterns(urlpatterns)

拡張子とかheaderで返りを変えれる。でもxml csv xls は返ってこなかった

http http://127.0.0.1:8000/snippets.json  # JSON suffix
http http://127.0.0.1:8000/snippets.api   # Browsable API suffix
http http://127.0.0.1:8000/snippets/ Accept:application/json  # Request JSON
http http://127.0.0.1:8000/snippets/ Accept:text/html         # Request HTML

Tutorial 3: Class-based Views

viewを書き換える

get/postの処理がわかりやすくなった


@api_view(['GET', 'POST'])
def snippet_list(request, format=None):
    """
    List all code snippets, or create a new snippet.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = SnippetSerializer(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)





class SnippetList(APIView):
    """
    List all snippets, or create a new snippet.
    """
    def get(self, request, format=None):
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = SnippetSerializer(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)

こっちはデータ取得がわかりやすくなった... のか?

get_object() が最初に呼ばれるってことなんだろう。



@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk, format=None):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)



class SnippetDetail(APIView):
    """
    Retrieve, update or delete a snippet instance.
    """
    def get_object(self, pk):
        try:
            return Snippet.objects.get(pk=pk)
        except Snippet.DoesNotExist:
            raise Http404

URLにもまた as viewが


urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]




urlpatterns = [
    path('snippets/', views.SnippetList.as_view()),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]

Mixins

mixin を使うと、先のコードがこう短くできる


class SnippetList(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

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

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

これをさらに短くしたのがこれ。ほとんど自動でやってくる。returnしないのが不思議だ。

listではupdate処理がなく、createだけだから ListCreateAPIView

detailでは put が update処理をしてる。 get/put(update)/delete(destroy) になってる。
こっちはcreateがない。

updateってpatchじゃないのか

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

PUT と PATCH の違い

これが知りたかったこと。↑のmixinで、putでもpatchでも同じ動きをすることを確認。
curlでためした。 'Content-Type: application/json' ヘッダは必須でした。これがないとjsonのparseに失敗するらしく、requiredなfieldsが足りないエラーになる。投げてるのに。

snippets/3/ のデータが有る前提です

全fields投げる場合、putでもpatchでも同じ動きをする

# PUT = Update
curl 'http://127.0.0.1:8000/snippets/3/' \
  -X 'PUT' \
  -H 'Content-Type: application/json' \
  --data-raw $'{"id": 3,"title": "333","code": "333","linenos": false,"language": "python","style": "friendly"}'

# Patch = Update
  curl 'http://127.0.0.1:8000/snippets/3/' \
  -X 'PATCH' \
  -H 'Content-Type: application/json' \
  --data-raw $'{"id": 3,"title": "444","code": "444","linenos": false,"language": "python","style": "friendly"}'

違いは、カラムひとつだけをupdateしたい場合。

PutはエラーになるがPatchは通る。これは大きな違いだ。殆どの場合、patchをつかうことになるだろう。

# Put requires all of the fields
# ERROR {"code":["This field is required."]}
curl 'http://127.0.0.1:8000/snippets/3/' \
  -X 'PUT' \
  -H 'Content-Type: application/json' \
  --data-raw $'{"id": 3,"title": "555"}'


# Patch does not require other columns!
  curl 'http://127.0.0.1:8000/snippets/3/' \
  -X 'PATCH' \
  -H 'Content-Type: application/json' \
  --data-raw $'{"id": 3,"title": "666"}'

PUT: リソースの作成、リソースの置換
POST: リソースの作成
PATCH: リソースの部分置換
https://qiita.com/suin/items/d17bdfc8dba086d36115

で、何がすごいって、PUTもPATCHも動いてるけど、DRFのviewには一切その文字がないところです。

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

returnもない。これでdelete methodも対応してる。
初見殺しすぎるだろ。

RetrieveUpdateDestroyAPIView のところを見て、何が動くのか想像する練習が必要そう。

で、ここまで形にはめられてるmixinの処理をちょっとだけ変えたいときはどうするんだろうか。

Tutorial 4

admin user作るときはこれ。覚えられない passowrd 決められるよ。

python manage.py createsuperuser

modelの .save() を上書きする。saveする前に、演算だけで作れるフィールドの値を作って差し込むコード。
def save を作って super().save() すれば、目に見えないsave()を上書きできる。
これもreturnしないのね。

def save(self, *args, **kwargs):
    """
    Use the `pygments` library to create a highlighted HTML
    representation of the code snippet.
    """
    lexer = get_lexer_by_name(self.language)
    linenos = 'table' if self.linenos else False
    options = {'title': self.title} if self.title else {}
    formatter = HtmlFormatter(style=self.style, linenos=linenos,
                              full=True, **options)
    self.highlighted = highlight(self.code, lexer, formatter)
    super(Snippet, self).save(*args, **kwargs)

Adding endpoints for our User models

userにはpostさせない。

modelにowner fieldを追加

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)

modelが変わったのでserializerも変わる。なぜ他のfieldがないかというと、それは Snippet modelの中で普通のカラムとして定義されてるから。たぶん。
ownerは models.FOreignKey となっているので、扱いが違う。たぶん。

Because 'snippets' is a reverse relationship on the User model, it will not be included by default when using the ModelSerializer class, so we needed to add an explicit field for it.

reverse relationship てのは、user tableにユーザのマスタがあるので、snippetsは参照するだけだからユーザのマスタじゃないから、ってことだと思う。

class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')      <---------------この行追加

    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style', 'owner']   <----ここにも, 'owner'たす

ここは全く意味がわからなかった。 owner.username って何?

まず ReadOnlyField は CharField, BooleanField の友達。CharField(read_only=True) と同じ。
getのときは使われるが、deserialized されてるときは update に使われない。便利。

The field we've added is the untyped ReadOnlyField class, in contrast to the other typed fields, such as CharField, BooleanField etc... The untyped ReadOnlyField is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized. We could have also used CharField(read_only=True) here.

でもowner fieldって今そのmodelにいて、Foreign keyをinsert/updateしないといけないのよね。どういう意味??

次の疑問。serializerに def perform_createを追加。そこでownerを足してやる。
modelがupdateするときにはownerが必須になったので、でも Snippet.objects.all() には含まれないので、無理やりsaveする前にownerを足してやる。
modelだと .save() 上書きは def .save() するけど、serializerだと perform_create() を足すような感じだ。

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def perform_create(self, serializer):         <--------------
        serializer.save(owner=self.request.user)       <--------------

で、owner.usenameだけどさ

This field is doing something quite interesting. The source argument controls which attribute is used to populate a field, and can point at any attribute on the serialized instance. It can also take the dotted notation shown above, in which case it will traverse the given attributes, in a similar way as it is used with Django's template language.
このフィールドは非常に興味深いことをしています。source 引数は、どの属性を使ってフィールドを生成するかを制御するもので、 シリアライズされたインスタンスの任意の属性を指定できます。また、上のようなドット表記も可能で、その場合は Django のテンプレート言語と同じように、与えられた属性を走査します。

日本語にしても意味不明だった。もしかして?という思いがあり人生で初めてsqlite3にコマンドを叩いた。
sqlite3 ってこんなコマンドなのね。

show tables = .tables
describe tables = .schema $TABLE_NAME

https://www.sqlitetutorial.net/sqlite-tutorial/sqlite-describe-table/

~/repos/tutorial$ sqlite3 ./db.sqlite3

sqlite> .tables
auth_group                  django_admin_log
auth_group_permissions      django_content_type
auth_permission             django_migrations
auth_user                   django_session
auth_user_groups            snippets_snippet
auth_user_user_permissions

sqlite> .schema auth_user
CREATE TABLE IF NOT EXISTS "auth_user" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "password" varchar(128) NOT NULL, "last_login" datetime NULL, "is_superuser" bool NOT NULL, "username" varchar(150) NOT NULL UNIQUE, "last_name" varchar(150) NOT NULL, "email" varchar(254) NOT NULL, "is_staff" bool NOT NULL, "is_active" bool NOT NULL, "date_joined" datetime NOT NULL, "first_name" varchar(150) NOT NULL);

sqlite> select * from auth_user;
1|pbkdf2_sha256$216000$yEDZc3V5o9Kr$4Lvb3YLYrSfzpFWS+u/jkEg7fDVFbx/zGluN22Vzoyo=||1|admin||admin@example.com|1|1|2021-03-13 15:33:06.223102|

これでひとつわかったんだけど

models.ForeignKey('auth.User'auth.User って auth table の User column じゃなくて、ただ auth_user tableのことだったのかもしや。
「このtableとjoinしたい」って言うだけで、PK勝手に入るてきなやつなのかな。
self.request.user ももしや user objectでauth_user talbeの全フィールドが入ってるのか。

model:		 Foreignkey('auth.User')
serializers: ReadOnlyField(source='owner.username')
view:        serializer.save(owner=self.request.user)

serializersのowner = auth_user のレコードひとつで、その中の username fieldを拾うってことなのかな。それはgetの動き。
get/putのときの動きの違いがよくわからない。putのときはviewがsave(owner-user-name) して、serializersはwriteだからreadonlyfieldをスルーしてモデルに渡す。
modelは・・・それでも viewの.save()できたuser objectは渡すのか。それをそのままforeign keyさせるのか、な??

そのあとはuser permissinoでupdate/delete権限を制限する話。疲れたので読むだけで終わり

Tutorial 5: Relationships & Hyperlinked APIs

ModelSerializer → HyperlinkedModelSerializer にして、relationしてみる。
PKではなくhyperlinkでjoinする
 

field に 'url', が増える。 urlってカラムあったらどうするんだろう・・・

class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style', 'owner']



class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ['url', 'id', 'highlight', 'owner',
                  'title', 'code', 'linenos', 'language', 'style']

当然ながら Snippet modelには url というカラムはない。pkの代わりに自動生成されて使う感じなんだろうか。
つまるところ。modelに書いてる ForeignKey の紐付けはpkでされるんだけど

urls.py には必ず name= をつけること。この名前がserializerのHyperlinkedRelatedFieldや、 HyperlinkedIdentityFieldで使われる。


# API endpoints
urlpatterns = format_suffix_patterns([
    path('', views.api_root),
    path('snippets/',
        views.SnippetList.as_view(),
        name='snippet-list'),
    path('snippets/<int:pk>/',
        views.SnippetDetail.as_view(),
        name='snippet-detail'),
    path('snippets/<int:pk>/highlight/',
        views.SnippetHighlight.as_view(),
        name='snippet-highlight'),
    path('users/',
        views.UserList.as_view(),
        name='user-list'),
    path('users/<int:pk>/',
        views.UserDetail.as_view(),
        name='user-detail')
])

よくわからないけど動いた。

image.png

うーん。だめだ。まったく理解できない。

Tutorial 6: ViewSets & Routers

ViewSet classes are almost the same thing as View classes, except that they provide operations such as retrieve, or update, and not method handlers such as get or put.

ViewSetはViewとほとんど同じだけど、get/postというハンドラーがなくて retrieve/update になっている。

let's refactor our UserList and UserDetail views into a single UserViewSet

ReadOnlyModelViewSetをつかうとupdateできないviewのlist/detailを一気に作れる


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer




class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    This viewset automatically provides `list` and `retrieve` actions.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

次は更新系も含む3つのviewを1つにする

ModelViewSet が更新もできるViewSet.

@action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer]) のところが、カスタム処理を更新に挟むときの部分。

Custom actions which use the @action decorator will respond to GET requests by default. We can use the methods argument if we wanted an action that responded to POST requests.

@action(methods="POST") とするとカスタム処理を入れれそうだ。

@action(url_path="/foovar") も使えるようだ。

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)






class SnippetViewSet(viewsets.ModelViewSet):
    """
    This viewset automatically provides `list`, `create`, `retrieve`,
    `update` and `destroy` actions.

    Additionally we also provide an extra `highlight` action.
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

urls.pyは少し複雑になる。

get, put, patch, delete がviewでいう retrieve, update, partial_update, destroy に紐づくらしい。
いやget : list , post: create もあるな・・複雑だ・・

list画面ではpostでcreateする。detail画面ではもうあるデータの置き換えだから put : update, patch: partial_update か。createはない。


snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})
...

urlpatterns = format_suffix_patterns([
    path('', api_root),
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
])

各routingにviewSet.as_viewのdictを叩き込む。

で、これだとやってられないので、DefaultRouter() を使うとシンプルにできる。

# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)

# The API URLs are now determined automatically by the router.
urlpatterns = [
    path('', include(router.urls)),
]

おいおいroutingのnameはどこいったんだ。

ソレを使ってるのは def api_root() だけど・・と思ったら、defaultRouterを使うとそれ自体が不要になる。

The DefaultRouter class we're using also automatically creates the API root view for us, so we can now delete the api_root method from our views module.

これをまんま削除しても、今まで通りに動く。

@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users': reverse('user-list', request=request, format=format),
        'snippets': reverse('snippet-list', request=request, format=format)
    })

省略されすぎだろ・・・何がどうして動いてるのか理解しきれんぞコレは・・・・

やっぱりそういう事がまとめに書いてあった。↓

Trade-offs between views vs viewsets
Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using viewsets is less explicit than building your views individually.
ビューとビューセットのトレードオフ
ビューセットを使用すると、非常に便利な抽象化が可能になります。ビューセットを使用することで、API全体でURL規則の一貫性を確保し、記述する必要のあるコードの量を最小限に抑え、URLコンフの仕様よりもAPIが提供するインタラクションと表現に集中することができます。
しかし、これが常に正しいアプローチであるとは限らない。関数ベースのビューではなく、クラスベースのビューを使用する場合と同様に、考慮すべきトレードオフがあります。ビューセットを使用すると、ビューを個別に構築するよりも明示的ではなくなります。

お、これでtutorial終わりだ。

終わり!

結論

よくわからないけど、不思議な力で動くシーンがたくさんあることがわかった
write権限の有無でviewsetを分けて
カスタム処理をするところにだけ処理を追加する方法
が、たぶん一般的なんだろう。

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?