django restframework 的平常使用

本文討論 django restframework 的平常使用,知足經常使用 api 編寫的需求,好比 List, Detail, Update, Put, Patch 等等。探討 django restframework 的通常使用,爭取總結出 django restframework 的最佳實踐。html

ModelSerializer classes don't do anything particularly magical, they are simply a shortcut for creating serializer classes:前端

  • An automatically determined set of fields.
  • Simple default implementations for the create() and update() methods.

問題:如何override ModelSerializer 的 fields?添加屬性,read_only 等python

 

django restframework 在 List, Retrieve, create, update,  等的實現原理

Listgit

將 QuerySet 序列化爲 dict,經過 JsonResponse 返回數據庫

if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)

Createdjango

從 request 中獲取 data: dict,將 data 傳入 Serializer,若是序列化後是有效的,就保存,則建立成功;不然,建立失敗。編程

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)

retrieve, update or deletejson

@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)

部分修改 (Partial Update)api

對於部分修改,不須要驗證全部的東西。安全

# Update `comment` with partial data
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True)

So the rest framework will not perform field validation check for the fields which is missing in the request data.  

 

自定義 validation 

文檔地址

能夠將自定義的放在 Controller 中,進行復用及管理

針對字段的

針對Class的

 

django restframework 相比 Django View 的改進

更加靈活的 request object:

request.POST  # Only handles form data.  Only works for 'POST' method.
request.data  # Handles arbitrary data.  Works for 'POST', 'PUT' and 'PATCH' methods.

REST framework also introduces a Response object, which is a type of TemplateResponse that takes unrendered content and uses content negotiation to determine the correct content type to return to the client.

return Response(data)  # Renders to content type as requested by the client.

問題:如何決定返回客戶端的格式?好比是 json 仍是 xml?

經過添加 format suffixes Adding optional format suffixes to our URLs

使用 Mixin 進一步精簡代碼 

One of the big wins of using class-based views is that it allows us to easily compose reusable bits of behaviour.

The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.

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)

問:如何控制返回的顯示字段。

好比增長不屬於model的字段?

1. 若是須要更改不少,那麼能夠:

重載 serializer 中的 to_representation

具體實現:

if field_name in except_fields:
    data[field_name]='權限不足,沒法查看'

2. 若是隻是更改單個,也能夠在 serializer 中重載 def get_field。

request 能夠在 self 的 context 找到,這樣就能根據用戶來控制顯示信息,好比根據權限

 

總結:對於使用框架可是須要自定義的狀況,怎麼查找本身想要的?

描述清楚本身的問題

  • 熟悉官方文檔中的專業名詞
  • 找到官方文檔中的對應章節
  • 看源碼,弄清源碼中各個字段的做用。能夠搜索源碼中的變量名

 

 問:自定義儲存的數據

密碼須要 hash 化

是使用 DRF 自帶的驗證仍是在 controller 中寫一個 custom_validate?

 

ManytoMany、外鍵的修改、增長?

前端傳入列表?具體看文檔

 

 

問:增長不屬於 serializer 的驗證條件

好比重置密碼、修改密碼都須要手機驗證碼。可是用戶 model 裏面並無驗證碼這個選項。

1. 在密碼字段增長一個 validator

實現:

使用 DRF 的 field-level-validation

好比有一個 password 字段,修改前須要驗證手機驗證碼。那麼在對應的 Serializer 中添加 validate_password

def validate_password(self, value):
    # 獲取請求中的驗證碼
    verfication_code = self.context['request'].data.get('verfication_code')
    if not verfication_code:
        raise serializers.ValidationError("verfication_code 不能爲空")
    # 驗證驗證碼
    if verfication_code != correct_verfication_code:
        raise serializers.ValidationError("verfication_code 錯誤")

優化:你可能在多處須要驗證驗證碼。那麼你把上面的驗證過程抽象出來,只須要傳遞驗證碼以及其餘你須要的信息進去。若是驗證失敗,拋出 serializers.ValidationError 就能夠。

 

