中国山水画 發表於 2020-8-15 20:32:00

在Flask中使用MongoDB:Flask-MongoEngine

<p>在Flask中使用MongoEngine,需要通过Flask-MongoEngine包来对MongoEngine进行配置。Flask-MongoEngine是MongoEngine的Flask封装,针对Flask对MongoEngine做出了一些拓展,而MongoEngine则是在PyMongo的基础上构建的一个类似于SQLAlchemy的对象文档映射器(Object-Document Mapper, ODM),为用户提供基本的数据模型以及类型约束,并对PyMongo的数据查询做了进一步的封装,避免直接书写MongoDB查询语句,简化数据查询。</p>
<p>PyMongo是MongoDB官方提供的Python库,将MongoDB CLI中JS风格的查询语句在Python中进行了封装,用户仍然可以在Python中采用和MongoDB CLI中相同的方法操作MongoDB数据库。但PyMongo本身缺乏类型约束、值校验、数据模型等特性支持,每一次的查询操作都需要直接和字典进行交互。</p>
<p>Flask-MongoEngine对MongoEngine做了以下拓展:</p>
<ol>
<li>从<code>MONGODB_XXX</code>格式变量中读取MongoEngine的配置信息,并自动建立连接。</li>
<li>对MongoEngine的查询<code>QuerySet</code>进行了拓展,以支持<code>get_or_404</code>,<code>first_or_404</code>,<code>paginate</code>,<code>paginate_field</code>。</li>
<li>支持直接从MongoEngine模型中生成WTForms表单。</li>
<li>支持将MongoEngine作为session存储后端。</li>
<li>为<code>flask_debugtoolbar</code>提供MongoEngine查询跟踪。</li>
<li>通过<code>app.json_encoder</code>对Flask默认的JSON编码器进行拓展,添加了<code>BaseDocument</code>和<code>QuerySet</code>两个类型的序列化支持。</li>
</ol>
<h1 id="资源">资源</h1>
<table>
<thead>
<tr>
<th>Item</th>
<th>Repository</th>
<th>Documents</th>
</tr>
</thead>
<tbody>
<tr>
<td>MongoEngine</td>
<td>https://github.com/MongoEngine/mongoengine</td>
<td>https://mongoengine-odm.readthedocs.io/</td>
</tr>
<tr>
<td>Flask-MongoEngine</td>
<td>https://github.com/MongoEngine/flask-mongoengine</td>
<td>https://flask-mongoengine.readthedocs.io</td>
</tr>
</tbody>
</table>
<h1 id="快速开始">快速开始</h1>
<pre><code class="language-py">from flask import Flask
from flask_mongoengine import MongoEngine

app = Flask(__name__)

# 通过MONGODB_SETTINGS配置MongoEngine
app.config.from_mapping({
    MONGODB_SETTINGS = {
      'db': 'test',
      'host': 'localhost',
      'port': 27017,
      'connect': True,
      'username': 'test',
      'password': '123456',
      'authentication_source': 'admin'
    }
})

# 初始化 MongoEngine
db = MongoEngine(app)

# 定义一个文档 User,MongoEngine 会自动在数据库中创建一个名为 user 的集合
class User(db.Document):
    email = db.StringField(required=True)
    username = db.StringField(required=True, max_length=128, unique=True)

    def __repr__(self):
      return 'User(email="{}", username="{}")'.format(self.username, self.password)

# 创建一个User文档实例并存储
user = User(email="xxx@hotmail.com", username="kikyou", password="123456")
user.save()

# 通过 objects 属性访问集合中的所有文档
# objects 是一个 QuerySet 实例
for user in User.objects[: 5]:
    print(user)

