想写个前后端分离的项目,需要在数据库中存储非常复杂的 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 模块,初始化项目:
$ 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 管理员后台,各部分功能使用正常:
此时访问 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_framework
和 blogs
两个应用:
...
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 运行效果:
结果爆出 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
:
[
{
"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 项留空,会自动生成):
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
形式)。
// 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)获取对应的数据对象。
由此 CRUD 操作全部可以正常支持。