2. 單獨開出 api 在須要驗證碼的地方

好比修改郵箱要驗證碼,那就添加一個對應 api;

修改密碼也要驗證碼,那再添加一個對應 api;

 

問:restframework 的表單與Django 的表單有什麼不一樣?

 

 

問:重寫 field 的邏輯

設計數據庫的時候,喜愛經過管道符 | 鏈接。可是前段傳遞過來的是一個 list。

怎麼修改這個驗證?

怎麼修改這個保存邏輯?

 

自定義 field

好比,Django model 中的 Datetime 字段取出來是 python datetime.datetime 類型,可是前端通常須要 timestamp。能夠經過重載 `serializers.ModelSerializer` 中的 `serializer_field_mapping`來解決。

# 自定義 Datetime field
class CustomDateTimeField(DateTimeField):
    def to_representation(self, value):
        if not value:
            return None
        value = timezone.localtime(value)
        value = int(value.timestamp())
        return value

        
serializer_field_mapping = {
    # ------------------------自定義------------------------
    models.DateTimeField: CustomDateTimeField,
    # ------------------------自定義------------------------
}        

 

區分如下幾種:

不可見 fields

只寫fields

 extra_kwargs = {'password': {'write_only': True}}

不包括fields

You can set the exclude attribute to a list of fields to be excluded from the serializer.

For example:

class AccountSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account
        exclude = ('users',)

只讀fields

read_only_fields = ('account_name',)

特殊狀況

對於 modelserializer,數據庫中是 required=True,可是在 serializer 中 update 並不須要填入全部的參數,這時須要 required=False

我如今採起 override 的方式  

# 顯示聲明 field required=False
title = serializers.CharField(required=False)

處理嵌套關係渲染外鍵)

有一個 field_name=PrimaryKeyRelatedField,我將它變成 field_name=Serializer(),以渲染更多信息。可是建立的時候傳入field=field_id報錯:

[
"該字段是必填項。"
],

緣由:變成嵌套關係後,須要的是一個 instance,而不只僅是一個 id了。能夠經過 update_request_data 來改變。能夠直接改變 request.data,若是隻須要用來查找而不須要用來建立以及修改。若是須要建立或修改,則修改對應方法中的data

def create(self, validated_data):
        profile_data = validated_data.pop('profile')
        user = User.objects.create(**validated_data)

上面的方法仍是不行,提示「字段是必填項」。

進行了搜索,發現都是建立嵌套關係的,不是我想要的。因而換了個思路,不改變 Serializer,只改變 to_representation()就能夠了。

進行嵌套的建立麻煩,而且通常那個嵌套的都有一個 Serializer,那樣就並不須要經過嵌套來建立。

小結:一樣的需求,有時換一個方式會很好實現。經過查找、比較,找到易實現、易維護的方法。

問:嵌套關係的做用是什麼?如何經過嵌套關係建立?與不經過嵌套關係建立有什麼不同?有什麼好處與缺點?

 

驗證與權限

是否登錄、實名認證、公司認證、銀行卡認證。經過 authentication_classes 組合解決

是不是某個obj的owner,經過 permission_classes 解決

 

修改密碼的問題

若是沒有密碼怎麼處理?

 

若是有密碼怎麼處理?

使用 validator 進行驗證怎麼樣?好比驗證舊的交易密碼。

 

因此我如今統一作成經過手機驗證碼找回密碼,沒有修改密碼的接口。

找回密碼接口的實現

新建一個 api 而後使用表單實現

由於手機驗證碼這個字段與 serializer 不要緊了。

 

使用 serializer 實現

若是是修改密碼,那麼驗證驗證碼字段,驗證碼字段應該是 required=False,可是在特定狀況(若是找回密碼)的時候須要。

 

問:authentication_classes 與 permission_classes 的區別

