本文討論 django restframework 的日常使用,滿足常用 api 編寫的需求,比如 List, Detail, Update, Put, Patch 等等。探討 django restframework 的一般使用,爭取總結出 django restframework 的最佳實踐。
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()andupdate()methods.
問題:如何override ModelSerializer 的 fields?添加屬性,read_only 等
django restframework 在 List, Retrieve, create, update, 等的實現原理
List
將 QuerySet 序列化為 dict,通過 JsonResponse 返回
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JsonResponse(serializer.data, safe=False)
Create
從 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 delete
@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)
對於部分修改,不需要驗證所有的東西。
# 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
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 自帶或者其擴展來實現搜索功能?
好處是能增強復用性,功能更多,寫更少代碼。
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
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.AnonymousUseris a class that implements thedjango.contrib.auth.models.Userinterface, with these differences:- id is always
None. usernameis always the empty string.get_username()always returns the empty string.is_anonymousisTrueinstead ofFalse.is_authenticatedisFalseinstead ofTrue.is_staffandis_superuserare alwaysFalse.is_activeis alwaysFalse.groupsanduser_permissionsare always empty.set_password(),check_password(),save()anddelete()raiseNotImplementedError.
- id is always
In practice, you probably won’t need to use
AnonymousUserobjects 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。