# 通过 username 查找用户,并更新 email
user = User.objects(username='kikyou').first()
user.email = '_kikyou_@kikyou.com'
user.save()
</code></pre>
<h1 id="flask-mongonengine配置">Flask-MongonEngine配置</h1>
<p>Flask-MongonEngine通过<code>MONGODB_XXX</code>格式的配置变量来配置MongoEngine。其支持通过<code>MONGODB_SETTINGS</code>来设置MongoEngine的整个配置,也支持通过<code>MONGODB_DB</code>等来直接设置<code>MONGODB_SETTINGS</code>下的每个子属性。需要注意的是,<strong>只要设置了<code>MONGODB_SETTINGS</code>变量,其余的<code>MONGODB_XXX</code>变量将被忽略</strong>。</p>
<h2 id="通过mongodb_settings配置mongoengine">通过MONGODB_SETTINGS配置MongoEngine</h2>
<p>MONGODB_SETTINGS变量是一个字典,其对应着mongoengine中connect函数支持的所有关键字。一旦该变量被设置,其他基于MONGODB_XXX格式指定的mongoengine配置信息将被忽略。</p>
<pre><code class="language-py">MONGODB_SETTINGS = {
    'db': 'appdb',
    'host': 'localhost',
    'port': 27017,
    'connect': True,
    'username': 'test',
    'password': '123456',
}
</code></pre>
<p>完整的参数列表参考<code>mongoengine.connection.register_connection</code>以及<code>pymongo.mongo_client.MongoClient</code>。</p>
<ul>
<li><code>alias</code> 建立的数据库连接的别名,默认为<code>default</code>,通过alias机制可以同时访问多个MongoDB数据库。</li>
<li><code>db</code> 将要访问的数据库名称,默认为<code>test</code></li>
<li><code>host</code> MongoDB服务器地址,默认为<code>localhost</code></li>
<li><code>port</code> MongoDB服务器端口,默认为<code>27017</code></li>
<li><code>username</code> 用户名</li>
<li><code>password</code> 用户密码</li>
<li><code>authentication_source</code> 认证源,创建该用户的数据库</li>
<li><code>authentication_mechanism</code> 认证机制,不需要设置</li>
<li><code>is_mock</code> 是否使用 mongomock</li>
<li><code>connect</code> 是否直接连接服务器,如果为false,则直到第一次操作时才会连接服务器</li>
<li><code>tz_aware</code> 是否自动识别时区,如果为false,则直接使用本地时间,忽略datetime的时区配置</li>
</ul>
<h2 id="通过mongodb_xxx配置mongoengine">通过MONGODB_XXX配置MongoEngine</h2>
<p>除了通过MONGODB_SETTTINGS直接配置connect的参数外,还可以通过MONGODB_XXX的形式直接指定参数的值,其中XXX对应MONGODB_SETTTINGS中关键字的大写形式,但目前只支持有限关键字:</p>
<ul>
<li><code>MONGODB_ALIAS</code></li>
<li><code>MONGODB_DB</code></li>
<li><code>MONGODB_HOST</code></li>
<li><code>MONGODB_IS_MOCK</code></li>
<li><code>MONGODB_PASSWORD</code></li>
<li><code>MONGODB_PORT</code></li>
<li><code>MONGODB_USERNAME</code></li>
<li><code>MONGODB_CONNECT</code></li>
<li><code>MONGODB_TZ_AWARE</code></li>
</ul>
<h2 id="flask-mongoengine初始化">Flask-MongoEngine初始化</h2>
<p>除了直接使用<code>db = MongoEngine(app)</code>进行初始化外,Flask-MongoEngine也像其他Flask扩展一样支持将定义和初始化分离。</p>
<pre><code class="language-py"># project_dir/__init__.py

from flask import Flask
from flask_mongoengine import MongoEngine

# 定义 MongoEngine
db = MongoEngine()

def create_app():
    app = Flask(__name__)

    # 通过MONGODB_SETTINGS配置MongoEngine
    app.config.from_mapping({
      MONGODB_SETTINGS = {
            'db': 'test',
            'host': 'localhost',
            'port': 27017,
            'connect': True,
            'username': 'test',
            'password': '123456',
            'authentication_source': 'admin'
      }
    })

    db.init(app)
    import .models

    return app
</code></pre>
<pre><code class="language-py"># project_dir/models.py

