写在前面
本文学习来源The-Flask-Mega-Tutorial-zh,学习如何使用数据库,再次表达对作者和译者的感谢,正是因为他们,才能学习到这么好的免费教程。本机环境:选用的Makedown编辑器为Atom,实验环境为Ubuntu18.04,Python版本为 3.7.1本篇仅作为自己Flask入门的记录,想通过此来记录代码和自己不懂的概念。
Flask中数据库插件
Flask本身不支持数据库,我们可以通过使用数据库插件来完成此项功能。数据库大都提供了Python的客户端包,它们被分为了两大类:关系数据库和非关系型数据库(nosql),一般说来,nosql是为了处理杂乱的非结构化数据来设计的,而SQL则更多的用于结构化数据的应用程序。
本章中,我们要用到两个Flask扩展,第一个插件是Flask-SQLAlchemy,第二个插件是Flask-Migrate,是SQLAlchemy的一个数据库迁移框架。安装命令如下(确认自己激活了虚拟环境):
pip3 install flask-sqlalchemy
pip3 install flask-migrate
SQLAchemy配置
开发阶段,使用SQLite数据库,SQLite数据库是开发小型乃至中型应用最方便的选择。下面修改配置文件。
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, 'my_app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
basedir = os.path.abspath(os.path.dirname(__file__))
是返回脚本的路径。Flask-SQLAlchemy
插件从SQLALCHEMY_DATABASE_URI
配置变量中获取应用的数据库的位置。首先从环境变量获取,没有就使用默认位置
SQLALCHEMY_TRACK_MODIFICATIONS
用于设置数据发生变更之后是否发送信号给应用,此处不需要。
数据库在应用中表现为数据库实例,需修改__init__.py
:
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
my_app = Flask(__name__)
my_app.config.from_object(Config)
my_db = SQLAlchemy(my_app)
migrate = Migrate(my_app,my_db)
from app import routes, models
在初始化脚本中,首先引入了数据库支持模块,并创建了实例化对象my_db来表示数据库,后添加了数据库引擎,最后导入了models
模块来定义数据库结构。
数据库模型
数据模型:定义一张表及其字段的类,先让我们定义一个用户模型。id
存在于所有模型并用做主键,每个用户都被分配一个id值;username
,email
和password_hash
字段被定义为字符串,并指定最大长度,模型存在于models.py
中,代码如下:
from app import my_db
class User(my_db.Model):
id = my_db.Column(my_db.Integer, primary_key=True)
username = my_db.Column(my_db.String(64), index=True, unique=True)
email = my_db.Column(my_db.String(120), index=True, unique=True)
password_hash = my_db.Column(my_db.String(128))
def __repr__(self):
return('<User {}>'.format(self.username))
User类继承自my_db.Model,它是Flask-SQLAlchemy中所有模型的基类。字段被创建为my_db.Column
类的实例,它传入字段类型以及其他可选参数。
而__repr__
这个特殊方法可以在调试时打印用户实例。测试情况如下:
>>> from app.models import User
>>> u = User(username='devil', email='devil@gmail.com')
>>> u
<User devil>
>>> u.username
'devil'
>>> u.email
'devil@gmail.com'
如果可以达到这一步测试,那我们可以开始来创建数据库迁移储存库了。
创建数据库迁移存储库
在开发过程中,我们需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式就是删除旧表,但这样会丢失数据。更好的解决办法是使用数据库迁移框架。在Flask中可以使用Flask-Migrate扩展,来实现数据迁移。Flask-Migrate添加了flask db子命令来管理与数据库迁移相关的所有事情。 那么让我们通过运行flask db init来创建microblog的迁移存储库,代码如下:
$ flask db init
Creating directory /home/evil/microblog/migrations ... done
Creating directory /home/evil/microblog/migrations/versions ... done
Generating /home/evil/microblog/migrations/script.py.mako ... done
Generating /home/evil/microblog/migrations/alembic.ini ... done
Generating /home/evil/microblog/migrations/env.py ... done
Generating /home/evil/microblog/migrations/README ... done
Please edit configuration/connection/logging settings in
'/home/evil/microblog/migrations/alembic.ini' before proceeding.
第一次数据库迁移
包含映射到User
数据库模型的用户表的迁移存储库生成后,可以创建第一次数据库迁移了。可以通过手动或自动。 要自动生成迁移,Alembic会将数据库模型定义的数据库模式与数据库中当前使用的实际数据库模式进行比较。 然后,使用必要的更改来填充迁移脚本,以使数据库模式与应用程序模型匹配。 当前情况是,由于之前没有数据库,自动迁移将把整个User模型添加到迁移脚本中。 flask db 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 /home/evil/microblog/migrations/versions/2e70cde282af_users_table.py
... done
从输出信息可以看到输出了一个User
表和俩个索引,给出了迁移脚本的输出路径,2e70cde282af
则是自动生成的迁移标识,之后我们使用flask db upgrade
更新数据库。
$ 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 -> 2e70cde282af, users table
(venv)
观察自己的文件目录发现多出了一个my_app.db的文件,这是因为我们使用了SQLite,所以upgrade命令检测到数据库不存在时,会创建它。在使用类似MySQL和PostgreSQL的数据库服务时,必须在运行upgrade之前在数据库服务器上创建数据库。
数据库关系
关系数据库擅长存储数据项之间的关系。 考虑用户发表动态的情况, 用户将在user表中有一个记录,并且这条用户动态将在post表中有一个记录。 标记谁写了一个给定的动态的最有效的方法是链接两个相关的记录。
也就是说你看到一个动态,想知道这个动态是谁发布的,或者知道一个用户,想看到他的所有动态,就可以在数据库中查询
下面对对数据库扩展以支持用户动态,设计一个新表post,post将具有必须的id
、用户动态的body
和timestamp
字段。 并在此基础上添加了一个user_id
字段,将该用户动态链接到其作者。 由于用户有唯一的id主键, 将用户动态链接到其作者的方法是添加对用户id的引用,这正是user_id字段所在的位置。 这个user_id字段被称为外键。
下面对app/models.py
进行修改:
# -*- coding: utf-8 -*-
from datetime import datetime
from app import my_db
class User(my_db.Model):
id = my_db.Column(my_db.Integer, primary_key=True)
username = my_db.Column(my_db.String(64), index=True, unique=True)
email = my_db.Column(my_db.String(120), index=True, unique=True)
password_hash = my_db.Column(my_db.String(128))
posts = my_db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
class Post(my_db.Model):
id = my_db.Column(my_db.Integer, primary_key=True)
body = my_db.Column(my_db.String(140))
timestamp = my_db.Column(my_db.DateTime, index=True, default=datetime.utcnow)
user_id = my_db.Column(my_db.Integer, my_db.ForeignKey('user.id'))
def __repr__(self):
return '<Post {}>'.format(self.body)
Post
类表示用户的发表动态,中间有timestamp
字段被编入索引,可以按时间顺序检索用户动态。通常,在服务应用中使用UTC日期和时间是推荐做法。 这可以确保你使用统一的时间戳,无论用户位于何处,这些时间戳会在显示时转换为用户的当地时间。
user_id
字段被初始化为user.id
的外键,这意味着它引用了来自用户表的id值。本处的user
是数据库表的名称,Flask-SQLAlchemy自动设置类名为小写来作为对应表的名称。 User
类有一个新的posts
字段,用my_db.relationship
初始化。这不是实际的数据库字段,而是用户和其动态之间关系的高级视图,因此它不在数据库图表中。对于一对多关系,my_db.relationship
字段通常在“一”的这边定义,并用作访问“多”的便捷方式。因此,如果我有一个用户实例u,表达式u.posts将运行一个数据库查询,返回该用户发表过的所有动态。 db.relationship
的第一个参数表示代表关系“多”的类。 backref
参数定义了代表“多”的类的实例反向调用“一”的时候的属性名称。这将会为用户动态添加一个属性post.author
,调用它将返回给该用户动态的用户实例。 lazy
参数定义了这种关系调用的数据库查询是如何执行的
一旦变更了应用模型,就需要生成一个新的数据库迁移:
$ 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/evil/microblog/migrations/versions/3db91c590389_posts_table.py
... done
使用flask db upgrade
迁移到数据库
$ 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 2e70cde282af -> 3db91c590389, posts table
(venv)
Demo
现在让我们在python交互环境中进行测试,导入数据库实例和模型:
>>> from app import my_db
>>> from app.models import User, Post
创建一个新用户:
>>> u = User(username='evil',email='evil@gmail.com')
>>> my_db.session.add(u)
>>> my_db.session.commit()
db.session
进行访问验证。,一旦所有更改都被注册,你可以发出一个指令db.session.commit()
来以原子方式写入所有更改。 如果在会话执行的任何时候出现错误,调用db.session.rollback()
会中止会话并删除存储在其中的所有更改。 要记住的重要一点是,只有在调用db.session.commit()
时才会将更改写入数据库。
下面添加一个新用户:
>>> u = User(username='Devil',email='Devil@gmail.com')
>>> my_db.session.add(u)
>>> my_db.session.commit()
进行查询操作:
>>> users = User.query.all()
>>> users
[<User evil>, <User Devil>]
>>> [print(u.id,u.username,u.email) for u in users]
1 evil evil@gmail.com
2 Devil Devil@gmail.com
[None, None]
query
属性是数据库查询的入口,最基本的查询就是返回该类的所有元素,在添加市,用户的id
字段被设置为1和2。得到了用户id,我们现在可以直接获取用户实例:
>>> u=User.query.get(1)
>>> u
<User evil>
# 添加用户动态
>>> p = Post(body='My first post!',author = u)
>>> my_db.session.add(p)
>>> my_db.session.commit()
在User
类中创建的my_db.relationship
为用户添加了posts
属性,并为用户动态添加了author
属性。 我使用author虚拟字段来调用其作者,而不必通过用户ID来处理。
现在看一下另外的数据库查询栗子:
# get all posts written by a user
>>> u=User.query.get(1)
>>> u
<User evil>
>>> 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 Devil>
>>> posts = u.posts.all()
>>> posts
[]
# print post author and body for all posts
>>> posts = Post.query.all()
>>> [print(p.id, p.author.username, p.body) for p in posts]
1 evil My first post!
[None]
# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User evil>, <User Devil>]
>>> User.query.order_by(User.username).all()
[<User Devil>, <User evil>]
具体操作可以参考Flask-SQLAlchemy
下面清除这些测试用户和用户动态:
>>> users = User.query.all()
>>> [my_db.session.delete(u) for u in users]
[None, None]
>>> posts = Post.query.all()
>>> [my_db.session.delete(p) for p in posts]
[None]
>>> my_db.session.commit()
>>>
补充姿势
在每次启动Python解释器之后,第一件事是运行两条导入语句:
>>> from app import my_db
>>> from app.models import User, Post
很烦有木有,这种反人类的事情总会有解决方案。flask shell
就是这样的让我们看个栗子:
>>> app
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined
$ flask shell
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
App: app [production]
Instance: /home/evil/microblog/instance
>>> app
<Flask 'app'>
当使用flask shell时,该命令预先导入应用实例。 flask shell的绝妙之处不在于它预先导入了app,而是你可以配置一个“shell上下文”,也就是可以预先导入一份对象列表。
下面在microblog.py中实现一个函数,它通过添加数据库实例和模型来创建了一个shell上下文环境:
from app import my_app, my_db
from app.models import User, Post
@my_app.shell_context_processor
def make_shell_context():
return {'db': my_db, 'User': User, 'Post': Post}
则现在运行flask shell
:
$ flask shell
>>> my_db
<SQLAlchemy engine=sqlite:////home/evil/microblog/my_app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>
>>>
如果产生NameError,说明 make_shell_context() 没有被Flask注册。最有可能的原因是你的环境变量中没有设定 FLASK_APP=microblog.py
,可以查看之前写的第一篇文章,配置,或者使用export FLASK_APP=microblog.py
,本篇到此结束,谢谢您的观看。(可以给我点个赞吗,就一个,谢谢了嘞)。