Интеграция базы данных во Flask
Большинству веб-приложений требуется постоянное хранилище данных.
В этой главе используется встроенная в Python база данных SQLite, чтобы показать, как интегрировать базу данных во Flask без установки дополнительных зависимостей.
Быстрый старт с SQLite
SQLite — это лёгкая база данных, которая хранит данные в одном файле и поддерживается стандартной библиотекой Python.
Это лучший выбор для обучения и прототипирования — не нужно устанавливать сервер базы данных или что-либо настраивать.
Пример
import sqlite3
# Подключиться к базе данных (example.db автоматически создаётся, если не существует)
conn = sqlite3.connect("example.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 (?, ?)", ("example", "pass123"))
conn.commit()
# Запросить данные
rows = conn.execute("SELECT * FROM user").fetchall()
for row in rows:
print(dict(row)) # Вывод: {'id': 1, 'username': 'example', 'password': 'pass123'}
conn.close()
Использование объекта g в Flask для управления подключениями к базе данных
gЭто специальный объект, жизненный цикл которого — один запрос.
Сохранение подключения к базе данныхgЭто гарантирует, что несколько операций с базой данных в рамках одного запроса используют одно и то же подключение, которое автоматически освобождается после завершения запроса.
Пример
import sqlite3
from flask import g, current_app
def get_db():
"""Получить подключение к базе данных, повторно используемое в рамках одного запроса"""
if "db" not in g:
# Прочитать путь к базе данных из конфигурации
db_path = current_app.config.get("DATABASE", "example.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)
Интеграция паттерна Factory
Используйте паттерн фабрики для интеграции базы данных с другими модулями:
Пример
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, "example.db")
# Убедитесь, что папка instance существует
os.makedirs(app.instance_path, exist_ok=True)
# Инициализируйте базу данных
from db import init_app as init_db
init_db(app)
# Зарегистрируйте blueprint
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 (Create, Read, Update, Delete) для статей:
Пример
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 в список словарей
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":"EXAMPLE 教程","body":"Flask 入门教程内容","author_id":1}'
# 更新文章
$ curl -X PUT http://127.0.0.1:5000/api/posts/1 \
-H "Content-Type: application/json" \
-d '{"title":"EXAMPLE 教程(修订版)","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/example.db ".tables" post user
Защита от SQL-инъекций
Использование параметризованных запросов (?заполнители) — ключ к предотвращению SQL-инъекций:
Пример
# Драйвер SQLite автоматически экранирует параметры, чтобы предотвратить выполнение вредоносного ввода как SQL
db.execute("SELECT * FROM user WHERE username = ?", (username,))
# Неправильный подход: используйте f-строку для конкатенации SQL (чрезвычайно опасно!)
# db.execute(f"SELECT * FROM user WHERE username = '{username}'")
# Вредоносный ввод '; DROP TABLE user; -- удалил бы всю таблицу!
Использование Flask-SQLAlchemy (продвинутое необязательное чтение)
В крупных проектах ручное написание SQL постепенно становится утомительным.
Flask-SQLAlchemyЭто широко распространённое расширение, которое предоставляет возможность ORM (Object-Relational Mapping):
Пример
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///example.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="example", email="[email protected]")
db.session.add(user)
db.session.commit()
# Запрос пользователя
users = User.query.all()
example_user = User.query.filter_by(username="example").first()
Другие расширенияВ этой главе используется встроенный SQLite, чтобы избежать дополнительных зависимостей, что делает её подходящей для обучения и небольших проектов. Для реальных производственных проектов мы рекомендуем использовать SQLAlchemy или аналогичную ORM, что может значительно сократить повторяющийся код и повысить безопасность.