class User(db.Document):
    email = db.StringField(required=True)
    username = db.StringField(required=True, max_length=128, unique=True)

    def __repr__(self):
      return 'User(email="{}", username="{}")'.format(self.username, self.password)
</code></pre>
<h2 id="flask-mongoengine-qeuryset-扩展">Flask-MongoEngine QeurySet 扩展</h2>
<p>Flask-MongoEngine 对 MongoEngine的查询<code>QuerySet</code>进行了拓展,以支持<code>get_or_404</code>,<code>first_or_404</code>,<code>paginate</code>,<code>paginate_field</code>。</p>
<ul>
<li><code>get_or_404</code> 与<code>QuerySet.get</code>类似,如果不存在或有两个及以上的匹配项,则会抛出404异常</li>
<li><code>first_or_404</code> 与<code>QuerySet.first</code>类似,如果不存在,则会抛出404异常</li>
<li><code>paginate</code> 对QuerySet结果进行分页,接受两个参数:<code>page</code>和<code>per_page</code>,返回一个Pagination对象。</li>
<li><code>paginate_field(field_name, doc_id, page, per_page, total=None)</code> 与<code>paginate</code>类似,对文档的数组类型字段进行分页。这个方法会首先尝试获取“字段名+count”形式的值,如<code>posts_count</code>,作为posts的总数,如果不存在则会获取整个文档,然后计算数组类型字段的长度。<strong>不建议使用!</strong></li>
</ul>
<pre><code class="language-py">paged = User.objects.paginate(page=1, per_page=10)
</code></pre>
<p><code>Pagination</code>支持的属性包括:</p>
<ul>
<li><code>total</code> 文档总数</li>
<li><code>pages</code> 总页数</li>
<li><code>page</code> 当前页数</li>
<li><code>has_prev</code> 是否有前一页</li>
<li><code>has_next</code> 是否有后一页</li>
<li><code>pre_num</code> 上一页页码</li>
<li><code>next_num</code> 下一页页码</li>
<li><code>items</code> 可迭代文档集合</li>
</ul>
<p>此外,<code>Pagination</code>还支持一个<code>iter_pages</code>方法,用于为分页器生成编号,被跳过的页码使用<code>None</code>表示。</p>
<pre><code class="language-py">class Pagination(object):
    ...
    def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2):
      ...
    ...
