Django Web 框架

django 框架开发心得

博文封面

基于重量级 Web 框架 Django 开发 Web 项目

The web framework for perfectionists with deadlines.

Django 数据库驱动推荐使用 mysqlclient

某些诡异的数据库端错误,很多都是数据库驱动与数据库后端, ORM 框架与数据库驱动之间的兼容性问题。

今天发现使用 Django + mysql 官方的数据库驱动(mysql-python-connector)时,
检索出来的博客模型实例评论数在数据库中默认为 0 的全部映射为 none 了,非 0 的正常映射成功,
换了 mysqlclient 数据库驱动就好了,看来 Django 驱动对于 mysqlclient 兼容性好,对 mysql 官方的数据库驱动兼容性不行。

ORM SQL 优化

  • 如果只需要更新记录,不需要额外操作模型实例,那么可以直接通过 QuerySet.update 方式更新, 这样就不需要先查询得到模型实现再去修改字段信息再 Model.save 了,同时避免了数据库中的资源竞争情况的发生; 但是由于不是通过 Model.save 方式更新,所以某些依赖模型实例的 save 方法的操作或者某些字段更新的字段, 比如 日期字段的自动更新就会失效。这样就只能手动去更新对于字段了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@method_decorator(login_required)
    def put(self, request, post_id):
        """ 更新文章 """
        if not Post.posts.filter(pk=post_id).exists():
            raise ApiException(status_code=404, message='文章不存在')
        title = request.PUT.get('title')
        summary = request.PUT.get('summary')
        body = request.PUT.get('body')
        # 由于是 SQL 级别的更新,而不是 Model.save 方式,所以 updated_at 自动不会根据当前时间自动更新,
        # 需要手动跟新 updated_at 字段
        Post.posts.filter(pk=post_id).update(
            **{'title': title, 'summary': summary, 'body': body, 'updated_at': timezone.now()})
        return json_response('', status=204)
  • select_relatedprefetch_related select_related 预查询 多对一(根据外键)或 一对一 关系数据 ,只需一次 INNER JOIN 查询, 关系数据会缓存下来,每次引用关联数据就不需要二次查询了,尤其在批量获取关联数据的时候。 prefetch_related 发出独立的 SQL 查询 一对多(反向的多对一的关系),或 多对多关系数据,查询语句是 where in(id, ...)。 并且是在 python 中去关联数据的,比如说查询小明和小红这两个人的信息以及其名下所有发布的文章,文章列表查询是一个在独立的的查询中执行, 查询下来的文章列表是两个人的,但是会在 python 中去提取各自的文章数据到这两个人名下的文章关联字段中, 关系数据会被缓存,除非你又去执行其他过滤等查询操作,这样会再次执行查询,预先查询出来的数据就会被刷新。

这两个查询都是为了减少在检索关联数据的时候减少数据库查询次数,提升性能。

  • defer, only 按需检索字段

优雅编写 Resuful Api

由于 django 对构建 restful api 方面不太友好,很多地方都需要手动写一些业务之外的代码来满足写 rest api 的需求;
比如内置的序列化/反序列化模块对于接口场景不友好,异常处理如果不去自定义处理视图默认返回 html 格式响应。

如何统一处理 json 格式响应?
中间件 ,重写两个方法:

  • process_exception(self, execption ) 这是中间件处理整个项目中抛出的异常的地方,除了那些 django 内置的异常,django 内置异常的抛出不会回调这个方法, 比如 Http404,PermissionDined,ValidactionError 等等,可以从 Django 源码中了解到:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# diango>core>handler>base>BaseHandler