從現有的使用來看,authentication_classes 與 permission_classes 彷佛能夠混用。好比實名認證放在 authentication_classes 與 permission_classes 彷佛均可以。

從文檔中以及字面意思來看:

authentication 用來識別用戶的身份

Authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with. The permission and throttling policies can then use those credentials to determine if the request should be permitted.

permission 用來判斷用戶是否有權限進行某項操做。

因此實名認證、公司認證、銀行卡認證應該放在 permission_classes 中比較好,而經過驗證碼登錄、帳號密碼登錄、微信等第三方登錄應該放在 authentication_classes 中。

 

問:如何自定義異常的返回信息

經過自定義 DRF 的 exception_handler

個人具體實現方法:

from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    # Now add the HTTP status code to the response.
    if response is not None:
        # 屬於自定義錯誤信息
        if response.data.get('code'):
            return response

# 如何將信息傳入 exception_handler 返回的 response 中?
raise serializers.ValidationError(detail={"code": "403", "message": "權限不足"})

  

總結一下 django restframework 的設計思路。

1. 設計 serializer

2. 獲得 queryset

3. 序列化後返回

或者

1. 設計 serializer

2. 從 request 中獲取數據

3. 反序列化後保存

 

若是你要實現自定義,能夠重載相關函數,很是靈活

 

問:使用 rest frame work實現filter功能

[不推薦]本身實現 filter 功能

1. 獲取搜索參數

request.query_params

多個 query_params 的問題 lists from query_params

傳遞: key=value1&key=value2

解析: request.query_params.getlist('key')

有時候請求會帶上 [],編程 key[],我採起了修改 request.query_params 的方法

def update_get_list_params(func):
    def wraps(self, request, *args, **kwargs):
        request.query_params._mutable = True
        for key in list(request.query_params.keys()):
            # Make sure you know this will not influence the other query_params
            if key.endswith('[]'):
                new_key = key.split('[]')[0]
                value = request.query_params.getlist(key)
                if value:
                    request.query_params.setlist(new_key, value)
        return func(self, request, *args, **kwargs)
    return wraps


@update_get_list_params
def get(self, request, *args, **kwargs):
     pass

 

修改: 修改 request.query_params 就能夠。由於DRF的源碼中也是經過 request.query_params 來 filter

# env/lib/python3.6/site-packages/django_filters/rest_framework/backends.py 74 行
        if filter_class:
            return filter_class(request.query_params, queryset=queryset, request=request).qs

如何修改 request.query_params  

  

request.query_params['city'] = ['廣州市', '深圳市', '成都市']
>>> AttributeError: This QueryDict instance is immutable
# 解決
# 添加
request.query_params._mutable = True
# DRF 的 request.data 、Django 的 GET 也是這樣

2. 根據搜索參數構建 filter

3. 經過 filter 找到 objects

3. 序列化 objects 後返回

self.list() 

[推薦]使用 django-filter 實現 filter 功能

問:使用DRF 以及 django-filter 實現tag filter功能

問:可否使用 DRF 自帶或者其擴展來實現搜索功能?

好處是能加強複用性,功能更多,寫更少代碼。

使用 django-filter

1. 普通filed

如 price=10, price>10

2. 外鍵

使用 modelchoicefilter

3. 多對多

    city = django_filters.ModelMultipleChoiceFilter(queryset=City.objects.filter(), name='city__name',
                                                    to_field_name='name')

3.2 多選

degree = django_filters.MultipleChoiceFilter(choices=constants.DEGREE_CHOICES, name='degree',)

問:OneToManyField 如何實現 ModelMultipleChoiceFilter 的效果

應該是相同的用法

 

4. 自定義  

自定義的 filter 只須要你返回一個 queryset 。

class CustomKeyWordFilterForRecruit(django_filters.CharFilter):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def filter(self, qs, value):
        """自定義關鍵字搜索--既搜索標題,也搜索內容"""
        q = Q(**{'title__icontains': value}) | Q(**{'content__icontains': value}) 
        qs.filter(q)
        return qs

  