</code></pre>
<p>{% raw%}</p>
<pre><code class="language-html">{# Display a page of todos #}
&lt;ul&gt;
    {% for todo in paginated_todos.items %}
      &lt;li&gt;{{ todo.title }}&lt;/li&gt;
    {% endfor %}
&lt;/ul&gt;

{# Macro for creating navigation links #}
{% macro render_navigation(pagination, endpoint) %}
&lt;div class=pagination&gt;
{% for page in pagination.iter_pages() %}
    {% if page %}
      {% if page != pagination.page %}
      &lt;a href="{{ url_for(endpoint, page=page) }}"&gt;{{ page }}&lt;/a&gt;
      {% else %}
      &lt;strong&gt;{{ page }}&lt;/strong&gt;
      {% endif %}
    {% else %}
      &lt;span class=ellipsis&gt;…&lt;/span&gt;
    {% endif %}
{% endfor %}
&lt;/div&gt;
{% endmacro %}

{{ render_navigation(paginated_todos, 'view_todos') }}
</code></pre>
<p>{% endraw %}</p>
<h1 id="额外的拓展">额外的拓展</h1>
<h2 id="为flask-mongoengine添加额外的json序列化类型支持">为Flask-MongoEngine添加额外的JSON序列化类型支持</h2>
<p>Flask-MongoEngine默认为Flask添加了<code>BaseDocument</code>和<code>QuerySet</code>类型的序列化支持,但对于常见的<code>ObjectId</code>以及<code>Datetime</code>数据类型缺乏支持。</p>
<pre><code class="language-py">from flask import Flask
from flask_mongoengine import MongoEngine

def override_json_encoder(app: Flask):
    from bson import ObjectId
    from datetime import date

    superclass = app.json_encoder

    class _JsonEncoder(superclass):
      def default(self, o):
            if isinstance(o, ObjectId):
                return str(o)
            if isinstance(o, date):
                return o.isoformat()
            return superclass.default(self, o)

    app.json_encoder = _JsonEncoder


def create_app()
    app = Flask(__name__)

    # 通过MONGODB_SETTINGS配置MongoEngine
    app.config.from_mapping({
      MONGODB_SETTINGS = {
            'db': 'test',
            'host': 'localhost',
            'port': 27017,
            'connect': True,
            'username': 'test',
            'password': '123456',
            'authentication_source': 'admin'
      }
    })

    override_json_encoder(app)

    db.init(app)
    import .models

    return app
</code></pre>
<p>注:<code>datetime</code>是<code>date</code>的子类。</p>
<h1 id="qa">Q&amp;A</h1>
<h2 id="编辑器pylint提示instance-of-mongoengine-has-no-stringfield-member">编辑器/pylint提示“Instance of 'MongoEngine' has no 'StringField' member”</h2>
<p>Flask-MongoEngine在初始化时,为MongoEngine类通过<code>_include_mongoengine</code>方法动态注入<code>mongoengine</code>和<code>mongoengine.fields</code>的所有属性,但pylint在进行静态检测时,无法处理动态注入的属性。</p>
<pre><code class="language-py">def _include_mongoengine(obj):
    """
    Copy all of the attributes from mongoengine and mongoengine.fields
    onto obj (which should be an instance of the MongoEngine class).
    """
    for module in (mongoengine, mongoengine.fields):
      for attr_name in module.__all__:
            if not hasattr(obj, attr_name):
                setattr(obj, attr_name, getattr(module, attr_name))

                # patch BaseField if available
                _patch_base_field(obj, attr_name)

class MongoEngine(object):
    """Main class used for initialization of Flask-MongoEngine."""

    def __init__(self, app=None, config=None):
      _include_mongoengine(self)
      ...
</code></pre>
<p>解决方法1:直接通过<code>mongoengine.fields.XXXField</code>引用。</p>
<pre><code class="language-py">import mongoengine.fields as fields
class User(db.Document):
    email = fields.StringField(required=True)
    username = fields.StringField(required=True, max_length=128, unique=True)
    password = fields.StringField(max_length=256)
</code></pre>
<p>解决方法2:禁用pylint错误报告。</p>
<pre><code class="language-py"># pylint: disable=no-member
value = db.StringField(max_length=200) # no error
</code></pre>
<blockquote>
<p>参考:</p>
<ul>
<li>https://stackoverflow.com/questions/61514739/instance-of-mongoengine-has-no-stringfield-member</li>
<li>https://github.com/MongoEngine/flask-mongoengine/blob/a0b0cf80ec321d29baca743e4075101db6be7d68/flask_mongoengine/<strong>init</strong>.py#L100</li>
</ul>
</blockquote>
<h2 id="数据库连接提示认证失败认证数据库未配置">数据库连接提示认证失败(认证数据库未配置)</h2>
<p>MongoDB支持在不同的数据库上创建不同的用户,即使这些用户的用户名相同。如果将要访问的数据库与用户所在的数据库不一致,而在连接时只配置将要访问的数据库,没有配置认证数据库,将产生认证错误。</p>
<p>需要通过<code>MONGODB_SETTINGS</code>变量配置<code>authentication_source</code>参数,指定用户所在的数据库。</p>
<pre><code class="language-py">MONGODB_SETTINGS = {
    'db': 'appdb',
    'host': 'localhost',
    'port': 27017,
    'connect': True,
    'username': 'test',
    'password': '123456',
    'authentication_source': 'admin'
}
</code></pre><br><br>
来源:https://www.cnblogs.com/wkyo/p/13510201.html
頁: [1]
查看完整版本: 在Flask中使用MongoDB:Flask-MongoEngine