def load_middleware(self):
    """
    Populate middleware lists from settings.MIDDLEWARE.

    Must be called after the environment is fixed (see __call__ in subclasses).
    """
    self._view_middleware = []
    self._template_response_middleware = []
    self._exception_middleware = []

    handler = convert_exception_to_response(self._get_response)
    for middleware_path in reversed(settings.MIDDLEWARE):
        middleware = import_string(middleware_path)
        try:
            mw_instance = middleware(handler)
        except MiddlewareNotUsed as exc:
            if settings.DEBUG:
                if str(exc):
                    logger.debug('MiddlewareNotUsed(%r): %s', middleware_path, exc)
                else:
                    logger.debug('MiddlewareNotUsed: %r', middleware_path)
            continue

        if mw_instance is None:
            raise ImproperlyConfigured(
                'Middleware factory %s returned None.' % middleware_path
            )

        if hasattr(mw_instance, 'process_view'):
            self._view_middleware.insert(0, mw_instance.process_view)
        if hasattr(mw_instance, 'process_template_response'):
            self._template_response_middleware.append(mw_instance.process_template_response)
        if hasattr(mw_instance, 'process_exception'):
            self._exception_middleware.append(mw_instance.process_exception)

        handler = convert_exception_to_response(mw_instance)

    # We only assign to this when initialization is complete as it is used
    # as a flag for initialization being complete.
    self._middleware_chain = handler

加载 settings 配置模块中所有中间件并且倒叙方式实例化中间件,中间件的实现其实是个 callable 对象,
中间件可以是类的实例(类的实例是个 callable 的对象)或者一个返回处理请求的函数,
convert_exception_to_response 是个包装中间件的装饰器,
保证在中间件处理请求或者响应的时候即使发生异常也会被转换为一个异常响应,
这样就能保证之前的中间件能够正常处理在自己之后的中间件的响应。
每一个中间件的 get_response 其实引用的就是下一个中间件实例,
层层包装,这样整个中间件链就形成了,也就是第一个声明的中间件被一层层包装,处于最内层,类似洋葱结构。
如果中间件链条的顺序是 A->B->C->D,洋葱由外而内的结构是 D->C->B->A, A 被包在最内层。
注意最后这个链条的头是 A,A 是第一个处理请求的中间件,如果 A 的 process_request 没有返回 HttpResponse
就会调用 B,然后是 C ,最后是 D。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def _get_response(self, request):
        """
        Resolve and call the view, then apply view, exception, and
        template_response middleware. This method is everything that happens
        inside the request/response middleware.
        """
        response = None

        if hasattr(request, 'urlconf'):
            urlconf = request.urlconf
            set_urlconf(urlconf)
            resolver = get_resolver(urlconf)
        else:
            resolver = get_resolver()

        resolver_match = resolver.resolve(request.path_info)
        callback, callback_args, callback_kwargs = resolver_match
        request.resolver_match = resolver_match

        # Apply view middleware
        for middleware_method in self._view_middleware:
            response = middleware_method(request, callback, callback_args, callback_kwargs)
            if response:
                break

        if response is None:
            wrapped_callback = self.make_view_atomic(callback)
            try:
                response = wrapped_callback(request, *callback_args, **callback_kwargs)
            except Exception as e:
                response = self.process_exception_by_middleware(e, request)

        # Complain if the view returned None (a common error).
        if response is None:
            if isinstance(callback, types.FunctionType):    # FBV
                view_name = callback.__name__
            else:                                           # CBV
                view_name = callback.__class__.__name__ + '.__call__'

            raise ValueError(
                "The view %s.%s didn't return an HttpResponse object. It "
                "returned None instead." % (callback.__module__, view_name)
            )

        # If the response supports deferred rendering, apply template
        # response middleware and then render the response
        elif hasattr(response, 'render') and callable(response.render):
            for middleware_method in self._template_response_middleware:
                response = middleware_method(request, response)
                # Complain if the template response middleware returned None (a common error).
                if response is None:
                    raise ValueError(
                        "%s.process_template_response didn't return an "
                        "HttpResponse object. It returned None instead."
                        % (middleware_method.__self__.__class__.__name__)
                    )

            try:
                response = response.render()
            except Exception as e:
                response = self.process_exception_by_middleware(e, request)

        return response

    def process_exception_by_middleware(self, exception, request):
        """
        Pass the exception to the exception middleware. If no middleware
        return a response for this exception, raise it.
        """
        for middleware_method in self._exception_middleware:
            response = middleware_method(request, exception)
            if response:
                return response
        raise

