想写个前后端分离的项目,需要在数据库中存储非常复杂的 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 |
$ pip install djongo djangorestframework $ django-admin startproject mongo_test $ cd mongo_test $ django-admin startapp blogs |
修改项目配置文件(mongo_test/settings.py
),添加数据库配置:
1 5 |
... DATABASES = { 'default': { 'ENGINE': 'djongo', 'NAME': 'mongo_test', } } ... |
数据库迁移,创建管理员账户,运行 WEB 服务:
1 |
$ 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 5 9 13 17 21 25 29 33 |
// 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") } |
在配置文件 mongo_test/settings.py
中的 INSTALLED_APPS
配置项下添加 rest_framework
和 blogs
两个应用:
1 5 9 |
... INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'blogs' ] ... |
编辑 blogs/models.py
文件,创建数据库模型,内容如下:
1 5 9 |
from djongo import models class Blog(models.Model): title = models.CharField(max_length=50) content = models.TextField() class Meta: db_table = 'mongo_blog' |
创建 blogs/serializers.py
文件,内容如下:
1 5 |
from blogs.models import Blog from rest_framework.serializers import ModelSerializer class BlogSerializer(ModelSerializer): class Meta: model = Blog fields = '__all__' |
编辑 blogs/views.py
文件,内容如下:
1 5 |
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 |
创建 blogs/urls.py
文件,内容如下:
1 5 9 |
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 5 |
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 5 |
[ { "id": null, "title": "Blog", "content": "This is a TEST Blog" } ] |
导致基于 REST API 的 CRUD 操作都是不能正常执行的。
实际上按照上述方式存入数据库的数据是以下格式:
1 5 |
// mongo shell > db.mongo_blog.findOne() { "_id" : ObjectId("5fc0ae2ea7795c8c4ddae815"), "title" : "Blog", "content" : "This is a TEST Blog" } |
修改数据库模型(blogs/models.py
),令其包含 _id
字段:
1 5 9 |
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 项留空,会自动生成):
上述实现仍有部分问题,实际上只有新值数据(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 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
的源代码,发现后台对 Retrieve 操作的响应逻辑是由mixinx.RetrieveModelMixin
类实现的,其中获取某个特定对象的函数是 self.get_object()
:
1 5 |
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 5 9 13 17 21 25 |
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 |
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 会以怎样的方式和条件检索某个对象。
综上,为了让 CURD 操作中的 URD 能够通过 _id
(ObjectId)检索获取特定对象,可以实现自己的 ModelViewSet 类,重写 get_object()
方法。
新建 blogs/mongo_viewset.py
文件,内容如下:
1 5 9 13 17 21 25 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 |
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 5 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 操作全部可以正常支持。
[Django REST framework 使用 MongoDB 作为数据库后端 | StarryLand](https://www.starky.ltd/2020/11/27/python-django-rest-framework-and-mongodb/ )]