manytomany filter by name(other field) not by id 

經過城市名搜索用戶

class ClientFilter(django_filters.rest_framework.FilterSet):
    city = django_filters.ModelMultipleChoiceFilter(queryset=City.objects.filter(), name='city__name', to_field_name='name')

    class Meta:
        model = Client
        fields = ['city']

name 的含義: 查詢 Client model 會變成 client.city__name   

to_field_name 的含義: 對應的city value 變成city.name

結果:如 ('city__name', city.name)

源碼位置:

# env/lib/python3.6/site-packages/django_filters/filters.py
# 296 行
return qs.distinct() if self.distinct else qs

 

問:關於filter外鍵

背景:client 是 user 的一對一

filter(client__user=self.request.user) 與 filter(client=self.request.user.client) 哪一個更好?

問:未登陸用戶是否會有 client 這個一對一關係?

沒有。

a = AnonymousUser()
self.a
>>>AnonymousUser
self.a.client
>>>{AttributeError}'AnonymousUser' object has no attribute 'client'
dir(self.a)
>>>['check_password', 'delete', 'get_all_permissions', 'get_group_permissions', 'get_username', 'groups', 'has_module_perms', 'has_perm', 'has_perms', 'id', 'is_active', 'is_anonymous', 'is_authenticated', 'is_staff', 'is_superuser', 'pk', 'save', 'set_password', 'user_permissions', 'username']

django.contrib.auth.models.AnonymousUser is a class that implements the django.contrib.auth.models.User interface, with these differences:

In practice, you probably won’t need to use AnonymousUser objects on your own, but they’re used by Web requests, as explained in the next section.

 因此 filter(client__user=self.request.user) 與 filter(client=self.request.user.client) 中第一種更好。

問:如何尋找自定義DRF中method的文檔、例子?

經過文檔搜索

搜索 custom, override, subclass等

(搜索 django-filter 的文檔沒找到很明顯的說明,搜索它的git倉庫也沒有找到。)

文檔沒有,經過搜索引擎搜索

搜索 django-filter custom filter

找到了這個  Django-filter and custom querysets,就能瞭解了。

最後根據源碼分析

好比我這裏須要自定義 CharFilter 的行爲,因而找到 `django_filters.CharFilter`,發現它的父類有一個 def filter 函數,返回 queryset。也和文檔中的匹配。

我就簡單的 override 了一下

class CustomKeyWordFilterForRecruit(django_filters.CharFilter):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def filter(self, qs, value):
        """關鍵字搜索 既搜索title,又搜索content"""
        q = Q(**{'title__icontains': value}) | Q(**{'content__icontains': value}) 
        qs.filter(q)
        return qs

令提交數據可改

DRF 的數據默認不能夠修改,要將其變爲可修改

request.data._mutable = True
request.query_params._mutable = True
# 若是常用,能夠寫一個裝飾器

而後就能夠直接修改 DRF request 中的數據

 

多選字段

在文檔中搜索 multiple,找到了 multiplechoicefilter

 

自定義字段

自定義一個關鍵字字段

 

客戶端傳遞列表

傳遞經過符號鏈接的字符串

好比,?tag=django|model&other_param=xxx

服務端接收到數據後 split() 一下

 

tag 在 model 中的字段類型

如今是 Charfield,使用 | 鏈接。

有沒有更好的選擇?

使用 ManytoMantfield

 

若是使用 ManytoMantfield:

問題:

要把數據寫入數據庫。

城市那麼多,要怎麼寫?

城市還有省份,要怎麼作

 答:首先獲取全球城市數據,而後寫一個腳本,將數據按照本身的要求寫入數據庫

 

問:使用 ManytoMantfield 的過程

建立 Model

建立 ManytoMantfield 關係

將數據寫入 Model

更新數據

變更serializer

 

 

