1. 在 Flask-SQLAlchemy 中,数据库使用 URL 指定。最流行的数据库引擎采用的数据库 URL
MySQL mysql://username:password@hostname/database
程序使用的数据库 URL 必须保存到 Flask 配置对象的 SQLALCHEMY_DATABASE_URI 键中。
from flask.ext.sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(b)
2. 最常用的SQLAlchemy列类型
3. 定义模型
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % s
类变量 __tablename__ 定义在数据库中使用的表名。如果没有定义 __tablename__,Flask-SQLAlchemy 会使用一个默认名字,但默认的表名没有遵守使用复数形式进行命名的约定,所以最好由我们自己来指定表名。其余的类变量都是该模型的属性,被定义为 db.Column类的实例。
4. 关系
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
如图 5-1 所示,关系使用 users 表中的外键连接了两行。添加到 User 模型中的 role_id 列被定义为外键,就是这个外键建立起了关系。传给 db.ForeignKey() 的参数 'roles.id' 表明,这列的值是 roles 表中行的 id 值。
添加到 Role 模型中的 users 属性代表这个关系的面向对象视角。对于一个 Role 类的实例,其 users 属性将返回与角色相关联的用户组成的列表。db.relationship() 的第一个参数表明这个关系的另一端是哪个模型。如果模型类尚未定义,可使用字符串形式指定。db.relationship() 中的 backref 参数向 User 模型中添加一个 role 属性,从而定义反向关系。这一属性可替代 role_id 访问 Role 模型,此时获取的是模型对象,而不是外键的值。大多数情况下,db.relationship() 都能自行找到关系中的外键,但有时却无法决定把哪一列作为外键。例如,如果 User 模型中有两个或以上的列定义为 Role 模型的外键,SQLAlchemy 就不知道该使用哪列。如果无法决定外键,你就要为 db.relationship() 提供额外参数,从而确定所用外键。表 5-4 列出了定义关系时常用的配置选项
如:
除了一对多之外,还有几种其他的关系类型。一对一关系可以用前面介绍的一对多关系表示,但调用 db.relationship() 时要把 uselist 设为 False,把“多”变成“一”。多对一关系也可使用一对多表示,对调两个表即可,或者把外键和 db.relationship() 都放在“多”这一侧。最复杂的关系类型是多对多,需要用到第三张表,这个表称为关系表。
5. 插入行
下面这段代码创建了一些角色和用户:
from hello import Role, User
admin_role = Role(name='Admin')
mod_role = Role(name='Moderator')
user_role = Role(name='User')
user_john = User(username='john', role=admin_role)
user_susan = User(username='susan', role=user_role)
user_david = User(username='david', role=user_role)
模型的构造函数接受的参数是使用关键字参数指定的模型属性初始值。注意,role 属性也可使用,虽然它不是真正的数据库列,但却是一对多关系的高级表示。
通过数据库会话管理对数据库所做的改动,在 Flask-SQLAlchemy 中,会话由 db.session表示。准备把对象写入数据库之前,先要将其添加到会话中:
db.session.add(admin_role)
db.session.add(mod_role)
db.session.add(user_role)
db.session.add(user_john)
db.session.add(user_susan)
db.session.add(user_david
或者简写成:
db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])
为了把对象写入数据库,我们要调用 commit() 方法提交会话:
db.session.commit()
数据库会话能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误,整个会话都会失效。如果你始终把相关改动放在会话中提交,就能避免因部分更新导致的数据库不一致性。
数据库会话也可回滚。调用 db.session.rollback() 后,添加到数据库会话中的所有对象都会还原到它们在数据库时的状态。
6. 修改行
在数据库会话上调用 add() 方法也能更新模型。我们继续在之前的 shell 会话中进行操作,下面这个例子把 "Admin" 角色重命名为 "Administrator":
admin_role.name = 'Administrator'
db.session.add(admin_role)
db.session.commit()
7. 删除行
数据库会话还有个 delete() 方法。下面这个例子把 "Moderator" 角色从数据库中删除:
db.session.delete(mod_role)
db.session.commit()
注意,删除与插入和更新一样,提交数据库会话后才会执行。
8. 查询行
Flask-SQLAlchemy 为每个模型类都提供了 query 对象。最基本的模型查询是取回对应表中的所有记录:
Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
使用过滤器可以配置 query 对象进行更精确的数据库查询。下面这个例子查找角色为"User" 的所有用户:
User.query.filter_by(role=user_role).all()
[<User u'susan'>, <User u'david'>]
若要查看 SQLAlchemy 为查询生成的原生 SQL 查询语句,只需把 query 对象转换成字符串:
str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username, users.role_id AS users_role_id FROM users WHERE :param_1 = users.role_id'
filter_by() 等过滤器在 query 对象上调用,返回一个更精确的 query 对象。多个过滤器可以一起调用,直到获得所需结果。
9. 多对多关系
在一对多关系中,我们可以在“多”这一侧添加外键指向“一”这一 侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。在SQLAlchemy中,要想表示多对多关系,除了关系两侧的模型外,我们还需要创建一个关联表(association table)。关联表不存储数据,只用来存储关系两侧模型的外键对应关系,如代码清单5-15所示。
association_table = db.Table('association',db.Column('student_id', db.Integer
)
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(70), unique=True)
grade = db.Column(db.String(20))
teachers = db.relationship('Teacher', secondary=association_table, back_populates='students')
class Teacher(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(70), unique=True)
office = db.Column(db.String(20))
关联表使用db.Table类定义,传入的第一个参数是关联表的名称。我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。借助关联表这个中间人存储的外键对,我们可以把多对多关系分化成两个一对多关系。
当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这些记录中再进一步获取每个关联表记录包含的老师记录。以图5-8中的随机数据为例,假设学生记录的id为1,那么通过查找关联表中student_id字段为1的记录,就可以获取到对应的teacher_id值(分别为3 和4),通过外键值就可以在teacher表里获取id为3和4的记录,最终,我们就获取到id为1的学生记录相关联的所有老师记录。我们在Student类中定义一个teachers关系属性用来获取老师集合。在多对多关系中定义关系函数,除了第一个参数是关系另一侧的模型名称外,我们还需要添加一个secondary参数,把这个值设为关联表的名称。
为了便于实现真正的多对多关系,我们需要建立双向关系。建立双向关系后,多对多关系会变得更加直观。在Student类上的teachers集合属性会返回所有关联的老师记录,而在Teacher类上的students集合属性会返回所有相关的学生记录:
class Teacher(db.Model):
...
students = db.relationship('Student', secondary=association_table, back_populates='teachers')
除了在声明关系时有所不同,多对多关系模式在操作关系时和其他关系模式基本相同。调用关系属性student.teachers时,SQLAlchemy会直接返回关系另一侧的Teacher对象,而不是关联表记录,反之亦同。和其他关系模式中的集合关系属性一样,我们可以将关系属性teachers和students像列表一样操作。比如,当你需要为某一个学生添加老师时, 对关系属性使用append()方法即可。如果你想要解除关系,那么可以使用remove()方法。
关联表由SQLAlchemy接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。同样的,在多对多关系中我们也只需要在关系的一侧操作关系。当为学生A的teachers添加了老师B后,调用老师B的students属性时返回的学生记录也会包含学生A,反之亦同。
10. 自关联
以文章与评论两张表为例,文章有多个评论,而评论又可以有回复,那么怎么处理评论与回复评论的记录关系呢?
class Post(db.Model, BaseModel):
"""文章"""
__tablename__ = 'blog_post'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(60))
body = db.Column(db.Text)
category_id = db.Column(db.Integer, db.ForeignKey('blog_category.id'))
category = db.relationship(Category, back_populates='posts')
comments = db.relationship('Comment', back_populates='post', cascade='all')
class Comment(db.Model):
"""评论"""
__tablename__ = 'blog_comment'
id = db.Column(db.Integer, primary_key=True)
author = db.Column(db.String(30))
email = db.Column(db.String(254))
site = db.Column(db.String(255))
body = db.Column(db.Text)
from_admin = db.Column(db.Boolean, default=False)
reviewed = db.Column(db.Boolean, default=False)
post_id = db.Column(db.Integer, db.ForeignKey('blog_post.id'))
post = db.relationship('Post', back_populates='comments')
你当然可以再为回复创建一个Reply模型,然后使用一对多关系将评论和回复关联起来。但是我们将介绍一个更简单的解决办法,因为回复本身也是评论,如果可以在评论模型内建立层级关系,那么就可以在一个模型中表示评论和回复。
这种在同一个模型内的一对多关系在SQLAlchemy中被称为邻接列表关系(Adjacency List Relationship)。具体来说,我们需要在Comment模型中添加一个外键指向它自身。这样我们就得到一种层级关系:每个评论对象都可以包含多个子评论,即回复。
下面是更新后的Comment模型:
class Comment(db.Model):
...
replied_id = db.Column(db.Integer, db.ForeignKey('blog_comment.id')) # 指向自身表的id
replied = db.relationship('Comment', back_populates='replies', remote_side=[id]) # 相当于一对多关系中的多的模型中设置的关系属性,需要添加remote_side
replies = db.relationship('Comment', back_populates='replied', cascade='all') # 相当于一对多关系中的多的模型一的模型
在Commet模型中,我们添加了一个replied_id字段,通过 db.ForeignKey()设置一个外键指向自身的id字段。
关系两侧的关系属性都在Comment模型中定义,需要特别说明的是表示被回复评论(父对象)的标量关系属性replied的定义。
这个关系和我们之前熟悉的一对多关系基本相同。仔细回想一下一对多关系的设置,我们需要在“多”这一侧定义外键,这样SQLAlchemy 就会知道哪边是“多”的一侧。这时关系对“多”这一侧来说就是多对一关系。但是在邻接列表关系中,关系的两侧都在同一个模型中,这时SQLAlchemy就无法分辨关系的两侧。在这个关系函数中,通过将remote_side参数设为id字段,我们就把id字段定义为关系的远程侧(Remote Side),而replied_id就相应地变为本地侧(Local Side),这样反向关系就被定义为多对一,即多个回复对应一个父评论。集合关系属性replies中的cascade参数设为all,因为我们期望的效果是,当父评论被删除时,所有的子评论也随之删除。