Flask-SQLAlchemy 多种应用场景
引言
Flask-SQLAlchemy是在SQLAlchemy基础之上进行封装的,SQLAlchemy本身的文档比较丰富完善。但是Flask-SQLAlchemy在一些复杂场景中应用的资料不是很多,有时候还需扒一下源码。以下是我在应用中遇到的一些场景,我把它模拟成一个博客的环境,通过这个环境分享在不同场景中建模的方法。
部署实验环境
$ mkdir sqlalchemy_case
$ cd sqlalchemy_case
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install flask flask-sqlalchemy
$ touch models.py
$ export FLASK_APP=models.py
$ cat models.py
我的操作是在MAC上进行的,每种平台创建与启动python虚拟环境的方法略有不同。上面的操作部署了python虚拟环境,安装相应的包。我们测试要在 flask shell 中进行,这样可以交互式地看即时效果。
models.py 文件内容如下
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
class Config():
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = 'P@ssw0rd'
SQLALCHEMY_DATABASE_URI = \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
db = SQLAlchemy()
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
# User, Post, Tag 等模型在这里创建
@app.shell_context_processor
def make_shell_context():
return {
'db': db,
'User': User,
'Post': Post,
'Tag': Tag,
'Likes': Likes,
'Follow': Follow}
应用场景
这张E-R图描述了我们即将要演示的博客模型,后面的所有实体、关系示例请参照这张图。共涉及3个实体(用户、文章、关键词)和4个关系。
博客模型E-R图一对多
一对多是最常见应用场景,在示例图中“用户”与“文章”的“写作”关系就是一对多。接下来创建“用户”、“文章”实例并构建关系,models.py 内容如下:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(4))
posts = db.relationship('Post', backref='author', lazy='dynamic')
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
content = db.Column(db.String(150))
注意:“多”需要有一个外键与“一”对应,也就是
user_id
进入flask shell环境,创建一些对象试试效果
$ (venv) flask shell
db.drop_all()
db.create_all()
u1 = User(name = 'Joe Santri')
u2 = User(name = 'Percy Buttons')
p1 = Post(author=u1, content = 'post1')
p2 = Post(content = 'post2')
p3 = Post(content = 'post3')
p1.author = u1
u1.posts.append(p2)
u1.posts.append(p3)
# 查看p1的作者
p1.author
# 查看u1所写过的所有文章
u1.posts.all()
多对多(不带属性)
这种多对多关系的应用比较简单,图中“文章”与“关键词”的关系就属于这种类型。通常我们写好一篇博文会为它添加几个TAG以便于检索。例如你现在阅读的这篇博文就可以添加python
、flask
、 sqlalchemy
等Tag。那么每篇博文与每个tag之间的关系,就需要用一张单独的表来描述,在这我把这个关系命名为 tags
。
在Flask-SQLAlchemy中,这种关系的“多对多”特点非常明显,关系表的存在感被弱化。这是由于构建出来的关系抽象程度很高,我们只需操作实体即可。实体间的多对多关系,Flask-SQLAlchemy会自动的在“关系描述表”里进行维护。这句话不太容易理解,这种体会需要你在操作过程中观察数据库表中数据变化才能体会到。所谓“不带属性”是因为关系描述表里只有一个复合主键,它是由两个字段组成的,也就是“文章”与“关键词”的索引字段。models.py 内容如下:
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
content = db.Column(db.String(150))
tags = db.relationship(
'Tag',
secondary='posts_tags', # 此处为该关系的参照表(关系描述表)
lazy='dynamic'
)
# 添加 __repr__ 便于观察
def __repr__(self):
return '<Post> {}'.format(self.content[:5])
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(4))
posts = db.relationship(
'Post',
secondary = 'posts_tags',
lazy = 'dynamic'
)
def __repr__(self):
return '<Tag> {}'.format(self.name)
# 定义参照表
tags = db.Table(
'tags',
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
)
再次进入flask shell环境,创建一些对象试试效果
$ (venv) flask shell
db.drop_all()
db.create_all()
#实例化三个“文章”对象
p1 = Post(content = 'post1')
p2 = Post(content = 'post2')
p3 = Post(content = 'post3')
#实例化三个“Tag”对象
tag1 = Tag(name = 'python')
tag2 = Tag(name = 'flask')
tag2 = Tag(name = 'sqlalchemy')
# 为文章 p1 添加 “python” 和 “flask” 标签
p1.tags.append(tag1)
p1.tags.append(tag2)
# 将标签 “sqlalchemy” 贴给文章 p2、p3
tag3.posts.append(p2)
tag3.posts.append(p3)
# 查看文章 p1 都有哪些标签
p1.tags.all()
#[<Tag> python, <Tag> sqlalchemy]
# 查看标签 “flask” 都贴在了哪些文章上
tag2.posts.all()
#[<Post> post2, <Post> post3]
其实有心的伙伴可以看出来,多对多关系就是两个一对多关系拼起来的。
多对多(带属性)
图中“用户”与“文章”组成的“点赞”关系就属于这种类型,由于每个用户为每篇文章点了赞之后,要记录点这个赞的时间,因此它多了一个属性——时间。因此也就无法单独用一张描述表来描述这个属性,它需要构建一个新的模型——Likes。
class Likes(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
user = db.relationship('User')
post = db.relationship('Post')
timestamp = db.Column(db.DateTime)
如此一来,我们就不能像之前那么直观和方便的为“一”增加“多”。例如用户U1
每写一篇文章,只需要U1.posts.append(新文章)
即可。那么用户为文章点赞我们需要怎么做呢?依旧是进入到flask shell环境:
flask shell
from datetime import datetime
db.drop_all()
db.create_all()
u1 = User(name = 'Joe Santri')
p1 = Post(content = 'post1')
p2 = Post(content = 'post2')
# Joe 为 post1 点赞
l1 = Likes(user=u1,post=p1,timestamp = datetime.utcnow())
# Joe 为 post2 点赞
l2 = Likes(user=u1,post=p2,timestamp = datetime.utcnow())
# 写入数据库
db.session.add_all([u1,p1,p2,l1,l2])
db.session.commit()
# 查询Joe 为 post2 点赞的时间
Likes.query.filter_by(user=u1).filter_by(post=p2).first().timestamp
如果想更清楚过程,可以一边添加赞的实例,一边观察数据库中likes表发生了什么变化。
其实在这里使用时间戳这个属性并不很有代表性,聪明的你可能已经发现了,在这也可以像前面那样,为不带属性多对多关系的描述表里添加timestamp属性,并且预置一个赋值语句。实际上这样做是可以的,但麻烦的是这个属性的可操作性。我没有在slqalchemy 的官方文档里找到类似的做法。你要操作这个属性就必须得构造SQL语句,因此程序的可读性也就下降了。
多对多(自引用)
自引用多对多关系与前面两种多对多的区别在于这种关系是由同一个实体构成的,即“用记”与“用户”构成了“关注”关系。同样的,不带属性可以使用一张单独的表描述,带属性就需要创建一个模型。
不带属性的实现方式:
class User(db.Model):
# ……
followed = db.relationship(
'User', secondary=followers,
primaryjoin=(followers.c.follower_id == id),
secondaryjoin=(followers.c.followed_id == id),
backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')
followers = db.Table(
'followers',
# follower -> 关注者
db.Column('follower', db.Integer, db.ForeignKey('user.id'), primary_key=True),
# followed -> 被关注者
db.Column('followed', db.Integer, db.ForeignKey('user.id'), primary_key=True)
)
primaryjoin
和secondaryjoin
是ORM构造关系一种表达形式,目的是实现SQL中内联接(inner join)的效果。
进入flask shell环境,创建一些对象试试效果
$ (venv) flask shell
db.drop_all()
db.create_all()
u1 = User(name = '张三')
u2 = User(name = '李四')
u3 = User(name = '佟丽娅')
# 佟丽娅被张三、李四关注
u3.followers.append(u1)
u3.followers.append(u2)
# 查看佟丽娅的所有粉丝
u3.followers.all()
# 查看张三关注了哪些人
u1.followed.all()
带属性的实现方式:
class Follow(db.Model):
follower_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
followed_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
follower = db.relationship(
'User',
foreign_keys='Follow.follower_id',
)
followed = db.relationship(
'User',
foreign_keys='Follow.followed_id',
)
timestamp = db.Column(db.DateTime)
模型中定义的两个属性都指定同一个外键,
foreign_keys='Follow.follower_id'
用于指定当前关系的外键使用哪个字段。
进入flask shell环境,创建一些对象试试效果
$ (venv) flask shell
db.drop_all()
db.create_all()
from datetime import datetime
u1 = User(name = '张三')
u2 = User(name = '李四')
u3 = User(name = '佟丽娅')
# 张三、李四关注佟丽娅
f1 = Follow(follower=u1,followed=u3,timestamp=datetime.utcnow())
f2 = Follow(follower=u2,followed=u3,timestamp=datetime.utcnow())
#写入数据库
db.session.add_all([f1,f2])
db.session.commit()
# 查看佟丽娅的所有粉丝
Follow.query.filter_by(followed=u3).all()
# 查看张三关注了谁
Follow.query.filter_by(follower=u1).all()
# 查看张三关注的第一位粉丝的姓名
Follow.query.filter_by(follower=u1).filter_by(followed=u3).first()
# 查看张三关注娅娅的时间
Follow.query.filter_by(follower=u1).filter_by(followed=u3).first().timestamp
擅长思考的小伙伴应该已经看出问题所在了:难道查询某位用户的粉丝只能在Follow
模型里操作?在User
模型里操作不是更直观吗?像这样user1.followers.all()
。实际上我已经查过资料,目前只在官方文档里看到了类似的做法,但我自己测试没通过,如果你有什么新进展,也请告诉我。