Django REST framework 使用 MongoDB 作为数据库后端

想写个前后端分离的项目,需要在数据库中存储非常复杂的 JSON 格式(包含多层嵌套)的数据,又不想将 JSON 数据转为文本后以 Text 的格式存到 Mysql 数据库中。

因此想尝试下文档型数据库 MongoDB,其用来存放数据的文档结构,本身就是非常类似 JSON 对象的 BSON(Binary JSON)。

但 Django 的官方版本目前还未支持 NoSQL 数据库(参考 FAQ),MongoDB 官方文档建议借助 Djongo 组件完成到原生 Django ORM 的对接。
Djongo 实际上是一个 SQL 到 MongoDB 的翻译器。通过 Django 的 admin 应用可以向 MongoDB 中添加或修改文档,其他 Django 模块如 contribauthsession 等也可以在不做任何改动的情况下正常使用。

项目初始化

安装需要用到的 Python 模块,初始化项目:

$ pip install djongo djangorestframework
$ django-admin startproject mongo_test
$ cd mongo_test
$ django-admin startapp blogs

修改项目配置文件(mongo_test/settings.py),添加数据库配置:

...
DATABASES = {
    'default': {
        'ENGINE': 'djongo',
        'NAME': 'mongo_test',
    }
}
...

数据库迁移,创建管理员账户,运行 WEB 服务:

$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver 0.0.0.0:8000

访问 http://127.0.0.1:8000/admin ,进入 Django 管理员后台,各部分功能使用正常:

Django Admin

此时访问 MongoDB 数据库,可以查询到存入的数据:

// mongo shell
> show dbs
admin        0.000GB
apscheduler  0.000GB
config       0.000GB
local        0.000GB
mongo_test   0.000GB
> use mongo_test
switched to db mongo_test
> show collections;
__schema__
auth_group
auth_group_permissions
auth_permission
auth_user
auth_user_groups
auth_user_user_permissions
django_admin_log
django_content_type
django_migrations
django_session
> db.auth_user.find().pretty()
{
        "_id" : ObjectId("5fc0a6a4e7b96c382fa9ccd8"),
        "id" : 1,
        "password" : "pbkdf2_sha256$180000$XL0v3lLCM1RW$rnw4qzoTUtwgc5EoKfB4yaaVEu1jTid8yuBVl0Y6P5Q=",
        "last_login" : ISODate("2020-11-27T07:11:55.492Z"),
        "is_superuser" : true,
        "username" : "admin",
        "first_name" : "",
        "last_name" : "",
        "email" : "",
        "is_staff" : true,
        "is_active" : true,
        "date_joined" : ISODate("2020-11-27T07:11:31.955Z")
}

Django REST framework

在配置文件 mongo_test/settings.py 中的 INSTALLED_APPS 配置项下添加 rest_frameworkblogs 两个应用:

...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'blogs'
]
...
数据库模型(Models)

编辑 blogs/models.py 文件,创建数据库模型,内容如下:

from djongo import models


class Blog(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()

    class Meta:
        db_table = 'mongo_blog'
序列化器(Serializers)

创建 blogs/serializers.py 文件,内容如下:

from blogs.models import Blog
from rest_framework.serializers import ModelSerializer


class BlogSerializer(ModelSerializer):
    class Meta:
        model = Blog
        fields = '__all__'
视图(Views)

编辑 blogs/views.py 文件,内容如下:

from blogs.models import Blog
from blogs.serializers import BlogSerializer
from rest_framework.viewsets import ModelViewSet


class BlogViewSet(ModelViewSet):
    queryset = Blog.objects.all()
    serializer_class = BlogSerializer
路由(URLs)

创建 blogs/urls.py 文件,内容如下:

from django.urls import include, path
from rest_framework import routers
from blogs import views

router = routers.DefaultRouter()
router.register(r'blog', views.BlogViewSet)

urlpatterns = [
    path('', include(router.urls))
]

根路由

编辑项目路由配置文件 mongo_test/urls.py,内容如下:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blogs.urls')),
]

访问 http://127.0.0.1/blog ,利用 POST 方法新增数据以测试 REST API 运行效果:

REST API

结果爆出 TypeError 错误(int() argument must be a string, a bytes-like object or a number, not 'ObjectId'):

TypeError

