Django REST framework 使用 MongoDB 作为数据库后端
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 作为数据库后端
想写个前后端分离的项目,需要在数据库中存储非常复杂的 JSON 格式(包含多层嵌套)的数据,又不想将 JSON 数据转为文本后以 Text 的格式存到 Mysql 数据库中。
因此想尝试下文档型数据库 MongoDB,其用来存放数据的文档结构,本身就是非常类似 JSON 对象的 BSON(Binary JSON)。
但 Django 的官方版本目前还未支持 NoSQL 数据库(参考 FAQ),MongoDB 官方文档建议借助 Djongo 组件完成到原生 Django ORM 的对接。
Djongo 实际上是一个 SQL 到 MongoDB 的翻译器。通过 Django 的 admin
应用可以向 MongoDB 中添加或修改文档,其他 Django 模块如 contrib
、auth
、session
等也可以在不做任何改动的情况下正常使用。
项目初始化
安装需要用到的 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 管理员后台,各部分功能使用正常:
此时访问 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_framework
和 blogs
两个应用:
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 运行效果:
结果爆出 TypeError
错误(int() argument must be a string, a bytes-like object or a number, not 'ObjectId'
):
重新访问 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 项留空,会自动生成):
Retrieve
上述实现仍有部分问题,实际上只有新值数据(Create)和获取数据列表(List)能够正常运行。而 CRUD 中的 Retrieve、Update、Delete 都会报出 404 错误。即无法通过 _id 获取对应的数据对象。
比如访问 http://127.0.0.1:8000/blog/5fc0b18e60870125f0ed846d/ :
原因是 MongoDB 中的 _id
是 OjbectId 类型,与 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)获取对应的数据对象。
由此 CRUD 操作全部可以正常支持。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK