本文討論 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:前端
create()
and update()
methods.問題:如何override ModelSerializer 的 fields?添加屬性,read_only 等python
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.
能夠將自定義的放在 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.
經過添加 format suffixes Adding optional format suffixes to our URLs
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、外鍵的修改、增長?
前端傳入列表?具體看文檔
好比重置密碼、修改密碼都須要手機驗證碼。可是用戶 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。
怎麼修改這個驗證?
怎麼修改這個保存邏輯?
好比,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 用來識別用戶的身份
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": "權限不足"})
1. 設計 serializer
2. 獲得 queryset
3. 序列化後返回
或者
1. 設計 serializer
2. 從 request 中獲取數據
3. 反序列化後保存
若是你要實現自定義,能夠重載相關函數,很是靈活
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()
問:可否使用 DRF 自帶或者其擴展來實現搜索功能?
好處是能加強複用性,功能更多,寫更少代碼。
1. 普通filed
如 price=10, price>10
2. 外鍵
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
經過城市名搜索用戶
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
背景: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 thedjango.contrib.auth.models.User
interface, with these differences:
- id is always
None
.username
is always the empty string.get_username()
always returns the empty string.is_anonymous
isTrue
instead ofFalse
.is_authenticated
isFalse
instead ofTrue
.is_staff
andis_superuser
are alwaysFalse
.is_active
is alwaysFalse
.groups
anduser_permissions
are always empty.set_password()
,check_password()
,save()
anddelete()
raiseNotImplementedError
.
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) 中第一種更好。
經過文檔搜索
搜索 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
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。