1. Flask数据库操作基础认知
第一次用Flask操作数据库时,我盯着SQLAlchemy的文档发了半小时呆。ORM(对象关系映射)这个概念听起来很美好——用Python类代替SQL语句,但实际配置时各种参数让人眼花缭乱。后来在电商项目中处理用户订单时,我才真正理解Flask-SQLAlchemy的价值:当订单表关联到用户表、商品表时,用原生SQL写JOIN查询简直是一场噩梦。
Flask连接数据库的核心在于选择合适的工具链。小型项目可以直接用sqlite3模块,但中大型项目强烈推荐使用Flask-SQLAlchemy+MySQL/PostgreSQL组合。我见过有团队在原型阶段图省事直接用SQLite,等用户量上来后迁移数据时,字段类型不兼容的问题让整个开发组加了三天班。
2. 环境配置与连接建立
2.1 依赖安装清单
在虚拟环境中执行以下命令会安装全套数据库工具链:
bash复制pip install flask-sqlalchemy pymysql psycopg2-binary
这里有个坑:MySQL官方驱动mysql-connector在Alembic迁移时会有兼容性问题,而pymysql是纯Python实现更稳定。PostgreSQL则必须用psycopg2,它的二进制版本(带-binary后缀)免去了编译依赖的麻烦。
2.2 配置文件最佳实践
不要在代码里硬编码数据库URI!我推荐采用三层配置方案:
python复制# config.py
class Config:
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///default.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False # 关闭警告信息
SQLALCHEMY_ECHO = True # 开发时显示SQL日志
生产环境通过环境变量注入:
bash复制export DATABASE_URL=mysql+pymysql://user:pass@localhost/prod_db
3. 模型定义与关系构建
3.1 字段类型选型指南
Flask-SQLAlchemy的字段类型远比表面看起来复杂。比如同样是字符串:
python复制from sqlalchemy import Text, String
class Article(db.Model):
title = db.Column(String(100)) # 定长适合索引
content = db.Column(Text) # 不限长度但性能较差
时间字段的处理更讲究:
python复制from datetime import datetime
from sqlalchemy.sql import func
class User(db.Model):
created_at = db.Column(db.DateTime, server_default=func.now()) # 数据库生成时间
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) # 自动更新
3.2 关系类型深度解析
一对多关系是最常用的模式:
python复制class Author(db.Model):
books = db.relationship('Book', back_populates='author')
class Book(db.Model):
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
author = db.relationship('Author', back_populates='books')
多对多关系需要中间表:
python复制tags = db.Table('tags',
db.Column('book_id', db.Integer, db.ForeignKey('book.id')),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)
class Book(db.Model):
tags = db.relationship('Tag', secondary=tags, back_populates='books')
4. 会话管理与CRUD操作
4.1 事务处理要点
Flask-SQLAlchemy默认自动提交,但在转账等场景需要手动控制:
python复制try:
db.session.begin(subtransactions=True)
sender.balance -= amount
receiver.balance += amount
db.session.commit()
except:
db.session.rollback()
raise
批量插入时用session.bulk_save_objects能提升10倍性能:
python复制db.session.bulk_save_objects([User(name=f'user{i}') for i in range(1000)])
4.2 查询优化技巧
避免N+1查询问题:
python复制# 错误做法:每次循环都查询数据库
books = Book.query.all()
for book in books:
print(book.author.name) # 每次循环都查询author
# 正确做法:使用joinedload预加载
from sqlalchemy.orm import joinedload
books = Book.query.options(joinedload(Book.author)).all()
复杂查询可以用hybrid_property:
python复制from sqlalchemy.ext.hybrid import hybrid_property
class Product(db.Model):
price = db.Column(db.Float)
discount = db.Column(db.Float)
@hybrid_property
def final_price(self):
return self.price * (1 - self.discount)
5. 性能监控与调试
5.1 SQL日志分析
启用慢查询日志:
python复制from sqlalchemy import event
from sqlalchemy.engine import Engine
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
context._query_start_time = time.time()
@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
duration = (time.time() - context._query_start_time) * 1000
if duration > 100: # 超过100毫秒视为慢查询
app.logger.warning(f"Slow query: {statement} took {duration:.2f}ms")
5.2 连接池配置
生产环境必须调整连接池参数:
python复制from sqlalchemy.pool import QueuePool
app.config.update({
'SQLALCHEMY_ENGINE_OPTIONS': {
'pool_size': 20,
'max_overflow': 10,
'pool_recycle': 3600, # 1小时回收连接
'pool_pre_ping': True # 自动检测断连
}
})
6. 数据库迁移实战
6.1 Alembic迁移流程
初始化迁移环境:
bash复制flask db init
flask db migrate -m "create user table"
flask db upgrade
处理列类型修改等危险操作时:
python复制# 错误做法:直接修改字段类型
# name = db.Column(db.String(50)) → name = db.Column(db.Integer)
# 正确做法:分步迁移
def upgrade():
op.add_column('user', sa.Column('new_name', sa.Integer()))
op.execute('UPDATE user SET new_name = CAST(name AS INTEGER)')
op.drop_column('user', 'name')
op.rename_column('user', 'new_name', 'name')
6.2 数据迁移策略
大型表迁移要用批处理:
python复制from sqlalchemy import select
def batch_migrate(source_table, target_table, batch_size=1000):
conn = db.engine.connect()
total = conn.execute(f"SELECT COUNT(*) FROM {source_table}").scalar()
for offset in range(0, total, batch_size):
batch = conn.execute(
select(source_table).limit(batch_size).offset(offset)
).fetchall()
target_table.insert().values(batch).execute()
7. 安全防护措施
7.1 SQL注入防御
永远不要拼接SQL语句:
python复制# 危险!
query = f"SELECT * FROM users WHERE name = '{request.args['name']}'"
# 安全方案1:使用参数化查询
User.query.filter_by(name=request.args['name'])
# 安全方案2:使用ORM表达式
from sqlalchemy import text
db.session.execute(text("SELECT * FROM users WHERE name = :name"),
{'name': request.args['name']})
7.2 敏感数据加密
密码必须加盐哈希:
python复制from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('password is not readable')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
8. 高级查询模式
8.1 复合索引优化
多条件查询要创建联合索引:
python复制class Product(db.Model):
__table_args__ = (
db.Index('idx_category_price', 'category_id', 'price'),
)
查询时注意最左前缀原则:
python复制# 能使用索引
Product.query.filter(Product.category_id == 1, Product.price > 100)
# 不能使用索引
Product.query.filter(Product.price > 100)
8.2 窗口函数应用
统计用户排名:
python复制from sqlalchemy import over, func
rank_query = db.session.query(
User.id,
User.score,
func.rank().over(order_by=User.score.desc()).label('rank')
)
9. 测试策略设计
9.1 测试数据库隔离
使用pytest-fixture创建临时数据库:
python复制import pytest
from app import create_app, db as _db
@pytest.fixture
def app():
app = create_app('testing')
with app.app_context():
_db.create_all()
yield app
_db.drop_all()
9.2 工厂模式生成测试数据
用factory_boy创建测试数据:
python复制import factory
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session = db.session
username = factory.Faker('user_name')
email = factory.Faker('email')
10. 生产环境部署
10.1 读写分离配置
使用SQLAlchemy的binds实现读写分离:
python复制app.config['SQLALCHEMY_BINDS'] = {
'master': 'mysql://master.example.com/db',
'slave': 'mysql://slave.example.com/db'
}
class User(db.Model):
__bind_key__ = 'master' # 默认写操作
@app.route('/users')
def list_users():
# 读操作指定slave
users = db.session().using_bind('slave').query(User).all()
10.2 连接健康检查
Kubernetes存活探针配置:
python复制@app.route('/health')
def health_check():
try:
db.session.execute('SELECT 1')
return 'OK', 200
except:
return 'DB Error', 500