这流程中间还会按照此顺序去遍历所有的中间件去处理额外的 'processview','processtemplateresponse'。
先解析 url 得带处理请求的回调和参数信息,然后处理逻辑是 process_view , 外部处理请求的视图 view
process_template_response, 只要一个中间件处理了请求, process_view 返回 response,
那么外部处理请求的 view 就没有机会处理请求,否则最终调用外部 view , 此时外部处理请求的 view 必须返回一个 response,
否则抛异常,然后检查这个响应是否是个延迟渲染的模板响应,如果是的话遍历中间件渲染模板响应,
因为有些需求 view 会延迟渲染模板,而是等中间件处理渲染。最后是倒叙的中间件的 'process
response'一个个处理响应,
最终整个请求响应结束。

_get_response 方法就是真正处理请求的地方,如果整个中间件都没有在 process_request 中返回 response 的话。

所以要想完整得处理 json 响应,光通过 process_exception 是不行的,因为整个中间件处理流程中抛出的异常不会被 process_exception
捕获到。比如说一开始就请求了一个未知的 url,直接返回抛出 http404 异常给异常handler处理了,然而就几个 http 错误可以自定义 handler,
包括 csrf 抛出的异常,但是还是无法保证每个请求返回 json 响应 ,这里还需要 process_response 方法处理这种异常情况,根据情况转换 json
响应。

  • process_response(self, request, response) 处理无法被 process_exception 捕获的异常转 json 响应。

最终的解决方案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class ApiMiddleware(MiddlewareMixin):
    """
    接口抛出的异常转 JsonResponse
    """
    def process_response(self, request, response):
        # 统一处理异常转 json 响应
        # 该方法对应的中间件应该应该处于中间件链的最前
        if not 200 <= response.status_code < 300 and is_json_request(request) \
                and not is_json_response(response):
            response = ApiException(status_code=response.status_code, message=response.reason_phrase).response
        return response

    def process_exception(self, request, exception):
        # 统一处理异常转 json 响应
        # 该方法对应的中间件应该应该处于中间件链的最后
        if is_json_request(request):
            return wrap(exception).response

其实这两个处理应该分两个中间件处理,处理 process_response 的中间件应该是中间件链条的第一个来保证无论什么异常情况都会正确返回 json 响应
处理 process_exception 的中间件应该是中间件链条的最后一个,这样就能第一个处理异常并返回 json 响应。

QuerySet / 模型实例序列化

面向 Api 场景

在写 Api 的时候,通过 ORM 查询数据库返回的 Queryset/Model 实例往往需要转为 json 格式输出后返回给前端。

Django 框架本身内置了 Serializer / Deserializer 服务,将 Queryset 或者Model 实例列表转化为指定的格式输出,支持 JSONXMLYMAL
但是这些序列化/反序列化接口对写 Api 的场景不友好,不是针对 Api 的。
序列化后的输出结果包含 pk(如果该模型有外键关联对象的话),model(模型类名),fields(模型下所有的字段) :

1
2
3
4
5
6
7
8
{
    "pk": 1,
    "model": "xxx",
    "fields": {
        "k": v,
        ...
    }
}

这种肯定不是 api 想要的输出结果,Django 序列化的输出就是为后期反序列化服务的,因此输出才带有一些元信息。

解决方案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AppJSONEncoder(DjangoJSONEncoder):

    def __init__(self, **kwargs):
        self.__call__(**kwargs)

    def __call__(self, serialize_callback=None, **kwargs):
        self.serialize_callback = serialize_callback
        super().__init__(**kwargs)
        return self

    def default(self, o):
        if isinstance(o, (QuerySet, Model)):
            return api_serializer.serialize(o, callback=self.serialize_callback)
        elif isinstance(o, Page):
            # Page 对象输出特定的内容的字典
            return o.to_dict()
        elif isinstance(o, Enum):
            return o.value
        return super().default(o)


# 全局单实例,而不是每次 dump 时创建新的实例
app_json_encoder = AppJSONEncoder()

