在 Flask 中使用 SQLite 3:按应用上下文管理连接

原作者:Pallets / Flask 文档贡献者。阅读原文:Using SQLite 3 with Flask。

版本与范围:Flask 官方 stable 文档,页面标注 3.1.x;原文核验日期:2026-10-05。本译稿覆盖原文章节及代码示例;示例输出来自原文,未在本地运行。

译者说明:以下是原文全篇译文,包含全部 11 个代码与交互输出片段。原文对 sqlite3.Row 的 namedtuple 称谓在对应段落中作了明确纠正;其他代码保持原样。代码片段预设应用对象 app 已建立,并不是独立脚本。

Flask 应用上下文中 SQLite 连接的按需创建、复用和关闭流程
译者绘制:g 保存当前应用上下文的连接,首次查询创建,后续复用,teardown 时检查并关闭。

在 Flask 中,可以很方便地按需打开数据库连接,并在上下文结束时关闭连接;通常,这发生在请求结束时。

下面是将 SQLite 3 与 Flask 配合使用的一个简单示例:

import sqlite3
from flask import g

DATABASE = '/path/to/database.db'

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

要使用数据库,应用必须处于有效的应用上下文中——只要有请求正在处理,这个条件就成立——或者自行创建应用上下文。此时可以调用 get_db,取得当前数据库连接。每当上下文被销毁,连接也会随之关闭。

例如:

@app.route('/')
def index():
    cur = get_db().cursor()
    ...

注意

无论请求前置处理函数失败,还是根本没有执行,处理请求与应用上下文清理的 teardown 函数都会运行。因此,关闭连接之前必须先确认连接已经存在。

按需连接

首次使用时才建立连接的好处,是只在确实需要数据库时打开连接。如果想在请求上下文之外使用这段代码,可以在 Python 交互环境中手动建立应用上下文:

with app.app_context():
    # now you can use get_db()

简化查询

现在,每个请求处理函数都可以通过 get_db() 取得当前已打开的连接。为了简化 SQLite 的使用,可以设置一个行工厂函数:数据库每返回一行结果,都会调用它来转换该行。例如,若希望得到字典而不是元组,可以把下面的代码加入前面编写的 get_db 函数:

def make_dicts(cursor, row):
    return dict((cursor.description[idx][0], value)
                for idx, value in enumerate(row))

db.row_factory = make_dicts

这样,sqlite3 模块就会为该连接返回更容易使用的字典。还有一种更简单的做法:在 get_db 中改用下面这一行:

db.row_factory = sqlite3.Row

此时查询结果会以 Row 对象而不是字典返回,既可以用索引访问,也可以用字段名访问。例如,假设 r 是一个 sqlite3.Row 对象,包含 id、FirstName、LastName 和 MiddleInitial 四列。原文将这类对象称为 namedtuple;准确地说,sqlite3.Row 是支持序列和映射式访问的专用行对象,并不是 collections.namedtuple。下面保留原文交互示例:

>>> # You can get values based on the row's name
>>> r['FirstName']
John
>>> # Or, you can get them based on index
>>> r[1]
John
# Row objects are also iterable:
>>> for value in r:
...     print(value)
1
John
Doe
M

此外,可以提供一个查询函数,把取得游标、执行语句、取回结果这几步合在一起:

def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

这个小函数与行工厂搭配使用,比直接操作原始游标和连接对象方便得多。

可以这样使用:

for user in query_db('select * from users'):
    print(user['username'], 'has the id', user['user_id'])

如果只想取得一条结果:

user = query_db('select * from users where username = ?',
                [the_username], one=True)
if user is None:
    print('No such user')
else:
    print(the_username, 'has the id', user['user_id'])

向 SQL 语句传入变化的值时,应在语句中使用问号占位符,并把参数放在列表中传入。绝不要使用字符串格式化,把值直接拼进 SQL 语句,否则攻击者可能利用 SQL 注入攻击应用。

原文参考:SQL Injections。

初始化数据库结构

关系型数据库需要表结构,因此应用通常会附带一个用于创建数据库的 schema.sql 文件。提供一个根据该文件建立数据库的函数很实用,下面的函数便完成这项工作:

def init_db():
    with app.app_context():
        db = get_db()
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())
        db.commit()

随后,可以从 Python 交互环境调用它来创建数据库:

>>> from yourapplication import init_db
>>> init_db()

译者核验与使用边界

g 属于当前应用上下文,不是进程级全局连接池。只有在有效应用上下文内才能访问这里保存的连接;with app.app_context() 用于显式建立这一生命周期。

teardown 回调即使前置处理失败或未运行也会执行,因此原代码先用 getattr 检查连接存在再 close;这项检查不可省略。

原文把 sqlite3.Row 称为 namedtuple 不够准确。它是支持整数索引、列名和迭代访问的专用 Row 类型;译文已显式纠正这一描述,未改变示例代码。

query_db 始终调用 fetchall,包括 one=True 的情况,因此 one=True 只改变返回值,不会限制数据库返回行数。大量数据应通过 SQL LIMIT、分页或分批获取控制内存;发生 execute/fetchall 异常时,示例也没有用 finally 显式关闭游标。

问号占位符用于绑定值,不能拿来绑定表名、列名等 SQL 标识符。不可信标识符应通过明确白名单选择;不要把用户输入直接拼进 SQL。

init_db 会把 schema.sql 全文交给 executescript 并提交,可能创建、修改或删除数据,风险由脚本内容决定。必须使用可信 schema,并在目标库备份和审核后调用;它不是迁移系统。

示例省略写事务边界、锁等待策略、异常回滚及应用对象创建;不能把教学片段直接当作完整生产数据库层。以上为静态审核,没有执行 SQLite 连接或 schema 脚本。

来源、署名与授权

本文译自 Pallets / Flask 文档贡献者 的 Using SQLite 3 with Flask。Flask 官方仓库提供 BSD-3-Clause 许可证,适用版权与完整许可声明见下文。 译者补充与原文内容已作区分。

补充核验来源

Flask 官方 BSD-3-Clause 许可:Copyright 2010 Pallets;完整许可保留在本文下方。

适用代码许可

以下为原示例适用的完整 BSD-3-Clause 许可与版权声明;中文说明和编校增补已标明。

Copyright 2010 Pallets

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

3.  Neither the name of the copyright holder nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容