第四章 数据库
在实际网站开发中,开发者在本地进行开发,测试成功后再把新版本的功能发布的正式应用环境。不断更新升级的过程,怎么能快速的在本地及正式环境中同步数据呢?那就需要下边的知识——数据库迁移技术。
一、 Flask架构中的数据库
其实在FLASK中,是没有自带数据库功能,但是,它可以与各种流行的数据库软件轻易结合,它把这种选择留给开发人员,这正是Flask的灵活支持。
虽然可供选择的数据库很多,比如关系型数据:mysql、PostgreSQL、mssql、oracle等,还有非关系型数据库。但是,本示例还是选用轻巧的关系型数据库sqlite(没有数据库引擎)。如果要把此应用发布到生产环境,仅需要把数据库引擎切换到其他类型的数据库。
第三章,我们就引进了几个flask的扩充库。这次,为了更好的管理数据库操作,需要一个第三方的库flask-sqlalchemy。
介绍下ORM
ORM 全称 Object Relational Mapping, 翻译过来叫对象关系映射。简单的说,ORM 将数据库中的表与面向对象语言中的类建立了一种对应关系。这样,我们要操作数据库,数据库中的表或者表中的一条记录就可以直接通过操作类、对象、方法或者类实例来完成。
SQLAlchemy 是Python 社区最知名的 ORM 工具之一,为高效和高性能的数据库访问设计,实现了完整的企业级持久模型。它可以适用各种数据库。
安装flask-sqlalchemy。 注意:先要激活虚拟环境再安装。
pip install flask-sqlalchemy
二、DATABASE Migrate数据库迁移
个人理解:这里的迁移不是仅指数据库文件的整体迁移(备份及还原、附加数据库等),也包含微小数据结构改变操作。
实际应该是所有数据库操作动作对应的语言脚本,通过这些脚本可以快速重复同样的操作。
很多的数据教程会提到怎么新建、使用数据库操作,但是当APP应用的数据结构变化或扩展后的相关操作,教程很少会提及。
尤其关系型数据库的关系结构复杂,一旦结构改变,相应的数据就需要对应改变。
安装flask-migrate
pip install flask-migrate 注意:先要激活虚拟环境再安装。
三、Flask-SQLAlchemy设置
1、设置SQLITE数据库引擎uri
如前边的章节介绍一样,把此处的设置内容,增添到config.py文件中。
path.dirname:去掉路径中的文件名称。
os.path.abspath:获取绝对路径。
file:当前文件的路径。
import os
basedir = os.path.abspath(os.path.dirname(__file__)) #__file__用来获得模块所在的路径.
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ #获取环境变量为DATABASE_URL值(windows系统中环境变量的path参数)
'sqlite:///' + os.path.join(basedir, 'app.db') #拼接一个sqlite接口引擎
SQLALCHEMY_TRACK_MODIFICATIONS = False #设置为False后,不会记录数据的变更记录。
2、初始化DB对象及迁移对象
app/init.py
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) #创建DB引擎
migrate = Migrate(app, db) #创建Migratey引擎
from app import routes, models #models是下边将建立的模块
Tip:更多的FLASK扩展也会像db、migrate这两个一样的初始化。
四、数据库模型(类似关系型数据库表结构)
用户表:id、username、email、password_hash。
为什么hash化password。主要是如果直接把密码明文存储,一旦数据库被黑客攻陷,那这对用户来说是灾难性的。所以,通过hash化密码,可以提升安全性。具体的讲解放到后边的章节。
新建一个模块:app/models.py,增加两个类(表结构):
这里请注意:如果完全按照教程中的代码,会遇到一个坑:在提交的时候,后台语句会报错,提示sqlite数据库中没有user表。所以,先用db.create_all()启动。
db.Column的参数说明:
primary_key: 如果设为 True,这列就是表的主键;
unique:如果设为 True,这列不允许出现重复的值;
index:如果设为 True,为这列创建索引,提升查询效率;
nullable:如果设为 True,这列允许使用空值;如果设为 False,这列不允许使用空值;
default:为这列定义默认值.
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 '<User {}>'.format(self.username)
五、生成迁移库
上边,我们已经建立一个数据表结构。但是,随着开发的进一步发展,可能会增加字段或删除一些项目。
Alembic(适用Flask-Migrate的迁移框架,Flask作者所作)可以不用重新构建表而改变schema(结构)。
Alembic包含一个迁移库,这个库有一个由这些迁移脚本组成目录。只要数据库结构一改变,记录这些变动的迁移脚本将添加到迁移库中。
类似第一章运行flask run 一样:
flask db init
另外用Pycharm配置后直接运行:
初始化后flask自动生成的语句如下:
Creating directory /home/miguel/microblog/migrations ... done
Creating directory /home/miguel/microblog/migrations/versions ... done
Generating /home/miguel/microblog/migrations/alembic.ini ... done
Generating /home/miguel/microblog/migrations/env.py ... done
Generating /home/miguel/microblog/migrations/README ... done
Generating /home/miguel/microblog/migrations/script.py.mako ... done
Please edit configuration/connection/logging settings in
'/home/miguel/microblog/migrations/alembic.ini' before proceeding.
六、第一个数据库迁移
上边建立迁移库,现在我们开始建立一个数据的迁移。
Alembic 会自动检测数据库model与实际数据库里的变动差异。由于先前没有建立过user的schema,迁移脚步会自动增加建立一个完整的user的脚本。
-m “users table”:增加migrate的简短描述。
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
#e517276bb1c2是系统自动生成的迁移版本的版本号 /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done
虽然运行了migrate,但是,此语法不能影响实际数据库的数据,它仅仅是生成了迁移脚本。
如果要通过迁移改变实际数据库的数据,需要用到如下两个函数
upgrade()和downgrade()。
upgrade():运用迁移脚本;
downgrade():移除当前脚本,可回溯到以往的历史节点。
七、数据库升级和降级
假设你开发一个APP应用,先是在本地进行开发。如果model发生了变化,在没有迁移功能情况下,你就需要在开发环境中修改model,同时在生产环境中修改model。
有了迁移功能,在开发环境中,我们就先执行migrate,然后审查这些变动能否正常运行。最后运行upgrade来更新这些变动。
如果发现有问题,可以运行downgrade回到先前的某个更新节点。
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
Tip:Flask-SQLAlchemy采用蛇形命名规则(用下划线连接多个单词)来命名数据库表名称。
例如上边的User模型,则会在数据库中建立一个user表。
如果要改变这默认的表名称,可以在模型类增加一个属性__tablename__,并赋值你所期望的名称。
八、数据库表关系
在关系数据库中,可以在“一”表中建立一个变量Post,用来构建user与Post的对应关系。
from datetime import datetime
from app import db #db是上边创建
from flask_login import UserMixin
#注意添加UserMixin,否则在提交注册的时候报错:User没有active这个属性。
class User(db.Model,UserMixin):
db.create_all() #这个语句是教程中没有,必须添加
id = db.Column(db.Integer, primary_key=True) #db.Column定义列字段:第一个参数字段类型,第二个是主键
username = db.Column(db.String(64), index=True, unique=True) #unique代表唯一性
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
#relationship生成一个关系,posts不是一个数据库的字段,仅是一个视图关系
#在一对多的模式下,第一个参数是代表多方的类(表)名称
#第二个参数backref,将向post类中添加一个author属性,从而定义反向关系。这一属性可替代user_id 访问user模型,此时获取的是模型对象,而不是外键的值(类似表的一行或一个实例)。
#u = User.query.get(1)
#author使用的例子:p = Post(body='my first post!', author=u)
posts = db.relationship('Post', backref='author', lazy='dynamic')
#__repr__主要是自定义一种输出格式,重组print的打印效果。另外一种__str__是更上一层的输出格式。
#终端用户显示使用__str__,而程序员在开发期间则使用底层的__repr__来显示
def __repr__(self):
return '<User {}>'.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 '<Post {}>'.format(self.body)
九、玩乐时间
下边的实例是在python自带的shell中运行。
>>> from app import db
>>> from app.models import User, Post
>>> u = User(username='john', email='john@example.com')
>>> db.session.add(u) #先建立回话后才可以进行数据库操作:新增实例
>>> db.session.commit() #执行操作
>>> u = User(username='susan', email='susan@example.com')
>>> db.session.add(u)
>>> db.session.commit()
>>> users = User.query.all() #查询user所有记录
>>> users
[<User john>, <User susan>]
>>> for u in users:
... print(u.id, u.username)
...
1 john
2 susan
>>> u = User.query.get(1) #查询主键是1的记录
>>> u
<User john>
>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u) #按照author赋值POST
>>> db.session.add(p)
>>> db.session.commit()
>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]
>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]
>>> # print post author and body for all posts
>>> posts = Post.query.all()
>>> for p in posts:
... print(p.id, p.author.username, p.body)
...
1 john my first post!
# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User susan>, <User john>]
>>> users = User.query.all()
>>> for u in users:
... db.session.delete(u)
...
>>> posts = Post.query.all()
>>> for p in posts:
... db.session.delete(p)
...
>>> db.session.commit()