前端獲取選項

將 Model 中的全部數據傳遞出去,以 (id, value) 的形式。

前端設置 ManytoMantfield 關係

 

前端獲取用戶全部tags

 

問:不一樣權限的用戶,看到的序列化後的東西不同。

好比有些字段要必定權限才能看到,沒有權限則顯示 `****`。

答:重載 serializer 中的 to_representation

問:若是有一個權限表,那麼restframework中應該怎麼作?

 

其餘

分頁

自帶的分頁功能足夠知足前端的須要。

{
  "count": 2,
  "next": "http://0.0.0.0:8112/business/apiv1/client/bank-cards/?page=2",
  "previous": null,
  "results": [
    {

前端經過 next 一直訪問下一頁,實現翻頁。當 next 爲空時,前端就能知道是最後一頁了。

若是須要適配前端的框架,也能夠新建一個 pagination_class,重載下面這個方法

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.page.paginator.count),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))

能夠更改返回字段的名稱,增長返回的字段,好比狀態碼  

 

使用 http 協議 built in 

使用不一樣的函數

 

post, put, patch 的區別

POST = 新增

新增,須要全部的數據

login 使用 post,新增一個 session
GET = 讀取

讀取單個或者列表
PUT = 新增或替換

存在則替換,不存在則新增。像 post 同樣,須要全部的數據

PATCH = 修改

修改,部分或者所有。
DELETE = 刪除

logout,刪除登錄 session

 

問題:[REST API ]login 與 logout 仍是有爭議。由於並無改變用戶的實際數據。

login

# Django login 的說明
def login(request, user):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """

這個能夠用 post  

logout

# Django logout 的說明
def logout(request):
    """
    Removes the authenticated user's ID from the request and flushes their
    session data.
    """

感受這個能夠用 delete。

問:session保持登錄的原理

能從 sessionid 中解析出 user_id

如何管理過時時間的?

 

問:刪除後這個token是否還以用

  

安全性

另外一個HTTP Methods 特性是」Safe」,這比較簡單,只有GET 和HEAD 是Safe 操做。Safe 特性會影響是否能夠快取(POST/PUT/PATCH/DELETE 必定都不能夠快取)。而Idempotent 特性則是會影響能否Retry (重試,反正結果同樣)。

  SAFE? IDEMPOTENT?
GET Y Y
POST N N
PATCH N N
PUT N Y
DELETE N Y

PUT 與 POST 的區別

 

使用頭部

 

期中總結

項目 demo 快要交付的時候,進行一下總結。

 

存在邏輯混亂的部分

返回混亂

沒有和前端約定好怎麼返回,寫到後面存在多種格式的返回。好比表單的選擇,有使用 Django form choices 的tuple形式,也有直接傳遞一個 list 的形式。

1. 和前端統一好。不要擅自提早端作主張。

 

代碼結構混亂 

驗證混亂

須要自定義驗證條件,代碼應該放在哪裏?

是使用 DRF 自帶的驗證仍是在 controller 中寫一個 custom_validate?

修改混亂

有時候須要需該儲存的數據,好比密碼 hash 化,是否應該在 controller 中寫一個 update_validated_data?

 

序列化混亂

大部分使用 serializer,可是有一些特殊的地方,在 Model 中新建了一些序列化方法。還有一些特殊字段的不一樣顯示,寫的混亂,東一下、西一下。很差管理、維護。本身寫的代碼,要增長、更改都會出錯。

體會:在以前的代碼基礎上面修改會更加輕鬆。可是!若是以前的架構不對,也須要更改架構。因此借鑑以前的代碼也須要動腦。

 

1. 將重複的邏輯進行整合,減小重複,增長可維護性--代碼可讀性,增長功能須要的代碼少而且易理解。

沒有單元測試

基本下是客戶端發現問題,而後跟我說 api 有問題。有時候改動了model,還影響了以前正常的 api。

相關文章
相關標籤/搜索