这个系列是学习《Flask Web开发:基于Python的Web应用开发实战》的部分笔记html
对于用户提交的信息,包括 帐号、文章 等,须要可以将这些数据保存下来python
持久存储的三种方法:mysql
一般会使用数据库保存信息,并向数据库发起查询获取信息git
关系型数据库把数据存储在表中,表在程序中经过 Python 的类实现。例如,订单管理程序的数据库中可能有表 customers、products 和 orders。web
表的列数是固定的,行数是可变的。sql
列
定义表所表示的实体的数据属性
。例如,customers表中可能有 name、address、phone 等列。表中的行
定义各列对应的真实数据
。shell
表中有个特殊的列,称为主键
,其值为表中各行的惟一标识符
。表中还能够有称为外键
的列,引用同一个表或不一样表
中某行的主键
。行之间的这种联系称为关系
,这是关系型数据库模型的基础。数据库
从这个例子能够看出,关系型数据库存储数据很高效,并且避免了重复
。将这个数据库中的用户角色重命名也很简单,由于角色名只出如今一个地方。一旦在 roles 表中修改完角色名,全部经过 role_id 引用这个角色的用户都能当即看到更新。django
但从另外一方面来看,把数据分别存放在多个表中仍是很复杂的。生成一个包含角色的用户列表会遇到一个小问题,由于在此以前要分别从两个表中读取用户和用户角色,再将其联结起来。关系型数据库引擎为联结操做提供了必要的支持。flask
将数据分开存放在多个表中,经过外键创建联结。减小数据重复量。查询比较麻烦,但修改方便。
关系型数据库有:
MySQL
PostgreSQL
SQLite
比较特殊,是存储于硬盘上单个文件中的数据库。用一个文件保存每个数据库的全部数据。Python 自带。但同一时间只能有一个链接访问。因此强烈建议不要在一个生产环境的web应用中使用。
等
键-值对数据存储是基于散列映射的数据结构。
MongoDB
Riak
Apache CouchDB
Python 能够经过数据库接口程序(DB-API)
或对象关系映射(ORM)
访问关系数据库。
Python 程序能够经过 API 链接到目标数据库, 并用 SQL 语句进行数据读取操做
connect(),建立链接 close(),关闭数据库链接 commit(),提交 rollback(),回滚/取消当前
Python 的官方规范 PEP 0249
MySQL 和 PostgreSQL 是最多见的存储 Python web 应用数据的开源数据库。
惟一的 MySQL API:MySQLdb
有至少三个接口程序
建立数据库、将数据库的权限赋给某个/所有用户
CREATT DATABASE test;
GRANT ALL ON test.* to user;
选择要使用的数据库
USE test;
删除数据库
DROP DATABASE test;
建立表
CREAT TABLE users;
删除表
DROP TABLE users;
插入行
INSERT INTO users VALUES();
更新行
UPDATE users SET XXX;
删除行
DELETE FROM users ;
使用DB-API
访问数据库,须要懂 SQL 语言,可以写 SQL 语句,若是不想懂 SQL,又想使用关系型数据库,可使用 ORM
对象关系映射(Object Relational Mapping,简称ORM)
一个 ORM , 它的一端连着 Database, 一端连着 Python DataObject 对象。有了 ORM,能够经过对 Python 对象的操做,实现对数据库的操做,不须要直接写 SQL 语句。ORM 会自动将 Python 代码转换成对应的 SQL 语句。其他的操做,包括数据检查,生成 SQL 语句、事务控制、回滚等交由 ORM 框架来完成。
DataObject 能够经过 Query 执行产生,也能够经过用户本身建立产生。
固然,ORM 仍是能够执行原始的 SQL 语句,以便执行一些复杂的/特别的操做。
查找角色为 "User" 的全部用户: >>> user_role = Role(name='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'
数据库将不少 SQL 的功能抽象为 Python 对象,这样,不须要写 SQL 也能完成对数据库的操做。
在Flask 中经过 Python 的类定义数据库的表
from flask.ext.sqlalchemy import SQLAlchemy # 从 flask 扩展中导入 SQLAlchemy db = SQLAlchemy() class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer, primary_key=True) body = db.Column(db.Text) # 博客正文,不限长度 timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # 发布博文的时间 body_html = db.Column(db.Text) # 存放转换后的 HTML 代码 author_id = db.Column(db.Integer, db.ForeignKey('users.id')) # 外键使用 ForeignKey,指向 User 表的 id comments = db.relationship('Comment', backref='post', lazy='dynamic')
ORM 相似标准的数据库接口,但不少工做由 ORM 代为处理了,不须要直接使用接口。
Python 的 ORM 模块:SQLAlchemy 等
一些大型 web 开发工具/框架 有本身的 ORM 组件。
import os basedir = os.path.abspath(os.path.dirname(__file__)) # 项目根目录 SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') # 数据库文件的路径、文件名 # print SQLALCHEMY_DATABASE_URI # sqlite:////Users/chao/Desktop/projects/flask/flask_blog/app.db SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') # 文件夹,保存`SQLAlchemy-migrate`数据文件,也就是迁移策略文件 # print SQLALCHEMY_MIGRATE_REPO # /Users/chao/Desktop/projects/flask/flask_blog/db_repository
hello.py
from flask.ext.sqlalchemy import SQLAlchemy # 从 flask 扩展中导入 SQLAlchemy db = SQLAlchemy() # 建立数据库实例`db`
你要考虑如下几个因素。
API
和ORM
,显而后者取胜。对象关系映射(Object-Relational Mapper,ORM)
在用户不知觉的状况下把高层的面向对象操做转换成低层的数据库指令
。选择一个能直接操做低层数据库的抽象层,以防特定的操做须要直接使用数据库原生指令优化
。基于以上因素,本书选择使用的数据库框架是 Flask-SQLAlchemy,这个 Flask 扩展包装了SQLAlchemy框架。
在 ORM 中,模型
通常是一个 Python 类
, 表明数据库中的一张表, 类中的属性
对应数据库表中的列
。
Flask-SQLAlchemy 建立的数据库实例
为模型提供了一个基类db.Model
以及一系列辅助类和辅助函数,可用于定义 模型/表 的结构。
下面的例子定义了两个表,一个是用户角色,一个是用户信息
hello.py
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>' % self.username
类变量__tablename__
定义在数据库中使用的表名
。若是没有定义__tablename__
,Flask-SQLAlchemy 会使用一个默认名字,但默认的表名没有遵照 使用复数形式进行命名(加 s ) 的约定, 因此最好由咱们本身来指定表名。
其他的类变量都是该 模型的属性/表的列,被定义为 db.Column 类的实例。
db.Column 类构造函数的第一个参数是数据库表列/模型属性 的类型
。
db.Column 中其他的参数指定属性的配置选项
。
选项名 | 说 明 |
---|---|
primary_key | 若是设为 True,这列就是表的主键 |
unique | 若是设为 True,这列不容许出现重复的值 |
index | 若是设为 True,为这列建立索引,提高查询效率 |
nullable | 若是设为 True,这列容许使用空值;若是设为 False,这列不容许使用空值 |
default | 为这列定义默认值 |
Flask-SQLAlchemy 要求每一个模型都要定义主键,这一列常常命名为 id。ID 由 Flask-SQLAlchemy 控制。
虽然没有强制要求,但这两个模型都定义了__repr()__
方法,返回一个 具备可读性的字符串 表示 模型,可在调试和测试时使用。
学习如何使用模型的最好方法是在 Python shell 中实际操做。
首先,咱们要让 Flask-SQLAlchemy 根据模型类建立数据库
。方法是使用 db.create_all() 函数:
(venv) $ python hello.py shell # 进入 Python shell >>> from hello import db # 从`hello.py`导入建立的数据库实例 >>> db.create_all()
若是你查看程序目录,会发现新建了一个名为app.db
的文件。这个 SQLite 数据库文件
的名字就是在配置中指定的。若是数据库表已经存在于数据库中,那么 db.create_all() 不会从新建立或者更新这个表。若是在模型中作了修改,想要把改动应用到现有的数据库中,这一特性会带来不便。
更新现有数据库表的粗暴方式是先删除旧表
再从新建立:
>>> db.drop_all() >>> db.create_all()
遗憾的是,这个方法有个咱们不想看到的反作用,它把数据库中原有的数据都销毁
了。末尾将会介绍一种称为数据库迁移
的方式用于更新数据库。
>>> 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 属性也可以使用,虽然它不是真正的数据库列,但倒是一对多关系的高级表示。这些新建对象的 id 属性并无明确设定,由于主键是由 Flask-SQLAlchemy 管理
的。如今这些对象只存在于 Python 中
,还未写入数据库
。所以id 还没有赋值
:
>>> print(admin_role.id) None >>> print(mod_role.id) None >>> print(user_role.id) None
经过数据库会话
管理对数据库所作的改动,在 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()
再次查看 id 属性,如今它们已经赋值了:
>>> print(admin_role.id) 1 >>> print(mod_role.id) 2 >>> print(user_role.id) 3
数据库会话能保证数据库的一致性。提交操做使用原子方式把会话中的对象所有写入数据 库。若是在写入会话的过程当中发生了错误,整个会话都会失效。若是你始终把相关改动放 在会话中提交,就能避免因部分更新致使的数据库不一致性
。 一致性:数据库中数据与实际保存的数据不一致。
数据库会话也可回滚。调用 db.session.rollback() 后,添加到
数据库会话
中、还未提交的全部对象都会还原到它们在数据库中
的版本。
在数据库会话
上调用 add() 方法
也能更新模型
。咱们继续在以前的 shell 会话中进行操做
下面这个例子把 "Admin" 角色重命名为 "Administrator":
>>> admin_role.name = 'Administrator' >>> db.session.add(admin_role) >>> db.session.commit()
数据库会话还有个 delete() 方法。下面这个例子把 "Moderator" 角色从数据库中删除:
>>> db.session.delete(mod_role) >>> db.session.commit()
注意,删除
与插入
和更新
同样,提交数据库会话
后才会执行。
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_role = Role(name='User'), role=user_role [<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'
若是你退出了 shell 会话,前面这些例子中建立的对象就不会以 Python 对象的形式存在,而是做为各自数据库表中的行。若是你打开了一个新的 shell 会话,就要从数据库中读取行, 再从新建立 Python 对象。
下面这个例子发起了一个查询,加载名为 "User" 的用户角色:
>>> user_role = Role.query.filter_by(name='User').first()
filter_by() 等过滤器在 query 对象上调用,返回一个更精确的 query 对象。多个过滤器能够一块儿调用,直到得到所需结果。
可在 query 对象上调用的经常使用过滤器。
过滤器 | 说明 |
---|---|
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit() | 使用指定的值限制原查询返回的结果数量,返回一个新查询 |
offset() | 偏移原查询返回的结果,返回一个新查询 |
order_by() | 根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() | 根据指定条件对原查询结果进行分组,返回一个新查询 |
在查询上应用指定的过滤器后
,经过调用 all() 执行查询,以列表的形式返回结果。除了 all() 以外,还有其余方法能触发查询执行
。
经常使用查询执行函数
方法 | 说明 |
---|---|
all() | 以列表形式返回查询的全部结果 |
first() | 返回查询的第一个结果,若是没有结果,则返回 None |
first_or_404() | 返回查询的第一个结果,若是没有结果,则终止请求,返回 404 错误响应 |
get() | 返回指定主键对应的行,若是没有对应的行,则返回 None |
get_or_404() | 返回指定主键对应的行,若是没找到指定的主键,则终止请求,返回 404 错误响应 |
count() | 返回查询结果的数量 |
paginate() | 返回一个 Paginate 对象,它包含指定范围内的结果 |
关系和查询的处理方式相似。
完整的列表参见SQLAlchemy query
下面这个例子分别从关系的两端查询角色和用户之间的一对 多关系:
>>> users = user_role.users
>>> users
[<User u'susan'>, <User u'david'>] >>> users[0].role <Role u'User'>
这个例子中的 user_role.users 查询有个小问题。执行 user_role.users 表达式时,隐含的查询会调用 all() 返回一个用户列表。query 对象是隐藏的,所以没法指定更精确的查询 过滤器。就这个特定示例而言,返回一个按照字母顺序排序的用户列表可能更好。
在示例 5-4中,咱们修改了关系的设置,加入了lazy = 'dynamic'参数,从而禁止自动执行查询。
class Role(db.Model): # ... users = db.relationship('User', backref='role', lazy='dynamic') # ...
这样配置关系以后,user_role.users 会返回一个还没有执行的查询,所以能够在其上添加过 滤器:
>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>] >>> user_role.users.count() 2
在 Python shell 中作过练习后,能够直接在视图函数中进行数据库的操做了。
@app.route('/', methods=['GET', 'POST']) def index(): form = NameForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.name.data).first() if user is None: user = User(username = form.name.data) db.session.add(user) # 没有提交?? 配置对象中有一个选项,即 SQLALCHEMY_COMMIT_ON_TEARDOWN 键,将其设为 True 时,`每次请求结束后都会自动提交数据库中的变更` session['known'] = False else: session['known'] = True session['name'] = form.name.data form.name.data = '' return redirect(url_for('index')) return render_template('index.html', form = form, name = session.get('name'), known = session.get('known', False))
提交表单后,程序会使用filter_by()
查询过滤器在数据库中查找提交的名字。变量 known 被写入用户会话中,所以重定向以后,能够把数据传给模板, 用来显示自定义的欢迎消息。注意,要想让程序正常运行,你必须按照前面介绍的方法, 在 Python shell 中建立数据库表。
对应的模板新版本。这个模板使用 known 参数在欢迎消息中加入了第二行,从而对已知用户和新用户显示不一样的内容。
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky{% endblock %} {% block page_content %} <div class="page-header"> <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1> {% if not known %} <p>Pleased to meet you!</p> {% else %} <p>Happy to see you again!</p> {% endif %} </div> {{ wtf.quick_form(form) }} {% endblock %}
>>> from sqlalchemy import Column, Integer, String >>> class User(Base): ... __tablename__ = 'users' ... ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... ... def __repr__(self): ... return "<User(name='%s', fullname='%s', password='%s')>" % ( self.name, self.fullname, self.password)
让咱们考虑第二个表与User
关联,能够被映射和查询。Users 在能够存储任意数量的电子邮件地址关联的用户名。这意味着一个从users
到一个存储电子邮件地址的新表Addresses
的一对多
关联。咱们在Address
中使用声明定义这张表与User
的映射:
>>> from sqlalchemy import ForeignKey >>> from sqlalchemy.orm import relationship, backref >>> class Address(Base): ... __tablename__ = 'addresses' ... id = Column(Integer, primary_key=True) ... email_address = Column(String, nullable=False) ... user_id = Column(Integer, ForeignKey('users.id')) ... ... user = relationship("User", backref=backref('addresses', order_by=id)) ... ... def __repr__(self): ... return "<Address(email_address='%s')>" % self.email_address
上述类使用了ForeignKey
函数,它是一个应用于Column
的指令,代表这一列的值应该保存指定名称的远程列的值
。这是关系数据库的一个核心特征,是“胶水”,将本来无关的表变为有丰富的重叠关系的集合。上面的ForeignKey
表示,Addresses.user_id
列的值应该等于users.id
列中的值,即,users
的主键。
第二个函数,称为relationship()
, 它告诉 ORM ,Address
类自己应该使用属性Address.user
连接到User
类。relationship()
使用两个表之间的外键关系来肯定这个连接的性质,这个例子中,肯定Address.user
将要成为多对一
中多的一侧。relationship()
的参数中有一个称为backref()
的relationship()
的子函数,反向提供详细的信息, 即在users
中添加User
对应的Address
对象的集合,保存在User.addresses
中。多对一
关系的反向始终是一对多
的关系。一个完整的可用的relationship()
配置目录在基本关系模式。
两个互补关系, Address.user
和User.addresses
被称为一个双向关系,而且这是SQLAlchemy ORM
的一个关键特性。小节Linking Relationships with Backref详细讨论了“Backref”特性。
relationship()
的参数中,关联的远程类能够经过字符串指定,若是声明系统在使用。在上面的例子的User
类中,一旦全部映射完成,这些字符串被认为是用于产生实际参数的 Python 表达式。容许的名字在这个评估包括,除其余方面外,全部类的名称已被建立的声明的基础。
下面咱们举例说明,用User
代替Address
建立相同的 地址/用户 双向关系:
class User(Base): # .... addresses = relationship("Address", order_by="Address.id", backref="user")
经过relationship()
得到参数风格的更多详细信息。
你知道么?
如今,当咱们建立一个User
对象、将出现一个空白Addresses
集合。集合有不少类型,如sets
和词典,这里都有可能(详细信息Customizing Collection Access),但默认状况下,集合是一个Python列表
。
>>> jack = User(name='jack', fullname='Jack Bean', password='gjffdd') >>> jack.addresses []
咱们能够向User
对象自由的添加Address
对象。在这个例子中,咱们直接分配一个完整列表:
>>> jack.addresses = [ ... Address(email_address='jack@google.com'), ... Address(email_address='j25@yahoo.com')]
当使用一个双向关系时, 元素在一侧被添加后,会自动在出如今另外一侧。这种行为的发生,基于属性的改变
事件,而且由 Python 判断,不须要使用任何SQL语句:
>>> jack.addresses[1] <Address(email_address='j25@yahoo.com')> >>> jack.addresses[1].user <User(name='jack', fullname='Jack Bean', password='gjffdd')>
咱们将Jack Bean
添加到数据库会话,并提交到数据库。jack
以及相应的addresses
集合中的两个Address
成员都被一次性添加到会话中, 这使用了一个叫级联
的处理:
>>> session.add(jack) >>> session.commit()
INSERT INTO users (name, fullname, password) VALUES (?, ?, ?) ('jack', 'Jack Bean', 'gjffdd') INSERT INTO addresses (email_address, user_id) VALUES (?, ?) ('jack@google.com', 5) INSERT INTO addresses (email_address, user_id) VALUES (?, ?) ('j25@yahoo.com', 5) COMMIT
查询 jack, Jack杰克回来了。SQL中没有提到Jack的地址:
>>> jack = session.query(User).filter_by(name='jack').one() >>> jack <User(name='jack', fullname='Jack Bean', password='gjffdd')>
BEGIN (implicit) SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name = ? ('jack',)
让咱们看一下addresses
集合。观察SQL:
>>> jack.addresses
[<Address(email_address='jack@google.com')>, <Address(email_address='j25@yahoo.com')>]
SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE ? = addresses.user_id ORDER BY addresses.id (5,)
当咱们访问addresses
集合时,SQL忽然提到了。这是一个延迟加载的例子。addresses
集合如今被加载,而且行为就像一个普通的列表。咱们将讨论如何优化这个集合的加载。
一个parent对多个child,一对多关系添加一个外键到child
表,用于保存对应parent.id
的值,引用parent
。relationship()在parent
中指定,引用/保存 一批 child 表中关联的条目。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) children = relationship("Child") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) parent_id = Column(Integer, ForeignKey('parent.id'))
在一对多模式中,创建一个双向关系,ForeignKey
所在的是多,在relationship
中指定backref
选项:
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) children = relationship("Child", backref="parent") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) parent_id = Column(Integer, ForeignKey('parent.id'))
表child
将所以得到一个parent
属性, 值为对应的parent
表中的条目。
多个parent对一个child。多到一 在parent
表添加一个外键,保存child.id
的值。relationship()
在parent
中被宣告,建立一个新的属性child
,保存关联的child
表的条目。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child_id = Column(Integer, ForeignKey('child.id')) child = relationship("Child") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True)
双向行为的实现,是经过在relationship
中设置值为"parents"
的backref
可选参数。在Child
类中产生集合,收集parent
表中对应条目。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child_id = Column(Integer, ForeignKey('child.id')) child = relationship("Child", backref="parents")
一对一本质上是一种同时在两边设置一个数量的属性的双向关系。为了达到这个目标, 设置一个限制数量的属性uselist=False
替代关系的many
侧的集合。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child = relationship("Child", uselist=False, backref="parent") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) parent_id = Column(Integer, ForeignKey('parent.id'))
或者转换一个 一对多 引用 到 一对一,使用backref()
函数为反向
端提供uselist=False
参数:
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child_id = Column(Integer, ForeignKey('child.id')) child = relationship("Child", backref=backref("parent", uselist=False)) class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True)
多对多关系须要在两个类之间增长一个关系表
。关系表经过relationship()
的secondary
参数标识。一般,Table
使用基类的MetaData
对象关联宣告,因此ForeignKey
的声明能够定位链路远端的表。
association_table = Table('association', Base.metadata, Column('left_id', Integer, ForeignKey('left.id')), Column('right_id', Integer, ForeignKey('right.id')) ) class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary=association_table) class Child(Base): __tablename__ = 'right' id = Column(Integer, primary_key=True)
对于一个双向关系,关系两边都包含一个集合。backref
关键字将自动使用一样的secondary
参数用于反向关系:
association_table = Table('association', Base.metadata, Column('left_id', Integer, ForeignKey('left.id')), Column('right_id', Integer, ForeignKey('right.id')) ) class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary=association_table, backref="parents") class Child(Base): __tablename__ = 'right' id = Column(Integer, primary_key=True)
relationship()
的secondary
参数还接受一个能够返回最终参数的调用,只有当映射第一次使用时进行评估。使用这个,咱们能够在之后定义association_table
,只要在全部模块初始化完成后可以被调用:
class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary=lambda: association_table, backref="parents")
经过使用扩展的声明,传统的"表的字符串名称"被接受,匹配的表名存储在Base.metadata.tables
中:
class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary="association", backref="parents")
backref
关键字参数它实际在作什么?
让咱们先从标准的用户和地址情境开始了解:
from sqlalchemy import Integer, ForeignKey, String, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", backref="user") class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id'))
以上配置在User
中创建一个名为User.addresses
的,关联的Address
对象/条目的集合。它还在Address
中创建了一个user
属性,保存关联的User
条目。
事实上,backref
关键字只是一个常见的快捷方式, 用于将第二个relationship()
放置到关系另外一端的Address
, 同时在两边创建一个事件侦听器,在关系两边对属性操做进行镜像复制。以上配置至关于:
from sqlalchemy import Integer, ForeignKey, String, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", back_populates="user") class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id')) user = relationship("User", back_populates="addresses")
在上面,咱们明确地向Address
添加了一个名为User
的关系。在关系的两边,back_populates
参数将关联的对端信息告诉给每个relationship
,代表他们应该互相之间创建“双向”的行为。这个配置的主要做用是将事件处理程序添加到relationship
, ”当一个添加或设置事件发生时,设置使用这个属性名称传入属性”。这种行为说明以下。从一个User
和一个Address
的实例开始。addresses
集合是空的, 而且user
属性是None
:
>>> u1 = User() >>> a1 = Address() >>> u1.addresses [] >>> print a1.user None
不管如何,一旦Address
被添加到u1.addresses
集合,全部的集合和标量属性将被填充:
>>> u1.addresses.append(a1) >>> u1.addresses [<__main__.Address object at 0x12a6ed0>] >>> a1.user <__main__.User object at 0x12a6590>
这种行为在反向删除操做中固然也同样 ,一样两边等效操做。例如,当user
属性再次设置为None
,Address
对象从反向集合中被删除:
>>> a1.user = None >>> u1.addresses []
对addresses
集合和user
属性的操做,彻底发生在 Python 中, 没有任何与SQL数据库的交互。若是不这样处理, 须要将数据更新到数据库,而后在一个提交或过时操做发生后从新加载,才能在两边看到正确的状态。backref/back_populates
行为的优势是常见的双向操做能够反映正确的状态,不须要一个数据库往返。
记住,当在一个关系的一边使用backref
关键字,和上面 在关系的两边单独使用back_populates
是同样的。
咱们已经创建backref
关键字只是一个快捷方式,用于构建两个独立的relationship()
结构去引用对方。这个快捷方式的部分行为,是肯定 应用到relationship()
的配置参数 也将被应用到另外一个方向——即那些参数描述模式层面的关系,不太可能在相反的方向不一样。一般的状况是一个多对多关系,有一个secondary
参数,或者一对多或多对一有一个primaryjoin
参数。好比若是咱们限制列表中的Address
对象以tony
开头:
from sqlalchemy import Integer, ForeignKey, String, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", primaryjoin="and_(User.id==Address.user_id, " "Address.email.startswith('tony'))", backref="user") class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id'))
咱们能够观察到,经过检查生成的内容,关系的两边应用jion条件:
>>> print User.addresses.property.primaryjoin "user".id = address.user_id AND address.email LIKE :email_1 || '%%' >>> >>> print Address.user.property.primaryjoin "user".id = address.user_id AND address.email LIKE :email_1 || '%%' >>>
重用的参数都应该作“正确的事”——它只使用合适的参数,在 多对多 关系中,将对另外一端反向使用primaryjoin
和secondaryjoin
。
最多见的状况是, 咱们想在backref
端指定另外一端使用的参数。这包括relationship()
的参数,好比lazy
,remote_side
、cascade
、cascade_backrefs
。对于这种状况,咱们使用backref()
函数代替字符串:
# <other imports> from sqlalchemy.orm import backref class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", backref=backref("user", lazy="joined"))
上面,咱们仅在Address.user
放置一个lazy="joined"
参数,代表当一个针对Address
的查询发生,一个User
实例的 join 应自动每一个返回的Address
的user
属性填充。backref()
函数格式化的参数 咱们将它变成一种被relationship()
解释为应用到它建立的新关系的附加参数
。
在开发程序的过程当中,你会发现有时须要修改数据库模型
, 好比 增长表、添加列 ,并且修改以后还须要更新数据库
, 就须要对数据库进行迁移
更新表的更好方法是使用数据库迁移框架
。源码版本控制工具能够跟踪源码文件的变化, 相似地,数据库迁移框架能跟踪数据库模式的变化,而后增量式的把变化应用到数据库中
。
SQLAlchemy 的主力开发人员编写了一个迁移框架,称为 Alembic。除了直接使用 Alembic 以外,Flask 程序还可以使用 Flask-Migrate扩展。这个扩展对 Alembic 作了轻量级包装,并集成到 Flask-Script 中,全部操做都经过 Flask-Script 命令完成。
安装 Flask-Migrate:
(venv) $ pip install flask-migrate
初始化、配置 Flask-Migrate
from flask.ext.migrate import Migrate, MigrateCommand # ... migrate = Migrate(app, db) # 初始化 manager.add_command('db', MigrateCommand) # 在命令行中,用`db`调用`MigrateCommand`
➜ flask_blog git:(master) ✗ python run.py usage: run.py [-?] {shell,db,runserver} ... positional arguments: {shell,db,runserver} shell Runs a Python shell inside Flask application context. db Perform database migrations runserver Runs the Flask development server i.e. app.run() optional arguments: -?, --help show this help message and exit
为了导出数据库迁移命令
,Flask-Migrate 提供了一个 MigrateCommand 类,可附加到 Flask- Script 的 manager 对象上。在这个例子中,MigrateCommand 类使用 db 命令附加。
在维护数据库迁移以前,要使用 init 子命令建立迁移仓库
:
(venv) $ python hello.py db init # 将向应用添加一个`migrations`文件夹。文件夹中的文件须要和其余源文件一块儿进行版本控制。➜
flask_blog git:(master) ✗ python run.py db init
Creating directory /Users/chao/Desktop/projects/flask/flask_blog/migrations ... done
Creating directory /Users/chao/Desktop/projects/flask/flask_blog/migrations/versions ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/alembic.ini ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/env.py ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/env.pyc ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/README ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/script.py.mako ... done
Please edit configuration/connection/logging settings in '/Users/chao/Desktop/projects/flask/flask_blog/migrations/alembic.ini' before proceeding.
这个命令会建立 migrations 文件夹
,全部迁移脚本
都存放其中。
数据库迁移仓库中的文件要和程序的其余文件一块儿归入版本控制。
在 Alembic 中,数据库迁移用迁移脚本
表示。脚本中有两个函数,分别是 upgrade() 和 downgrade()。upgrade() 函数把迁移中的改动应用到数据库中
,downgrade() 函数则将改动删除
。Alembic 具备添加和删除改动的能力,所以数据库可重设到修改历史的任意一点。
咱们可使用 revision 命令手动建立 Alembic 迁移,也可以使用 migrate 命令自动建立。
手动建立的迁移只是一个骨架,upgrade() 和 downgrade() 函数都是空的,开发者要使用 Alembic 提供的 Operations 对象指令实现具体操做。
自动建立的迁移会根据模型定义
和数据库当前状态
之间的差别生成 upgrade() 和 downgrade() 函数的内容。
自动建立的迁移不必定老是正确的,有可能会漏掉一些细节。自动生成迁移 脚本后必定要进行检查。
migrate 子命令用来自动建立迁移脚本:
(venv) $ python hello.py db migrate -m "initial migration" # 生成一个初始的迁移 INFO [alembic.migration] Context impl SQLiteImpl. INFO [alembic.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate] Detected added table 'roles' INFO [alembic.autogenerate] Detected added table 'users' INFO [alembic.autogenerate.compare] Detected added index 'ix_users_username' on '['username']' Generating /home/flask/flasky/migrations/versions/1bc 594146bb5_initial_migration.py...done
➜ flask_blog git:(master) ✗ python run.py db migrate -m 'migration' INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'user' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']' INFO [alembic.autogenerate.compare] Detected added table 'post' Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/versions/0fb164ef6c11_migration.py ... done
检查并修正好迁移脚本以后,咱们可使用db upgrade命令把迁移应用到数据库
中:
(venv) $ python hello.py db upgrade
INFO [alembic.migration] Context impl SQLiteImpl. INFO [alembic.migration] Will assume non-transactional DDL. INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration
➜ flask_blog git:(master) ✗ python run.py db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 0fb164ef6c11, migration
对第一个迁移来讲,其做用和调用 db.create_all() 方法同样
。但在后续的迁移中, upgrade 命令能把改动应用到数据库中,且不影响其中保存的数据
。
原文链接:http://www.jianshu.com/p/0c88017f9b46