自定义JSONEncoder

  • 能够调用自定义 Serializer 序列化由 ORM 接口数据库操作后直接返回的 QuerySetModel 实例。同时对于分页时返回的 Page 对象进行自动序列化处理。
  • 继承 DjangoJSONEncoder 为了继承 django 默认的处理其他数据类型的方式,比如 datetimedatetime.datedatetime.timeUUID 等其他数据类型。
  • 重写 __call__ 方法,为了使 AppJSONEncoder 能够全局单例复用,在json.dumps 函数中传递的 cls 参数其实就是这个全局实例对象,只是在json.dumps 内部中 cls(...) 是对这个实例的 call。
  • serialize_callback 参数用于后面自定义 Serializer 时用到的,后面再解释。
1
2
3
4
from django.core.serializers import python

class ApiSerializer(python.Serializer):
    ...

自定义 Serializer:

  • 继承 python serializer ,而不是 json serializer,主要因为在某些情况下需要分部去序列化数据,修改数据结构更加灵活。 比如在查询到某个用户后需要查询该用户下所有发布的文章,可以先将 user 模型实例序列化成 python 字典数据结构,然后再序列化文章列表,再将这个数组插入到 user 字典中,最后再 json.dumps 转化为最终的 json 输出。 如果先 user 转 json 字符串再插入文章列表json字符串就有点尴尬了,难道去字符串拼接操作?这么做不合适,也不 pythonic。
1
def serialize(self, queryset, fields=None, exclude_fields=None, callback=None, **kwargs):

重写父类的 serialize 方法 , 新增 exclude_fields 和 callback 字段

  • exclude_fields 用于指定需要排除序列化的字段,与 fields 参数互斥,fields 参数指定哪些字段需要被序列化。
  • callback

用于更加灵活动态地细粒度控制整个序列化进程。
比如不同接口需要返回的用户信息字段不一样。
博客列表接口中每一条博客所关联的用户信息只需要返回部分用户信息,头像,昵称等基本信息,而不需要返回全部的用户信息,博客点赞列表也一样道理。然而在获取用户信息接口时需要返回更加全面的用户信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def serialize(self, queryset, fields=None, exclude_fields=None, callback=None, **kwargs):
    # 是否为单一模型实例的序列化,
    # 主要因为基类序列化时会把传递的 queryset 参数当作可迭代对象去便利,要么是一个 QuerySet 实例,要么是一个可迭代的模型实例序列。
    # 这里为了简化外部调用序列化接口,默认支持传递单一模型实例,下面的代码会去判断是否为单一模型实例的情况。如果是的话会包装为一个列表传递给基类的 serialize 方法。
    self.single = False
    # 序列化回调函数,动态改变序列化的处理方式
    self.callback = callback

    if fields and exclude_fields:
        raise ValueError('不能同时指定 fields 和 exclude_fields')
    if exclude_fields:
        fields_all = self.get_model(queryset)._meta.get_fields()
        fields = [f.name for f in fields_all if f.name not in exclude_fields]
    elif not fields and isinstance(queryset, QuerySet):
        ...

如果显式传了fields参数,说明调用者完全明确了需要被序列化的字段。
否则如果传入要排除被序列化的字段,就遍历模型所有的字段去一一排除掉生成需要被序列化的字段。

这里获取模型中所有的字段是通过模型元类查询得到,getfields 方法默认会一并返回所有的模型本地声明的字段+继承自父类的字段+反向关系字段,如果存在的话。
模型中声明的 Meta 类对应源码中的 Options 类,源码中的 local
fields 顾名思义只存储了所有在模型中声明的本地字段。

