17

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

 3 years ago
source link: https://rollingstarky.github.io/2020/11/27/python-django-rest-framework-and-mongodb/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

2020-11-27

| Python

| 0

|

9k

|

0:09

想写个前后端分离的项目,需要在数据库中存储非常复杂的 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 模块,初始化项目:

1
2
3
4
$ pip install djongo djangorestframework
$ django-admin startproject mongo_test
$ cd mongo_test
$ django-admin startapp blogs

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

1
2
3
4
5
6
7
8
...
DATABASES = {
'default': {
'ENGINE': 'djongo',
'NAME': 'mongo_test',
}
}
...

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

1
2
3
$ 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 数据库,可以查询到存入的数据:

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
// 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 两个应用:

1
2
3
4
5
6
7
8
9
10
11
12
...
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 文件,创建数据库模型,内容如下:

1
2
3
4
5
6
7
8
9
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 文件,内容如下:

1
2
3
4
5
6
7
8
from blogs.models import Blog
from rest_framework.serializers import ModelSerializer


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

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

1
2
3
4
5
6
7
8
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 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
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,内容如下:

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
[
{
"id": null,
"title": "Blog",
"content": "This is a TEST Blog"
}
]

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

ObjectId

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

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

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

1
2
3
4
5
6
7
8
9
10
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 形式)。

1
2
3
4
5
// 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()

1
2
3
4
5
6
7
8
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 类中实现的,其代码为:

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
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

其中最关键的两句为:

1
2
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 文件,内容如下:

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
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

最主要的改动即:

1
2
3
4
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 改为如下版本:

1
2
3
4
5
6
7
8
9
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 操作全部可以正常支持。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK