Flask学习之四 数据库
英文博客地址:http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iv-database
中文翻译地址:http://www.pythondoc.com/flask-mega-tutorial/database.html
开源中国社区:www.oschina.net/translate/the-flask-mega-tutorial-part-iv-database
一、Flask中的数据库
教程:将使用 Flask-SQLAlchemy 扩展来管理我们应用程序的数据。这个扩展封装了 SQLAlchemy 项目,这是一个 对象关系映射器 或者 ORM。
大多数数据库教程会涉及到创建和使用一个数据库,但没有充分讲述随着应用程序扩大更新数据库的问题。通常情况下,每次你需要进行更新,你最终不得不删除旧 的数据库和创建一个新的数据库,并且失去了所有的数据。如果数据不能容易地被重新创建,你可能会被迫自己编写导出和导入脚本。
我们将使用 SQLAlchemy-migrate 来跟踪数据库的更新。它只是在开始建立数据库的时候多花费些工作,这只是很小的代价,以后就再不用担心人工数据迁移了。
二、配置
教程采用的数据库是sqlite 数据库。sqlite 数据库是小型应用的最方便的选择,每一个数据库都是存储在单个文件里。
有许多新的配置项需要添加到配置文件中(文件 config.py):
import os basedir = os.path.abspath(os.path.dirname(__file__)) SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
SQLALCHEMY_DATABASE_URI 是 Flask-SQLAlchemy 扩展需要的。这是我们数据库文件的路径。
SQLALCHEMY_MIGRATE_REPO 是文件夹,我们将会把 SQLAlchemy-migrate 数据文件存储在这里。
当我们初始化应用程序的时候,我们也必须初始化数据库。修改初始化文件(文件 app/__init__.py):
from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy app = Flask(__name__) app.config.from_object('config') db = SQLAlchemy(app) from app import views, models
这里的改变有两个。一是创建了一个 db 对象,这是我们的数据库,接着导入一个新的模块,叫做 models。接下来我们将编写 models。
三、数据库模型
我假设大家能学过数据库。
用户表如下图:
表中共有4个字段。
id:主键
nickname :字符串,并且指定了最大的长度以便数据库可以优化空间占用。
email :字符串,并且指定了最大的长度以便数据库可以优化空间占用。
role :整型,表示哪个用户是管理员,哪个不是。
把它转换成代码(文件 app/models.py):
from app import db ROLE_USER = 0 ROLE_ADMIN = 1 class User(db.Model): id = db.Column(db.Integer, primary_key = True) nickname = db.Column(db.String(64), index = True, unique = True) email = db.Column(db.String(120), index = True, unique = True) role = db.Column(db.SmallInteger, default = ROLE_USER) def __repr__(self): return '<User %r>' % (self.nickname)
User类把我们刚刚创建的几个字段定义为类变量。字段使用db.Column类创建实例,字段的类型作为参数,另外还提供一些其他可选参数。例如,标识字段唯一性和索引的参数.
__repr__方法告诉Python如何打印class对象,方便我们调试使用。
四、创建数据库
教程:配置以及模型都已经到位了,是时候准备创建数据库文件。SQLAlchemy-migrate 包自带命令行和 APIs,这些 APIs 以一种将来允许容易升级的方式来创建数据库。我发现命令行使用起来比较别扭,因此我们自己编写一些 Python 脚本来调用迁移的 APIs。
创建数据库的脚本(文件 db_create.py):
#!flask/bin/python from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO from app import db import os.path db.create_all() if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository') api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) else: api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, api.version(SQLALCHEMY_MIGRATE_REPO))
这个文件放在根目录下。
注意这个脚本是完全通用的,所有的应用路径名都是从配置文件读取的。当你用在自己的项目时,你可以把脚本拷贝到你app`s目录下就能正常使用了。
创建数据库:
chmod a+x db_create.py ./db_create.py
运行这条命令之后,文件夹中就多了一个新的app.db文件,这是个支持迁移的空sqlite数据库。同时也会生成一个带有几个文件的 db_repository目录,这是SQLAlchemy-migrate存储数据库文件的地方,注意如果数据库已存在它就不会再重新生成了。这将帮助 我们在丢失了现有的数据库后,再次自动创建出来。
五、第一次迁移
既然已经定义好了model,也把它和数据库做了关联,接下来初次尝试下做一个改变应用数据库结构的一次迁移,这将帮助我们从一个空的数据库变成一个可以存储users信息的数据库。
首先需要一个迁移脚本(文件 db_migrate.py):
#!flask/bin/python import imp from migrate.versioning import api from app import db from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO migration = SQLALCHEMY_MIGRATE_REPO + '/versions/%03d_migration.py' % (api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) + 1) tmp_module = imp.new_module('old_model') old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) exec old_model in tmp_module.__dict__ script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, tmp_module.meta, db.metadata) open(migration, "wt").write(script) api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) print 'New migration saved as ' + migration print 'Current database version: ' + str(api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO))
脚本代码略长,慢慢看。
教程:SQLAlchemy-migrate通过对比数据库的结构(从app.db文件读取)和models结构(从app/models.py文件读取)的方 式来创建迁移任务,两者之间的差异将作为一个迁移脚本记录在迁移库中,迁移脚本知道如何应用或者撤销一次迁移,所以它可以方便的升级或者降级一个数据库的 格式。
然而在使用上面的脚本自动地完成迁移的时候也不是没有问题的,我见过有时候它很难识别新老格式的变化。为了让 SQLAlchemy-migrate 容易地识别出变化,我绝不会重命名存在的字段,我仅限于增加或者删除模型或者字段,或者改变已存在字段的类型。当然我一直会检查生成的迁移脚本,确保它是 正确。
毋庸置疑你不应该在没有备份下去尝试迁移数据库。当然也不能在生产环境下直接运行迁移脚本,必须在开发环境下确保迁移运转正常。
然后,执行吧:
chmod a+x db_migrate.py ./db_migrate.py
六、数据库升级和回退
假设你有一个应用程序在开发机器上,同时有一个拷贝部署在到线上的生产机器上。在下一个版本中,你的数据模型有一个变化,比如新增了一个表。如果没有迁移脚本,你可能必须要琢磨着如何修改数据库格式在开发和生产机器上,这会花费很大的工作。
如果有数据库迁移的支持,当你准备发布新版的时候,你只需要录制一个新的迁移,拷贝迁移脚本到生产服务器上接着运行脚本,所有事情就完成了。数据库升级也只需要一点 Python 脚本(文件 db_upgrade.py):
#!flask/bin/python from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) print 'Current database version: ' + str(api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO))
当你运行上述脚本的时候,数据库将会升级到最新版本。
通常情况下,没有必要把数据库降低到旧版本,但是,SQLAlchemy-migrate 支持这么做(文件 db_downgrade.py):
#!flask/bin/python from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v - 1) print 'Current database version: ' + str(api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO))
这个脚本会回退数据库一个版本。运行多次则可以回退多个版本。
七、数据库关系
学过数据库应该知道数据库关系指的是什么,我假设大家能看到下面的图:
修改(app/models.py):
from app import db ROLE_USER = 0 ROLE_ADMIN = 1 class User(db.Model): id = db.Column(db.Integer, primary_key = True) nickname = db.Column(db.String(64), unique = True) email = db.Column(db.String(120), unique = True) role = db.Column(db.SmallInteger, default = ROLE_USER) posts = db.relationship('Post', backref = 'author', lazy = 'dynamic') def __repr__(self): return '<User %r>' % (self.nickname) class Post(db.Model): id = db.Column(db.Integer, primary_key = True) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) def __repr__(self): return '<Post %r>' % (self.body)
我们添加了一个 Post 类,这是用来表示用户编写的 blog。在 Post 类中的 user_id 字段初始化成外键,因此 Flask-SQLAlchemy 知道这个字段是连接到用户上。
值得注意的是我们已经在 User 类中添加一个新的字段称为 posts,它是被构建成一个 db.relationship 字段。这并不是一个实际的数据库字段,因此是不会出现在上面的图中。对于一个一对多的关系,db.relationship 字段通常是定义在“一”这一边。在这种关系下,我们得到一个 user.posts 成员,它给出一个用户所有的 blog。
运行迁移脚本:
./db_migrate.py
八、测试