Flask 数据库集成
大多数 Web 应用都需要持久化存储数据。
本章使用 Python 内置的 SQLite 数据库,展示如何在 Flask 中集成数据库,无需安装额外依赖。
SQLite 快速入门
SQLite 是一个轻量级数据库,数据存储在一个文件中,Python 标准库自带支持。
它是学习和原型开发的最佳选择——无需安装数据库服务器,无需配置。
实例
# 一个简单的 SQLite 示例(不使用 Flask)
import sqlite3
# 连接数据库(如果 runoob.db 不存在则自动创建)
conn = sqlite3.connect("runoob.db")
# row_factory 让查询结果像字典一样可用字段名访问
conn.row_factory = sqlite3.Row
# 创建表
conn.execute("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)")
# 插入数据
conn.execute("INSERT INTO user (username, password) VALUES (?, ?)", ("runoob", "pass123"))
conn.commit()
# 查询数据
rows = conn.execute("SELECT * FROM user").fetchall()
for row in rows:
print(dict(row)) # 输出: {'id': 1, 'username': 'runoob', 'password': 'pass123'}
conn.close()
import sqlite3
# 连接数据库(如果 runoob.db 不存在则自动创建)
conn = sqlite3.connect("runoob.db")
# row_factory 让查询结果像字典一样可用字段名访问
conn.row_factory = sqlite3.Row
# 创建表
conn.execute("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)")
# 插入数据
conn.execute("INSERT INTO user (username, password) VALUES (?, ?)", ("runoob", "pass123"))
conn.commit()
# 查询数据
rows = conn.execute("SELECT * FROM user").fetchall()
for row in rows:
print(dict(row)) # 输出: {'id': 1, 'username': 'runoob', 'password': 'pass123'}
conn.close()
在 Flask 中使用 g 对象管理数据库连接
g 是一个特殊的对象,它的生命周期是单个请求。
将数据库连接存入 g,可以确保同一请求内的多次数据库操作共享同一连接,请求结束后自动清理。
实例
# 文件路径:db.py(数据库管理模块)
import sqlite3
from flask import g, current_app
def get_db():
"""获取数据库连接,同一请求内复用"""
if "db" not in g:
# 从配置中读取数据库路径
db_path = current_app.config.get("DATABASE", "runoob.db")
g.db = sqlite3.connect(db_path)
g.db.row_factory = sqlite3.Row # 让结果支持字段名访问
return g.db
def close_db(error=None):
"""关闭数据库连接(请求结束时自动调用)"""
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
"""初始化数据库表结构"""
db = get_db()
db.execute("""
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
db.execute("""
CREATE TABLE IF NOT EXISTS post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
author_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES user (id)
)
""")
db.commit()
def init_app(app):
"""在 Flask 应用中注册数据库相关功能"""
# 请求结束时自动关闭数据库连接
app.teardown_appcontext(close_db)
# 注册 CLI 命令:flask init-db
app.cli.command("init-db")(init_db)
import sqlite3
from flask import g, current_app
def get_db():
"""获取数据库连接,同一请求内复用"""
if "db" not in g:
# 从配置中读取数据库路径
db_path = current_app.config.get("DATABASE", "runoob.db")
g.db = sqlite3.connect(db_path)
g.db.row_factory = sqlite3.Row # 让结果支持字段名访问
return g.db
def close_db(error=None):
"""关闭数据库连接(请求结束时自动调用)"""
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
"""初始化数据库表结构"""
db = get_db()
db.execute("""
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
db.execute("""
CREATE TABLE IF NOT EXISTS post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
author_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES user (id)
)
""")
db.commit()
def init_app(app):
"""在 Flask 应用中注册数据库相关功能"""
# 请求结束时自动关闭数据库连接
app.teardown_appcontext(close_db)
# 注册 CLI 命令:flask init-db
app.cli.command("init-db")(init_db)
工厂模式整合
使用工厂模式将数据库与其他模块整合:
实例
# 文件路径:app.py
from flask import Flask
def create_app():
app = Flask(__name__)
app.secret_key = "dev-secret-key"
# 配置数据库路径(存储在 instance 文件夹中)
import os
app.config["DATABASE"] = os.path.join(app.instance_path, "runoob.db")
# 确保 instance 文件夹存在
os.makedirs(app.instance_path, exist_ok=True)
# 初始化数据库
from db import init_app as init_db
init_db(app)
# 注册蓝图
from auth import bp as auth_bp
from blog import bp as blog_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(blog_bp)
return app
from flask import Flask
def create_app():
app = Flask(__name__)
app.secret_key = "dev-secret-key"
# 配置数据库路径(存储在 instance 文件夹中)
import os
app.config["DATABASE"] = os.path.join(app.instance_path, "runoob.db")
# 确保 instance 文件夹存在
os.makedirs(app.instance_path, exist_ok=True)
# 初始化数据库
from db import init_app as init_db
init_db(app)
# 注册蓝图
from auth import bp as auth_bp
from blog import bp as blog_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(blog_bp)
return app
写一个完整的博客 API
结合数据库,实现文章的增删改查(CRUD)操作:
实例
# 文件路径:blog.py
from flask import Blueprint, request, jsonify, abort, g
from db import get_db
bp = Blueprint("blog", __name__)
@bp.get("/api/posts")
def list_posts():
"""获取所有文章列表"""
db = get_db()
posts = db.execute(
"SELECT p.id, p.title, p.body, p.created_at, u.username"
" FROM post p JOIN user u ON p.author_id = u.id"
" ORDER BY p.created_at DESC"
).fetchall()
# 将 Row 对象列表转为 dict 列表
return jsonify([dict(post) for post in posts])
@bp.get("/api/posts/<int:post_id>")
def get_post(post_id):
"""获取单篇文章详情"""
db = get_db()
post = db.execute(
"SELECT id, title, body, created_at FROM post WHERE id = ?",
(post_id,)
).fetchone()
if post is None:
abort(404, description=f"文章 {post_id} 不存在")
return jsonify(dict(post))
@bp.post("/api/posts")
def create_post():
"""创建新文章"""
data = request.json
title = data.get("title", "").strip()
body = data.get("body", "").strip()
author_id = data.get("author_id", 1) # 默认作者 ID
if not title:
return jsonify({"error": "标题不能为空"}), 400
db = get_db()
cursor = db.execute(
"INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)",
(title, body, author_id)
)
db.commit()
return jsonify({"id": cursor.lastrowid, "title": title, "message": "文章创建成功"}), 201
@bp.put("/api/posts/<int:post_id>")
def update_post(post_id):
"""更新文章"""
data = request.json
title = data.get("title", "").strip()
body = data.get("body", "").strip()
if not title:
return jsonify({"error": "标题不能为空"}), 400
db = get_db()
db.execute(
"UPDATE post SET title = ?, body = ? WHERE id = ?",
(title, body, post_id)
)
db.commit()
return jsonify({"id": post_id, "message": "文章更新成功"})
@bp.delete("/api/posts/<int:post_id>")
def delete_post(post_id):
"""删除文章"""
db = get_db()
db.execute("DELETE FROM post WHERE id = ?", (post_id,))
db.commit()
return jsonify({"message": "文章已删除"})
from flask import Blueprint, request, jsonify, abort, g
from db import get_db
bp = Blueprint("blog", __name__)
@bp.get("/api/posts")
def list_posts():
"""获取所有文章列表"""
db = get_db()
posts = db.execute(
"SELECT p.id, p.title, p.body, p.created_at, u.username"
" FROM post p JOIN user u ON p.author_id = u.id"
" ORDER BY p.created_at DESC"
).fetchall()
# 将 Row 对象列表转为 dict 列表
return jsonify([dict(post) for post in posts])
@bp.get("/api/posts/<int:post_id>")
def get_post(post_id):
"""获取单篇文章详情"""
db = get_db()
post = db.execute(
"SELECT id, title, body, created_at FROM post WHERE id = ?",
(post_id,)
).fetchone()
if post is None:
abort(404, description=f"文章 {post_id} 不存在")
return jsonify(dict(post))
@bp.post("/api/posts")
def create_post():
"""创建新文章"""
data = request.json
title = data.get("title", "").strip()
body = data.get("body", "").strip()
author_id = data.get("author_id", 1) # 默认作者 ID
if not title:
return jsonify({"error": "标题不能为空"}), 400
db = get_db()
cursor = db.execute(
"INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)",
(title, body, author_id)
)
db.commit()
return jsonify({"id": cursor.lastrowid, "title": title, "message": "文章创建成功"}), 201
@bp.put("/api/posts/<int:post_id>")
def update_post(post_id):
"""更新文章"""
data = request.json
title = data.get("title", "").strip()
body = data.get("body", "").strip()
if not title:
return jsonify({"error": "标题不能为空"}), 400
db = get_db()
db.execute(
"UPDATE post SET title = ?, body = ? WHERE id = ?",
(title, body, post_id)
)
db.commit()
return jsonify({"id": post_id, "message": "文章更新成功"})
@bp.delete("/api/posts/<int:post_id>")
def delete_post(post_id):
"""删除文章"""
db = get_db()
db.execute("DELETE FROM post WHERE id = ?", (post_id,))
db.commit()
return jsonify({"message": "文章已删除"})
测试 API:
# 获取文章列表
$ curl http://127.0.0.1:5000/api/posts
# 创建文章
$ curl -X POST http://127.0.0.1:5000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"RUNOOB 教程","body":"Flask 入门教程内容","author_id":1}'
# 更新文章
$ curl -X PUT http://127.0.0.1:5000/api/posts/1 \
-H "Content-Type: application/json" \
-d '{"title":"RUNOOB 教程(修订版)","body":"更新后的内容"}'
# 删除文章
$ curl -X DELETE http://127.0.0.1:5000/api/posts/1
最佳实践:API 返回 JSON 数据时,使用正确的 HTTP 状态码——创建用 201,成功用 200,客户端错误用 400,资源不存在用 404。这能让前端代码更容易判断请求结果。
数据库初始化命令
通过 Flask CLI 可以创建自定义命令来初始化数据库:
# 初始化数据库(创建表结构) (.venv) $ flask init-db # 验证表已创建 (.venv) $ sqlite3 instance/runoob.db ".tables" post user
SQL 注入防护
使用参数化查询(? 占位符)是防止 SQL 注入的关键:
实例
# 正确做法:使用 ? 占位符(参数化查询)
# SQLite 驱动会自动转义参数,防止恶意输入被当作 SQL 执行
db.execute("SELECT * FROM user WHERE username = ?", (username,))
# 错误做法:用 f-string 拼接 SQL(极其危险!)
# db.execute(f"SELECT * FROM user WHERE username = '{username}'")
# 恶意输入 '; DROP TABLE user; -- 会删除整个表!
# SQLite 驱动会自动转义参数,防止恶意输入被当作 SQL 执行
db.execute("SELECT * FROM user WHERE username = ?", (username,))
# 错误做法:用 f-string 拼接 SQL(极其危险!)
# db.execute(f"SELECT * FROM user WHERE username = '{username}'")
# 恶意输入 '; DROP TABLE user; -- 会删除整个表!
使用 Flask-SQLAlchemy(进阶选读)
对于大型项目,手动写 SQL 会逐渐变得繁琐。
Flask-SQLAlchemy 是一个广受欢迎的扩展,提供了 ORM(对象关系映射)能力:
实例
# 安装:pip install flask-sqlalchemy
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///runoob.db"
db = SQLAlchemy(app)
# 定义数据模型(表结构用 Python 类表示)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f"<User {self.username}>"
# 创建表
with app.app_context():
db.create_all()
# 创建用户
user = User(username="runoob", email="test@runoob.com")
db.session.add(user)
db.session.commit()
# 查询用户
users = User.query.all()
runoob_user = User.query.filter_by(username="runoob").first()
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///runoob.db"
db = SQLAlchemy(app)
# 定义数据模型(表结构用 Python 类表示)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f"<User {self.username}>"
# 创建表
with app.app_context():
db.create_all()
# 创建用户
user = User(username="runoob", email="test@runoob.com")
db.session.add(user)
db.session.commit()
# 查询用户
users = User.query.all()
runoob_user = User.query.filter_by(username="runoob").first()
本章使用原生 SQLite 是为了不引入额外依赖,适合学习和小型项目。对于实际生产项目,建议使用 SQLAlchemy 或类似 ORM,能显著减少重复代码并提升安全性。
