Django Rest Framework之Tutorial 4分析与实践

前言

在DRF所有的练习中,Tutorial 4算是比较难以理解的一部分,因此对这一节做一分析。在开始阅读之前,强烈建议你完成Tutorial 1-3的所有内容,我们以下所有的内容都会用到前面三个练习的代码。

知识准备

  1. 主键:若某一个属性组(注意是组)能唯一标识一条记录,该属性组就是一个主键。主键不能重复,且只能有一个,也不允许为空。定义主键主要是为了维护关系数据库的完整性。
  2. 外键:外键用于与另一张表的关联,是能确定另一张表记录的字段。外键是另一个表的主键,可以重复,可以有多个,也可以是空值。定义外键主要是为了保持数据的一致性。
    这里对Tutorial 4中的User表说明一下:
    auth_user表
    在这个表中,参阅Django官方文档中提到,如果没有特别声明,将默认添加一个id字段并以该字段作为主键。虽然auth_user不是我们主动创建的,在auth_user中,id还是作为主键:
    这里写图片描述

正式开始

Adding information to our model

这一节首先向models.py中的Snippetmodle中写入了两个新的字段:

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

我们重点关注第一行代码。其中涉及了一个函数models.ForeignKey,先看Django官方的描述:

A many-to-one relationship. Requires two positional arguments: the class to which the model is related and the on_delete option.
一种多对一的关系。需要两个参数:需要建立关系的model 类和on_delete选项。

model类和参数on_delete很好理解:

  • 我们需要告知model Sneppet要和model User建立关系
  • on_delete=models.CASCADE的意思如下:
    删除:删除主表时自动删除从表。删除从表,主表不变
    更新:更新主表时自动更新从表。更新从表,主表不变
  • 最后,related_name参数,如果看字面意思应该是关系名,这个有什么用呢?官方的解释太拗口,可以参考这篇博客,后面我会用代码解释这个关系名的具体用法。暂时可以这么理解,我们的Snappet model和User model是一对多的关系,我们可以通过外键表明某段“snappet”属于某个“user”,我们还需要从某个“user”入手查找属于他的“snappet”,这就是related_name的用处。

现在我们可以这样说:
Snippets表中的owner字段是Snippets表的外键
这里写图片描述
这一节剩下的部分很好理解,不再赘述。
我们来解决上面提到的related_name参数的问题。
首先完成所有需要的代码,我下面给出的代码均来源于Tutorial 4,你的snipetts/models.py应该是这样:

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

LEXERS=[item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0],item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())

class Snippet(models.Model):
    create=models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
    owner=models.ForeignKey('auth.User',related_name='snippets',on_delete=models.CASCADE)
    highlighted=models.TextField()

    class Meta:
        ordering = ('create',)
    def save(self,*args,**kwargs):
        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)

snippets/serializers.py应该是这样

from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
from django.contrib.auth.models import User
class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
    class Meta:
        model = User
        fields = ('id', 'username', 'snippets')

然后运行:

rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate

然后使用

python manage.py createsuperuser --email admin@admin.com --username
admin
python manage.py createsuperuser --email admin@admin.com --username
root

创建两个用户,用户名可随意
然后进入shell:

python manage.py shell

首先import:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from snippets.serializers import UserSerializer
from django.contrib.auth.models import User

然后我们直接操作Snippet写入一部分数据:

u=User.objects.get(pk=1) //这是为了获取一个用户
s=Snippet(owner=u,code="print hello") //为获取到的用户写入一段snippet
s.save() //保存

查看数据库:

成功写入了一条数据
最后,我们来使用related_name

u.snippets.all() //这里就是使用related_name进行反向查询
<QuerySet [<Snippet: Snippet object (1)>]> //执行结果

也就是说我们通过user中的一条记录,查询到了该用户具有的snippet

Adding endpoints for our User models

在这一节开始,我们要向serializers.py文件中加入如下代码:

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ('id', 'username', 'snippets')

如果你对serializer理解不太深,可暂且认为serializer是将model和json互相转换的过程。
显然,User model中没有snippets这个字段,当然,我们也大可不必再User的serializer中加入snippets这个字段,User model仍然会很好的运行。这里加入的原因是我们想在从model中获取全部或某个用户的时候同时获得他拥有的snippets。
我们对serializers.PrimaryKeyRelatedField的作用描述如下:使用 PrimaryKeyRelatedField 将返回一个对应关系 model 的主键,参考知乎
对应到我们的练习中,snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())这句代码将返回当前序列化USER的在model Snippets中的记录的所有主键。
我们用实验来说明。不必修改代码,继续上面的shell。
首先我们还是再加入一段snippet,不过这次使用另一个用户:

>>> u=User.objects.get(pk=2)
>>> s=Snippet(owner=u,code="whoami")
>>> s.save()
>>> s=Snippet(owner=u,code="ifconfig")
>>> s.save()
>>> us=UserSerializer(u)
>>> us.data

