一文搞懂 MongoEngine 的使用
Synopsis: Flask-MongoEngine 是集成了 MongoEngine 的一个 Flask 插件,并且它可以从 MongoEngine 定义的数据模型中快速生成 WTForms。而 MongoEngine 是一个 Document-Object Mapper (非常类似于 ORM),是 Python 语言的、用来操作 MongoDB 数据库的 DOM 架构
代码已上传到 https://github.com/wangy8961/flask-mongoengine-tutorial ,欢迎 star
1. 安装
如果你在使用 Flask
框架:
Microsoft Windows [版本 10.0.14393] (c) 2016 Microsoft Corporation。保留所有权利。 C:\Users\wangy>D: D:\>cd python-code\flask-mongoengine-tutorial D:\python-code\flask-mongoengine-tutorial>python -m venv venv3 D:\python-code\flask-mongoengine-tutorial>venv3\Scripts\activate (venv3) D:\python-code\flask-mongoengine-tutorial>pip install flask
然后再直接安装 Flask-MongoEngine
这个插件:
它会自动安装如下依赖包:
flask-mongoengine
mongoengine
: DOM 框架pymongo
: 连接 MongoDB 的数据库驱动Flask-WTF
: 集成了 WTForms 包的 Flask 插件,能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery,CSRF)
的攻击WTForms
: 适用于 Python 的灵活的表单验证
和Web表单生成
库six
: Python 2 and 3 compatibility utilities
2. 配置
2.1 连接数据库
from flask import Flask ... app = Flask(__name__) # 配置项 app.config['MONGODB_DB'] = 'test' app.config['MONGODB_HOST'] = '127.0.0.1' app.config['MONGODB_PORT'] = 27017 app.config['MONGODB_USERNAME'] = None app.config['MONGODB_PASSWORD'] = None
表明要连接本机上的 MongoDB(默认端口: 27017
),数据库名为 test
,且数据库没有开启用户认证
2.2 初始化 Flask-MongoEngine 插件
3. 定义数据模型
创建数据模型文件 models.py
:
from datetime import datetime import random from mongoengine.queryset.manager import queryset_manager from slugify import slugify from app import db class User(db.DynamicDocument): # required=True 表明此字段必须填写 email = db.StringField(required=True) # max_length 表明字符串字段的最大长度 first_name = db.StringField(max_length=50) last_name = db.StringField(max_length=50) def __str__(self): # 建议: 给每个模型增加 __str__ 方法,它返回一个具有可读性的字符串表示模型,可在调试和测试时使用 return self.first_name + ' ' + self.last_name class Category(db.Document): name = db.StringField(max_length=64, required=True) # unique=True 表明此字段的值必须在整个 collection 中唯一 slug = db.StringField(max_length=64, unique=True) caption = db.StringField() # 自引用 parent = db.ReferenceField('self', reverse_delete_rule=db.DENY) meta = { 'collection': 'classes', # 更改数据库中默认的 collection 名称 'indexes': ['parent'], 'ordering': ['slug'] } def __str__(self): return self.name class Tag(db.Document): name = db.StringField(max_length=64, required=True) slug = db.StringField(max_length=64, unique=True) def __str__(self): return self.name class Comment(db.EmbeddedDocument): '''EmbeddedDocument 表明它是一个可被嵌入其它文档的对象,比如嵌套到下面的 Post 中''' content = db.StringField() name = db.StringField(max_length=120) def __str__(self): return self.name class Post(db.Document): title = db.StringField(max_length=120, required=True) slug = db.StringField(max_length=64, unique=True) # ReferenceField 是引用字段,在数据库中真正存储的是 ObjectID # reverse_delete_rule=db.CASCADE 定义了,author 这个引用字段当被引用的对象删除时,它如何处理 # 比如 author 引用了用户 ross,当我们删除 ross 时,由于定义了 db.CASCADE,所以会 [级联删除] ross 用户所发表过的所有 Post 文章 author = db.ReferenceField(User, reverse_delete_rule=db.CASCADE) category = db.ReferenceField(Category, reverse_delete_rule=db.NULLIFY) # ListField 表明它是一个列表,可以保存多个其它类型的字段值,比如 StringField、ReferenceField、EmbeddedDocumentField 都可以 tags = db.ListField(db.ReferenceField(Tag, reverse_delete_rule=db.PULL)) comments = db.ListField(db.EmbeddedDocumentField(Comment)) # 创建的时间,建议在数据库中全部存储 UTC 时间 # default=datetime.utcnow 表明不指定此字段的值时,它会默认保存当前的时间 created_at = db.DateTimeField(default=datetime.utcnow) # 价格。需要数学计算时,请使用 DecimalField,不要用 FloatField (计算结果不对) price = db.DecimalField(default='0.00') # 是否发布 published = db.BooleanField(default=True) meta = { 'allow_inheritance': True, # 允许被继承,比如下面的 TextPost 就继承自 Post 'indexes': ['title', 'author'], # 索引字段,后续按这两个字段值查询时可以加快速度 'ordering': ['-created_at'] # 表示按 created_at 降序排列,没有减号表示升序排列 } @queryset_manager def live_posts(doc_cls, queryset): '''非默认的objects查询集,此查询集只返回发布状态为True的博客文章''' return queryset.filter(published=True) def clean(self): ''' MongoEngine allows you to create custom cleaning rules for your documents when calling save(). By providing a custom clean() method you can do any pre validation / data cleaning. ''' # 如果创建Post对象时没有提供slug,则根据title自动生成;如果提供了slug,用slugify再清理 if self.slug: self.slug = slugify(self.slug) else: self.slug = slugify(self.title) # 判断slug是否唯一 filters = dict(slug=self.slug) if self.id: filters['id__ne'] = self.id # 不能用 exist = self.__class__.objects(**filters),因为可能创建 TextPost 对象时,其它子类有相同的 slug exist = Post.objects(**filters) if exist.count(): self.slug = "{0}-{1}".format(self.slug, random.getrandbits(32)) def __str__(self): return self.title class TextPost(Post): # 继承自 Post 模型,所以它也有 title、author 等字段 content = db.StringField() class ImagePost(Post): image_path = db.StringField() class LinkPost(Post): link_url = db.StringField()
3.1 文档模型
每个 数据模型类
一般会继承自 db.Document
,类里面再定义一些类似 db.StringField()
、db.DateTimeField()
、db.DecimalField()
、db.ReferenceField()
、db.ListField()
等字段
(1)嵌入型
如果 数据模型类
继承自 db.EmbeddedDocument
,那么这个模型类就可以被嵌入到其它模型中,同时这个嵌入型文档不会在数据库中有 collection,且它的对象没有 save()
方法
class Comment(db.EmbeddedDocument): content = db.StringField() ... class Post(db.Document): ... comments = db.ListField(db.EmbeddedDocumentField(Comment))
现在让我们插入一些数据,使用方法见后续的第 4 节 [插入数据]
(venv3) D:\python-code\flask-mongoengine-tutorial>flask shell Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32 App: app [development] Instance: D:\python-code\flask-mongoengine-tutorial\instance >>> comment1 = Comment(content='Good work!') # 如果调用 comment1.save() 会报错 >>> comment2 = Comment(content='Nice article!') >>> >>> post1 = Post(title='The post has many comments', slug='test-post') >>> post1.comments = [comment1, comment2] >>> post1.save() <Post: The post has many comments>
此时,数据库中只有 post
集合
说明: 像文章下面的评论只属于该文章,就非常适合将评论定义为
嵌入型文档
,而文章的标签或分类,因为标签或分类可以多篇文章共用,所以适合用下面将要描述的引用字段(db.ReferenceField)
!
(2)动态型
模型继承 db.Document
的话,如果在模型定义中没有指定的字段,在程序运行中动态添加额外的字段和值,却无法保存到数据库中,比如下面 User
模型没有 age
字段,就算给它赋值了,在数据库中 alice
对象还是看不到 age
字段和值
(venv3) D:\python-code\flask-mongoengine-tutorial>flask shell Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32 App: app [development] Instance: D:\python-code\flask-mongoengine-tutorial\instance >>> alice = User.objects.get(first_name='Alice') >>> alice.age Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: 'User' object has no attribute 'age' >>> alice.age = 18 >>> alice.save() <User: Alice Haby> >>> alice.age 18 >>> exit() (venv3) D:\python-code\flask-mongoengine-tutorial>flask shell Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32 App: app [development] Instance: D:\python-code\flask-mongoengine-tutorial\instance >>> alice = User.objects.get(first_name='Alice') >>> alice.age Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: 'User' object has no attribute 'age'
如果我们使用 db.DynamicDocument
替代 db.Document
的话,模型的对象可以动态增加没有定义过的字段,比如现在 User 模型如下
class User(db.DynamicDocument): # required=True 表明此字段必须填写 email = db.StringField(required=True) # max_length 表明字符串字段的最大长度 first_name = db.StringField(max_length=50) last_name = db.StringField(max_length=50)
虽然还是没有定义 age
字段,但重复上面 Flask Shell
的执行过程后,数据库中 alice
对象有 age = 18
(venv3) D:\python-code\flask-mongoengine-tutorial>flask shell Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32 App: app [development] Instance: D:\python-code\flask-mongoengine-tutorial\instance >>> alice = User.objects.get(first_name='Alice') >>> alice.age Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: 'User' object has no attribute 'age' >>> alice.age = 18 >>> alice.save() <User: Alice Haby> >>> alice.age = 18 >>> exit() (venv3) D:\python-code\flask-mongoengine-tutorial>flask shell Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32 App: app [development] Instance: D:\python-code\flask-mongoengine-tutorial\instance >>> alice = User.objects.get(first_name='Alice') >>> alice.age 18
3.2 字段
请参考: http://docs.mongoengine.org/guide/defining-documents.html#fields
需要注意的是,每种字段类型都会有一些共同的参数:
db_field
: (Default: None) The MongoDB field name.primary_key
: (Default: False) 如果设置为 True,则该字段是文档的主键
,还可以通过pk
来访问此字段required
: (Default: False) 字段是否必填,如果指定了required=True
时,而创建文档对象时没有给该字段赋值的话,会抛出ValidationError
default
: (Default: None) A value to use when no value is set for this field.unique
: (Default: False) When True, no documents in the collection will have the same value for this field.choices
: (Default: None) An iterable (e.g. list, tuple or set) of choices to which the value of this field should be limited.
class Payment(db.Document): ... pay_type = db.StringField( choices=(('alipay', '支付宝'), ('wechat', '微信'), ('qqpay', 'QQ钱包')), default=
5 条评论
评论者的用户名
评论时间321坤-TOO
2019-08-15T05:17:29Z666666666666666666666666,请问作者,文章买了会过期吗?永久会员在哪买,咋没看到咧
Madman 321坤-TOO Author
2019-08-15T05:21:05Z不会过期,只要我还运营此网站就能看,当然读者的支持是我继续下去的动力!会员的权限可以联系我微信 wangy8961 了解详情
321坤-TOO
2019-08-15T05:19:32Z666666,请问作者,文章买了有期限的吗?还有永久会员在哪啊。没找到咧~
zhouboboya
2019-11-10T13:22:02Z'indexes': ['title', 'author'], # 索引字段,后续按这两个字段值查询时可以加快速度 这个索引是使用的过程中自建的索引吗?我是数据库小白
Madman zhouboboya Author
2019-11-10T13:34:41Z不需要你自己建,由 mongoengine 完成