重新访问 http://127.0.0.1:8000/blog ,发现新增的数据已添加到数据库中,只是 id 项为 null

[
    {
        "id": null,
        "title": "Blog",
        "content": "This is a TEST Blog"
    }
]

导致基于 REST API 的 CRUD 操作都是不能正常执行的。

ObjectId

实际上按照上述方式存入数据库的数据是以下格式:

// mongo shell
> db.mongo_blog.findOne()
{
        "_id" : ObjectId("5fc0ae2ea7795c8c4ddae815"),
        "title" : "Blog",
        "content" : "This is a TEST Blog"
}

修改数据库模型(blogs/models.py),令其包含 _id 字段:

from djongo import models


class Blog(models.Model):
    _id = models.ObjectIdField()
    title = models.CharField(max_length=50)
    content = models.TextField()

    class Meta:
        db_table = 'mongo_blog'

刷新 http://127.0.0.1:8000/blog 页面,此时数据显示正常,也可以通过 POST 方法正常添加数据(_id 项留空,会自动生成):

POST

POST 结果

Retrieve

上述实现仍有部分问题,实际上只有新值数据(Create)和获取数据列表(List)能够正常运行。而 CRUD 中的 Retrieve、Update、Delete 都会报出 404 错误。即无法通过 _id 获取对应的数据对象。

比如访问 http://127.0.0.1:8000/blog/5fc0b18e60870125f0ed846d/ :

Retrieve

原因是 MongoDB 中的 _idOjbectId 类型,与 Django REST framework 用于检索的 _id 类型不一致,导致无法通过 _id 找到对应的对象。需要在中间做一步转换工作(将字符串形式的 _id 转换为 ObjectId 形式)。

// mongo shell
> db.mongo_blog.find({"_id": "5fc0b18e60870125f0ed846d"})
>
> db.mongo_blog.find({"_id": ObjectId("5fc0b18e60870125f0ed846d")})
{ "_id" : ObjectId("5fc0b18e60870125f0ed846d"), "title" : "Blog2", "content" : "This is another Blog" }

查看 ModelViewSet 源代码

通过查看 ModelViewSet 的源代码,发现后台对 Retrieve 操作的响应逻辑是由mixinx.RetrieveModelMixin 类实现的,其中获取某个特定对象的函数是 self.get_object()

class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

进一步查找,发现 get_object() 函数是在 generics.GenericAPIVie 类中实现的,其代码为:

class GenericAPIView(views.APIView):
    def get_object(self):
        """
        Returns the object the view is displaying.

        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj

其中最关键的两句为:

filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)

{self.lookup_field: self.kwargs[lookup_url_kwarg]} 决定了最终 MongoDB 会以怎样的方式和条件检索某个对象。

实现自己的 ModelViewSet

综上,为了让 CURD 操作中的 URD 能够通过 _id(ObjectId)检索获取特定对象,可以实现自己的 ModelViewSet 类,重写 get_object() 方法。

新建 blogs/mongo_viewset.py 文件,内容如下:

from bson import ObjectId
from django.shortcuts import get_object_or_404
from rest_framework.viewsets import ModelViewSet


class MongoModelViewSet(ModelViewSet):
    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        if self.lookup_field == '_id':
            filter_kwargs = {self.lookup_field: ObjectId(self.kwargs[self.lookup_field])}
        else:
            filter_kwargs = {self.lookup_field: self.kwargs[self.lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj

最主要的改动即:

if self.lookup_field == '_id':
    filter_kwargs = {self.lookup_field: ObjectId(self.kwargs[self.lookup_field])}
else:
    filter_kwargs = {self.lookup_field: self.kwargs[self.lookup_url_kwarg]}

视图代码 blogs/views.py 改为如下版本:

from blogs.models import Blog
from blogs.serializers import BlogSerializer
from blogs.mongo_viewset import MongoModelViewSet


class BlogViewSet(MongoModelViewSet):
    queryset = Blog.objects.all()
    serializer_class = BlogSerializer
    lookup_field = '_id'

此时访问 http://172.20.23.34:8000/blog/5fc0b18e60870125f0ed846d/ 即可正常显示,即能够通过 _id(ObjectId)获取对应的数据对象。

Retrieve

由此 CRUD 操作全部可以正常支持。

你可能感兴趣的:(Django REST framework 使用 MongoDB 作为数据库后端)