Flask-SQLAlchemy拓展为SQLAlchemy提供了一层包装,也就是对象关系映射(ORM)。ORM允许应用通过类,对象和方法来管理数据库而不是表和SQL。ORM的工作就是将高级操作转换为数据库命令。
输入以下命令安装Flask-SQLAlchemy:
(venv) $ pip install flask-sqlalchemy
Flask-Migrate拓展对Alembic做了包装,可以跟踪数据库模式的变化。
输入以下命令安装Flask-Migrate:
(venv) $ pip install flask-migrate
将以下代码写入config.py文件:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
Flask-SQLAlchemy拓展从SQLALCHEMY_DATABASE_URI配置变量中获取应用程序数据库所在的位置。
在应用中数据库将以数据库实例表示,数据库迁移也一样。在app/__init__.py文件中导入配置类,Flask-SQLAlchemy和Flask-Migrate模块,在应用初始化的后面使用app.config配置对象提供的from_object()方法将配置类导入程序,然后创建Flask-SQLAlchemy和Flask-Migrate对象:
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from app import routes, models
app.models.py
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self):
return ''.format(self.username)
首先,从应用实例中导入数据库对象。
User类继承的db.Model是所有Flask-SQLAlchemy创建的模型的基类。这个类以类变量的形式定义了多个字段。字段是db.Column类的实例,接收字段类型为参数,加上其他可选的参数,比如说,允许定义哪些字段是不重复(unique)的,哪些是有索引(index,有助于提升查询效率)的。
__repr__方法告诉Python如何打印这个类的对象,在debug的时候很有用。下面是Python解释器中__repr__方法的应用:
>>> from app.models import User
>>> u = User(username='susan', email='[email protected]')
>>> u
Alembic可以在每次数据库结构有变化时,向仓库添加一个包含更改细节的迁移脚本。如果要将迁移应用到数据库,这些迁移脚本会按照创建的顺序执行。
执行以下命令,创建迁移仓库:
(venv) $ python manage.py db init
创建数据库迁移的方法有两种:手动和自动。要自动生成数据库迁移,Alembic会对比数据库模型定义的数据库结构和现有的数据库结构。然后生成可以使数据库结构与应用模型相符的迁移脚本。在这个例子中,由于目前还没有生成过数据库,所以自动迁移会将整个 User 模型添加到迁移脚本。 执行flask db migrate 子命令会创建以下迁移:
(venv) $ flask db migrate -m "users table"
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']'
Generating /home/username/projectname/migrations/versions/e517276bb1c2_users_table.py ... done
这条命令的输出的第一行和第二行一般可以忽略,后面几行表示它检测到的表和索引,最后一行表示它存储迁移脚本的位置。e517276bb1c2 表示这次迁移对应的唯一编码。命令中包含的 -m 选项是可选的,用于给迁移添加一条简短的描述。
flask db migrate 命令并不会对数据库做出任何更改,它只是用来生成迁移脚本的,要想向数据库应用更改,必须要使用 flask db upgrade 命令。
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> e517276bb1c2, users table
由于这个数据库用的是SQLite, 所以 upgrade 命令会检测到数据库还不存在,并会创建它(运行完这条命令后主目录会多出一个名叫app.db的文件,这是个SQLite数据库)。如果你用的是MySQL或者PostgreSQL时,你必须在运行 upgrade 命令前创建数据库。
注意Flask-SQLAlchemy默认使用”snake case“命名法。比如像上面的 User 模型,在数据库中对应的表名会是 user。 像 AddressAndPhone 模型类,表名会是 address_and_phone。如果你倾向于自定义表名, 可以向模型类添加名为 __tablename__ 属性,然后以字符串方式命名。
关系型数据库擅长存储数据项之间的关系。比如说用户写博客帖子的例子。用户在users表中会有一条记录,帖子在 posts表中会有记录。记录谁写了某个帖子最有效率的方法就是链接这两个相关的记录。
一旦建立了用户和帖子之间的联系,数据库就可以回答有关查询此连接的查询。SQLAlchemy可以帮助你处理”谁写了这个帖子“和”这个用户写了哪些帖子“这两种类型的查询。
app/models.py
from datetime import datetime
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return ''.format(self.username)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return ''.format(self.body)
新的Post类代表用户编写的博客帖子。timestamp字段会被编入索引,如果你希望以时间顺序检索帖子,这将非常有用。我还传递了 datetime.utcnow 函数作为默认参数。当你将某个函数作为默认值传递时, SQLAlchemy会将该字段设置为调用该函数的值(注意utcnow后面没有(),所以我传递的是函数本身,而不是调用它得到的结果)。一般在应用程序中会使用UTC日期和时间,确保无论用户在哪里,都使用统一的时间戳。这些时间戳在显示时会被转化为用户的当地时间。
user_id字段被初始化为user.id的外键,这意味着它应用users表中id的值。在某些实例例如 db.relationship()调用中,模型是由模型类引用的,通常以大写字符开头,而在其他情况下,比如db.ForeignKey()声明中,模型名由其数据库表名给出,SQLAlchemy默认使用小写字符,如果是多个单词组成的模型名,采用蛇形命名法(即不同单词之间由下划线_隔开,全部小写的命名法)。
User表有一个新的posts字段,由db.relationship初始化。这个字段并不是一个真实存在的数据库字段,而是用户与帖子之间关系的高级视图。在一对多关系中, db.relationship字段通常在”一“端定义,作为访问”多“端的简便方式。比如,我在u里存储了一个用户, u.posts 表达式会在数据库中查询这个用户所写的所有帖子。db.relationship 的第一个参数是在数据库关系中代表”多“端关系的模型类。backref参数定义添加到”多“端类对象并指回”一“端的字段的名称。lazy参数定义了数据库关系查询是如何加载的。
由于应用模型已经更新了,所以我们需要进行新的数据库迁移:
(venv) $ flask db migrate -m "posts table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'post'
INFO [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
Generating /home/username/projectname/migrations/versions/780739b227a7_posts_table.py ... done
然后把迁移应用到数据库:
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade e517276bb1c2 -> 780739b227a7, posts table
flask shell命令是一个很有用的工具, shell命令是继 run 命令之后第二个核心命令,这个命令的用处是在应用上下文环境中启动Python解释器:
(venv) $ python
>>> app
Traceback (most recent call last):
File "", line 1, in
NameError: name 'app' is not defined
>>>
(venv) $ flask shell
>>> app
一般情况下,app实例必须显式的导入,而当使用 flask shell 时,命令行会预先导入应用实例。
下面的函数创建了一个shell上下文,将数据库实例和模型导入shell会话。
from app import app, db
from app.models import User, Post
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
app.shell_context_processor装饰器将函数注册为shell上下文函数。 当运行flask shell命令时,它将调用此函数并在shell会话中注册它返回的键值对。函数返回字典而不是列表的原因是,对于每个键值对,你都得给出一个用于引用的名称,而它会由字典的键给出。
添加shell上下文处理器函数后你就可以直接操作数据库而不用导入了:
(venv) $ flask shell
>>> db
>>> User
>>> Post