如上,我们先使用id=2的用户建立了两条sneppts记录,然后使用UserSerializer序列化了该用户,最后我们输出序列化结果:

{'id': 2, 'username': 'root', 'snippets': [2, 3]}

显然,截止目前,我们能够做的事情有:
- 能够在Snippet model中插入和获取数据,且能够实现owner作为外键
- 能够使用User的Serializer对User model进行序列化和反序列化,且能够反查snippet
这一节其他的代码很简单,不再赘述。

Associating Snippets with Users

这一节只做了一件事:

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

将上面的代码写入到了snippets的view.py中的SnippetList类中。为什么要这么写?参考Tutorial 3
首先(WHAT),我们知道SnippetList继承于ListCreateAPIView类,这个类中替我们实现了列出序列化所有数据和反序列化创建一条记录,也就是数据库的select allinsert。函数perform_create在这里被重写了一次,这里的重写是OOP里面的含义。可以在执行继承来的ListCreateAPIView中的create()函数时添加一个额外的owner字段。因此,serializer (注意上面的这段代码中serializer只是一个参数名而已,不是类名)也将首先反序列化owner字段并保存。当然,对应到我们实际的代码中,serializer实际上是SnippetSerializer,因为我们在SnippetList类中显式声明了serializer_class = SnippetSerializer。这里可能有点绕,关于serializer.save这个用法,可以参考Tutorial 1中的代码,如下所示:
这里写图片描述
其次(WHY),我们要问,既然ListCreateAPIView替我们完成了select allinsert功能,那么为什么我们要单独把owner提出来?单独为它写一段代码?Good Question。事实上,我们可以不写这一段代码,前提是,owner作为一个POST 数据包的参数而不是HTTP HEADER传入
最后(WHERE),请记住,我们写这段代码的位置是view.py,也就是说,我们应当从浏览器或者HTTP协议的角度去考虑这个函数,而不是从命令行。我们后面会有一个实验解释这个问题,现在,请把它写入到你的view.py。

Updating our serializer

按照这节的要求,我们的serializer.py应该是下面这个样子:

from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
from django.contrib.auth.models import User
class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner')
class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
    class Meta:
        model = User
        fields = ('id', 'username', 'snippets')

我们来研究下owner = serializers.ReadOnlyField(source='owner.username')这行代码。
首先从整体来说,这行代码和我们在Tutorial 1 这一节中做的类似,我们在序列化器中声明了一个owner字段。但是同样的问题,为什么SnippetSerializer在继承了serializers.ModelSerializer类后还要再次声明呢?我们用实验来解释。
第一步:先注释掉这行代码,然后进入python manage.py shell
第二步:执行下面的代码:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
s=Snippet.objects.get(pk=1)
ser=SnippetSerializer(s)
ser.data

我们看到返回的数据为:

{'linenos': False, 'title': '', 'language': 'python', 'owner': 1, 'style': 'friendly', 'code': 'print hello', 'id': 1}

注意owner这个值,显然不需要这一行代码,我们的序列化器依旧可以正常工作,这里owner值为1,我们知道它代表User model中id=1的记录。
第三步我们取消被注释的代码,重复上面的步骤,返回的结果是:

{'title': '', 'code': 'print hello', 'owner': 'admin', 'linenos': False, 'style': 'friendly', 'language': 'python', 'id': 1}

这里有一些有意思的变化,owner的值变为了admin。为什么会有这样的变化?原因在source='owner.username'这里。
先看DRF官方关于source关键字的解释:

The name of the attribute that will be used to populate the field. May be a method that only takes a self argument, such as URLField(source=’get_absolute_url’), or may use dotted notation to traverse attributes, such as EmailField(source=’user.email’). When serializing fields with dotted notation, it may be necessary to provide a default value if any object is not present or is empty during attribute traversal.
The value source=’*’ has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.
Defaults to the name of the field.

为了方便我翻译下这段话:

(source参数)是指用来填充字段的属性的名称。可以是一个采用self关键字的方法,例如URLField(source='get_absolute_url'),或者可以使用点符号来遍历属性,例如EmailField(source =’user.email’),在使用点符号序列化字段时,如果在属性遍历期间对象不存在或为空,则可能需要提供缺省值。
source ='*'具有特殊含义,表示整个对象应该传递到该字段。……
默认为该字段的名称