在 fields 和 exclude_fields 都没有传的情况下,下面会去自动从 Queryset.query 中获取一些有用信息用于默认的序列化行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
elif not fields and isinstance(queryset, QuerySet):
    # 如果没有显式地提供 fields 或 exclude_fields,
    # 默认从 queryset.query 中查询出需要加载的字段和关系信息,
    # 为了在 callback 没有提供的情况下序列化那些默认已经从数据库中查询出的字段
    fields = self.get_loaded_field_names(queryset)
    # 所有数据库查询出来的字段, 模型常规字段 + fk many_to_one 中的字段,
    # 不包含被 defer 的字段和 prefetch_related 关联模型中的字段。
    self._queried_fields = fields

    _queried_related_fields = set()
    # select_related  值有以下几种情况。bool 类型数据或者关系字段字典。
    # False 表明没有加载任何关系数据,
    # True 表明加载了所有关系数据,
    # 非空字典说明加载了指定的关系数据。
    # 注意 select_related 指的是那些正向 many_to_one 或者 one_to_one foreignkey 关系
    select_related = queryset.query.select_related
    if select_related:
        # select_related 为 bool 类型并且为 True
        if select_related is True:
            _queried_related_fields = {f.name for f in queryset.model._meta.get_fields()
                                       if f.is_relation and (f.many_to_one or f.one_to_one)}
        else:
            # select_related 为非空关系字段名称字典,查找所有已经加载的关系字段名称
            _queried_related_fields = self.get_loaded_related_field_names(select_related)
    self._queried_related_fields = _queried_related_fields

    # 找出所有 prefetch_related 查询出来的关系字段
    self._queried_prefetch_related_models = []
    _queried_prefetch_related_fields = set()
    all_fields = {f.name: f for f in queryset.model._meta.get_fields()}
    for lookup in queryset._prefetch_related_lookups:
        if isinstance(lookup, Prefetch):
            lookup = lookup.prefetch_to
        for part in lookup.split(LOOKUP_SEP):
            if part in all_fields:
                f = all_fields[part]
                if f.is_relation and (f.many_to_many or f.one_to_many):
                    _queried_prefetch_related_fields.add(part)
            else:
                _queried_prefetch_related_fields.add(part)
    self._queried_prefetch_related_fields = _queried_prefetch_related_fields

真正需要处理的是能够从 ORM 接口查询中获取查询信息。这样就能够跟随 query 的情况用于自动序列化那些已经查询出来的字段,包括关系字段。

即使没有指定 callback 去动态控制序列化,但是从 query 中找到信息是有必要的,这样就能处理默认序列化情况,而不是每次手动去控制,或者分步骤去序列化。

比如博客列表接口,ORM 数据库查询返回一个博客列表 queryset,外部只需一次调用序列化方法就能返回最终的 json 输出。因为是先通过 json.dumps 去先转化为 python 数据结构才转 json 的,自定义的 JSONEncoder 遇到不能被内部自动序列化的对象时会调用 default 返回,这里总是会去再次调用序列化接口。最终会把所有的 Queryset 和 Model 实例给序列化了,貌似这是一个广度优先的递归操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@require_GET
def posts(request):
    page = request.GET.get('page', 1)
    per_page = request.GET.get('per_page', 15)
    data = Post.posts.defer('body', 'author__password', 'author__email', "author__phone_number") \
        .select_related('author') \
        .prefetch_related('author__user_permissions', 'author__groups', 'author__groups__permissions') \
        .paginate(page, per_page, 'posts')

    def serialize(obj, field):
        logger.debug('posts_serialize=' + obj._meta.model_name + ", " + field.attname + ", " + field.name)
        # 判断模型字段是否为关系类型字段
        if field.is_relation:
            pass
            # if field.many_to_one:
            #     return (True, True) if field.name in ('author',) else (False, False)
        else:
            # 处理模型实例常规字段
            if isinstance(obj, User):
                return field.name in ('id', 'username', 'avatar_url', 'is_superuser')

            # elif isinstance(obj, Post):
            #     return field.name in ('created_at', 'title', 'id')

    return json_response(data=data, serialize_callback=serialize)

以上面的 AppJSONEncoder + 分页式博客列表接口的序列化流程为例:
page 对象=>包含分页 object_list QuerySet 实例+分页元数据的字典=>遍历博客列表 QuerySet序列化每个博客模型实例输出博客字典列表 => 单一博客作者用户模型实例=>单一用户组模型实例=>单一用户权限实例=>单一用户组权限实例=>完毕

先将数据全部序列化为 python 数据结构,然后 json.dumps 输出最终的 json 格式。

参考:

Parsing Unsupported Requests (PUT, DELETE, etc.) in Django