回到我们的代码owner = serializers.ReadOnlyField(source='owner.username')这行代码中。也就是说,如果我们将这行代码写为上面的形式的时候,序列化器将从owner指向的User model的某条记录中获取其username字段的值,如果我们写为owner = serializers.ReadOnlyField(source='*'),那么最终序列化出的owner的值就是当前Snippet对象,是的你没有看错,可以自行实验一下。如果我们写为owner = serializers.ReadOnlyField(),也就是默认值情况下,那么最终序列化出的owner的值就是当前User对象,是的你也没有看错,可以自行实验一下。DRF的文档说的也不是那么明白。
OK,现在我的的序列化器会将owner序列化为User.username,并且是一个ReadOnly字段。我们来看一下serializers.ReadOnlyField这段的意思。先通俗的说明一下,ReadOnlyField表示具有该属性的字段在从model序列化时可以正常返回其值,但是在从json反序列化为model时,不接受json中对该值的修改。结合我们的例子看,owner也应该是只读的。我们用实验来解释:
第一步修改serializer.py中的代码:

owner = serializers.ReadOnlyField(source='owner.username')

也就是沿用Tutorial 4中的代码。
第二步进入python manage.py shell,执行:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
s=Snippet.objects.get(pk=1)
ser=SnippetSerializer(s)
ser.data

这里没有什么变化,输出的内容和之前一样:

{'title': '', 'code': 'print hello', 'style': 'friendly', 'id': 1, 'owner': 'admin', 'language': 'python', 'linenos': False}

还是注意owner的值,表明这行代码属于admin用户,现在,我们更新Snippet model中的这条记录,尝试将它的owner修改为root,并且为了对比,我们也修改code值。
第三步,继续执行:

data=ser.data
data['owner']='root'
data['code']='env'
ser=SnippetSerializer(s,data=data)
ser.is_valid()
ser.save()
ser.data

这里的输出为:

{'id': 1, 'title': '', 'language': 'python', 'owner': 'admin', 'linenos': False, 'code': 'env', 'style': 'friendly'}

我们可以重新序列化一下s:

s=Snippet.objects.get(pk=1)
ser=SnippetSerializer(s)
ser.data

输出为:

{'id': 1, 'title': '', 'language': 'python', 'owner': 'admin', 'linenos': False, 'code': 'env', 'style': 'friendly'}

显然,我们的确修改code的值,但是owner的值并没有变为root。当然,如果我们修改代码为:

owner = serializers.CharField(source='owner.username')

取消ReadOnly属性,然后重复上面的步骤,就会在save的时候报错,错误信息为”update()函数无法对采用了source='owner.username'这类形式的变量进行save,需要重写update函数或者加入readonly属性。”
现在,我们解决最后一个疑问,为什么要写这行代码?

owner = serializers.ReadOnlyField(source='owner.username')

查看我们的model.py:

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

LEXERS=[item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0],item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())

class Snippet(models.Model):
    create=models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
    owner=models.ForeignKey('auth.User',related_name='snippets',on_delete=models.CASCADE)
    highlighted=models.TextField()

    class Meta:
        ordering = ('create',)
    def save(self,*args,**kwargs):
        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)

我们在声明owner时代码如下:

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

我们没有指明owner为readonly,也没有指明owner在序列化时要以User.username进行填充,这导致了两个问题:

  • 我们可以像上一节一样修改owner的值
  • 在序列化一个snippet对象后,owner的值是User.id

最重要的是,在model中,我们无法显式声明一个字段为’read-only’,因为这是序列化器的事。所以,我们无法在model中设置owner为只读。

小结

截止目前,我们实现了:

  • 向snippet model中加入了一个owner外键,关联到User model的主键
  • 我们在snippet model中将这种关系命名为snippets,从而我们可以由User反查Snippet
  • 我们在Snippet的序列化器中,声明了owner字段在序列化和反序列化时的行为如readonly,填充User.username
  • 我们在User的序列化器中加入了snippet字段,使得序列化和反序列化一个user对象时将snippets加入到其中

思考一下,我们完成了吗?我们上面所有的操作,都没有到达VIEW层面,也就是HTTP层面,现在不论是谁,都可以查询任意用户的snippets,也可以更新任意用户的snippets,除了你不能将其他用户的snippet占为己有。我们需要在VIEW层加入访问权限

Object level permissions

我们跳过了Adding required permissions to viewsAdding login to the Browsable API两节,这两节内容很简单,但你还是应该完成其中要求的代码。完成之后,我们遇到一个问题,我们加入的权限控制为permissions.IsAuthenticatedOrReadOnly,也就是说,现在如果你通过浏览器登录成功之后,对任意用户的snippets均具有读写权限,如果登录失败,则仅具有读权限。这不是我们想要的,至少只是其中一部分。我们想要实现的是登录之后的用户,对自己的snippets具有完全的控制权,但是对其他用户的snippets,则仅具有可读权。我们需要实现自定义的permissions类:

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

我们研究一下这段代码,首先我们创建了一个类IsOwnerOrReadOnly,它继承于permissions.BasePermission,然后我们重写了方法has_object_permission。这个方法有三个参数,request参数很好理解,代表我们发送的http请求对象,view参数,可以将其理解为我们在snippets/urls.py定义的各个url,snippets/urls.py如下:

from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns=[
    url(r'^users/$', views.UserList.as_view()),
    url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
    url(r'^snippets/$',views.SnippetList.as_view()),
    url(r'^snippets/(?P<pk>[0-9]+)/$',views.SnippetDetail.as_view()),
]

urlpatterns=format_suffix_patterns(urlpatterns)

view参数代表上面的每一个url指向的view类,最终我们会在每个view类中声明permission_classes = (permissions.IsAuthenticatedOrReadOnly,),这样每次调用has_object_permission函数时参数view会对应到各个view类上。最后一个参数obj代表我们每个访问请求的对象,对应到我们的代码中就是某个SnippetSerializer对象。可以将其理解为资源。
我们继续研究has_object_permission函数:

def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

对于if request.method in permissions.SAFE_METHODS:,我们首先要知道permissions.SAFE_METHODS,代表什么,启动命令行,我们看一下:
这里写图片描述
显然是一个元祖,其中定义了三种请求方式:GETHEADOPTIONS,如果你对HTTP的方法的定义比较理解,你会提出一个问题,GET方法依旧可以携带参数,很多时候POST的内容也可以以GET的方式作为参数提交,如果仅仅以请求方式来判断权限,是否能够保证安全?答案是可以,因为我们在urls.py中仅接受pk参数以GET的方式提交。
现在,不管你是否登录成功,是否是你要访问的SnippetSerializer对象的owner,你都可以查看SnippetSerializer对象序列化的内容(return True)。
最后我们对于request.methodPUTPOSTDELETE方式的请求,做了一个判断,如果是某个SnippetSerializer对象的拥有者则返回True,否则返回False。
OK,完善后面的代码,我们来测试一下是否成功了!

测试

你可以使用DRF文档中介绍的方式来测试,我这里采用burpsuite测试。

GET测试(List)

这里写图片描述
成功获取到了6组记录

POST测试(Create)

这里需要在HTTP头中加入参数Authorization: Basic YWRtaW46cXdlcjEyMzQ=YWRtaW46cXdlcjEyMzQ=这段是【用户名:密码】的Base64编码,在我测试时,即为admin:qwer1234的Base64编码,这是BasicAuthentication的基本要求,具体请百度。同时也要加入Content-Type: application/json为HTTP头。然后,我们尝试创建一个属于admin用户的snippets:
这里写图片描述
创建成功,你也可以测试一下将owner修改为root,看最后创建的Snippets对象的owner是否会随着你的修改而变化。

PUT测试(Update)

这里写图片描述
修改成功,我们尝试修改一下以admin用户的身份修改一下root用户的snippet:
这里写图片描述
很不错,权限控制代码成功发挥了作用。
对于其他的测试如DELETE可以自行测试。

完了吗?

不要忘记,我们还有一个问题没有解答。
view.py中的SnippetList类中为什么要有下面这行代码?

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

不多说,我们先去把这个代码注释掉,然后运行我们的程序,进行测试:
我们尝试create一条snippets记录:
这里写图片描述
这里写图片描述
报错了,什么意思呢?告诉我们owner_id这个字段不能为空,至于为什么是owner_id而不是owner字段,参考Django官方文档,在这里不再赘述。也就是说,代码没有从我们我们的数据包中获取到owner字段的值,但是我们POST的数据中明明包含了owner的值为admin。
我们先通过一个实验来解释:
首先进入命令行,然后导入我们需要的包:

python manage.py shell
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from snippets.serializers import UserSerializer
from django.contrib.auth.models import User

然后,我们尝试直接写入一条记录到 model Snippet中:

 s=Snippet(code="print helloworld",owner="admin")

看起来很美好,我们写入了一个属于admin用户的snippet,结果却报错了:
这里写图片描述
原因是owner应该是一个User记录而不是一个字符串。
也就是说,如果我们修改view.py中内容如下:

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.data['owner'])

我们从POST过去的json中获取owner的话,仍然会报错,应为它是一个字符串而不是一个User记录。
这里我们需要理解request.user的含义,它从我们POST的数据包的HTTP头中获取用户名,并且将它转换为一个User记录对象。如果我们将view.py修改为:

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
    def perform_create(self,serializer):
        print(type(self.request.user))
        serializer.save(owner=self.request.user)

然后POST一条数据的话,可以看到request.user是一个User对象。
这里写图片描述
这样就能够序列化并保存了。

结语

写这篇文章的目的,是因为我自己也是一个DRF的初学者,在做练习的时候发现练习4还是比较难理解,尤其对我这种只用过FLASK的人来说,因此,将我学习过程中的一些分析和理解分享出来,希望对初学者有用。

猜你喜欢

转载自blog.csdn.net/leehdsniper/article/details/80710786