乐趣区

第4天在Flask应用中使用数据库FlaskSQLAlchemy

原文: http://www.catonlinepy.tech/
声明:原创不易,未经许可,不得转载

1. 你将学会什么

接第 3 天表单的使用课程,今天的课程主要涉及到与数据库相关的两个插件的使用,一个是 Flask_SQLAlchemy,另外一个是 Flask_Migrate。通过今天的学习,你将学会如何对数据库进行基本的操作,以及如何完成数据库的迁移。教程中的代码都会托管到 github 上,猫姐不厌其烦地强调,在学习本课内容时一定要自己尝试手敲代码,遇到问题再到 github 上查看代码,如果实在不知道如何解决,可以在日志下方留言。

2. 使用 Flask_SQLAlchemy 管理数据库

2.1 Flask_SQLAlchemy 的安装

对于 web 后台开发工作,必须要掌握的一项技能便是对数据库的 CRUD(create, read, update, delete) 操作,如果开发过程中直接使用原生的 sql 语句对数据库进行操作,将是非常痛苦的事件(毕竟 sql 语句有很多反人类的设计)。Flask_SQLAlchemy 插件将开发人员从这个泥潭中解救出来了,我们只需要使用 Python 的类就能轻松的完成对表的增删改查操作,并且该插件还支持多种数据库类型,如 MySQL、PostgreSQL、和 SQLite 等。

在进入正式学习之前,我们照旧要建立今天的项目目录,如下:

# 进入到虚拟环境目录,激活虚拟环境
maojie@Thinkpad:~/flask-plan/$ source miao_venv/bin/activate

# 到 flask-course-primary 目录下创建第四天的课程 day4 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary$ mkdir day4

# 进入 day4 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary$ cd day4

# 新建 database_demo 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ mkdir database_demo

# 进入到 database_demo 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ cd database_demo/

# 在 database_demo 目录中新建__init__.py 文件
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4/database_demo$ touch __init__.py

# 在 database_demo 包中新建 routes.py 路由文件
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4/database_demo$ touch routes.py

# 在 day4 目录下新建 run.py 文件
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4/$ touch run.py

安装 Flask_SQLAlchemy 插件还是使用 pip 命令,如下:# 注意一定要在虚拟环境中
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ pip install Flask_SQLAlchemy

2.2 配置 Flask_SQLAlchemy

我们的教程中使用的数据库是 SQLite(Linux),主要原因是 SQLite 足够简单,不需要进行任何配置便可使用,十分适用于入门。下面在__init__.py 文件中配置数据库,如下所示:

# 在__init__.py 文件中的内容
from flask import Flask
# 从 flask_sqlalchemy 导入 SQLAlchemy 类
from flask_sqlalchemy import SQLAlchemy

import os
# 通过 Flask 创建一个 app 实例
app = Flask(__name__)
basedir = os.path.abspath(os.path.dirname(__file__))
# Flask_SQLAlchemy 插件从 SQLALCHEMY_DATABASE_URI 配置的变量中获取应用的数据库位置
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'database.db')
# 通过 SQLAlchemy 创建 db 实例,表示程序使用的数据库,并且 db 能够使用 Flask_SQLAlchemy 的所有功能
db = SQLAlchemy(app)

2.3 构建数据库模型

数据模型通常用来定义数据库中的表及表中的字段。下面代码中的 User 类和 Post 类就代表了数据库中的两张表(官方叫法是数据模型)。后面我们会看到,通过这里定义的两个 Python 类,我们就可以非常容易的完成表的增删改查,以下是 routes.py 文件中的内容。

# 在 routes.py 文件中的内容
from database_demo import db

# 定义类 User,继承自基类 db.Model
class User(db.Model):
    # 定义数据库表的名称,为 user 表
    __tablename__ = 'user'
    # db.Column 类构造函数中的第一个参数表中该字段的类型和该字段的其它属性
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    # print User 对象时,会打印 return 的字符串,方便调试
    def __repr__(self):
        return f"User('{self.username}')"


class Post(db.Model):
    __tablename__ = 'post'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), unique=True, nullable=False)

    def __repr__(self):
        return f"Post('{self.title}')"

2.4 建立表之间的关系

关系型数据库是通过关系把数据库中不同表进行关联。下面,将介绍表之间的一种关系(一对多的关系),通常一个用户可以发表多篇日志,这也是与实际情况相符的。在 routes.py 文件中添加两行代码便可以建立这种“一对多”的关系了。猫姐提醒:”一对多“的关系在实际开发中十分常见,但是下面的两行建立关系的代码却不太容易看懂,因此大家不用太关心代码的细节,后面只要大家会照着葫芦画瓢,建立好这种关系就足够了。

# 修改 routes.py 文件的内容

from database_demo import db

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    # 添加反向引用关系
    posts = db.relationship('Post', backref='author', lazy=True)

    def __repr__(self):
        return f"User('{self.username}')"


class Post(db.Model):
    __tablename__ = 'post'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), unique=True, nullable=False)
    # 添加表 user 的外键 user_id
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    def __repr__(self):
        return f"Post('{self.title}')"

添加到 Post 模型中的 user_id 被称为外键,两个表的关系主要通过外键建立起表之间的联系。db.ForeignKey 的参数 'user.id' 表示这列的值是 user 表中的 id。添加到 User 模型的 posts 属性表示这个关系的面向对象视图,它不是实际数据库的字段,它是用户和其发表日志关系之间的视图。如果有一个 User 类的实例 u,u.posts 则返回用户发表过的所有日志。

3. 数据库的基本操作

3.1 数据库中表的创建

首先在 run.py 文件中输入如下代码:

# run.py 文件中的内容
from database_demo import app

if __name__ == "__main__":
     app.run(debug=True)

然后在终端中输入 flask shell 命令,前提条件是必须在终端中配置环境变量,在第一课中猫姐已经讲过如何配置,这里不再重复。输入 flask shell 后进入 python 交互环境,如下所示:

 在终端中输入命令 flask shell

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ flask shell
/home/meyou/flask-plan/miao_venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and'
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
App: database_demo [production]
Instance: /home/meyou/flask-plan/flask-course-primary/day4/instance
>>> 

# 注意:出现三个大于号,表示已经进入 python 交互环境 

开始在数据库中创建 user 表和 post 表,如下所示:

>>> from database_demo import db       # 从 database_demo 包中导入 db
>>> db.create_all()                    # 用 db.create_all() 创建数据库表                 
>>>

这时查看程序目录,发现已经存在 database.db 库了,如下所示:

# 数据库存在于 database_demo 包里
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4/database_demo$ ls
database.db  __init__.py  __init__.pyc  __pycache__  routes.py

# database.db 文件名是我们在__init__.py 中配置的 

要想查看 dabase.db 中是否存在新建的表,可以用下面的方式,如下所示:

# 在 database_demo 包目录中用 sqlite3 加上数据库名字进入到数据库中

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4/database_demo$ sqlite3 database.db 
SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.

# 用.tables 查看表
sqlite> .tables
post  user
sqlite> 

# post,user 是我们在建立数据模型时定义的两张表 

虽然通过 db.create_all() 可以创建表,但是如果数据库中已经存在这两张表,再用 db.create_all() 时是不会重新创建表或是更新表,只有先通过 db.drop_all() 删除数据库中所有的表,然后再用 db.create_all() 创建表,这样原有的数据库中表的数据都被销毁了,这是我们不想看到的。所以,在本文末会用一种更有效的方式来管理数据库。

3.2 表的数据插入

继上面 python shell 环境,在 user 表中插入两行数据,如下所示:

>>> from database_demo.routes import User,Post    # 需要导入模型 User,Post 所在的位置
>>> user1=User(username='miaojie')                # 创建用户 1
>>> user2=User(username='miaoge')                 # 创建用户 2
>>> print(user1.id)                               # 打印用户 1 的 id
None
>>> print(user2.id)                               # 打印用户 2 的 id
None
>>> 

# 因为这些数据还未被真正写到数据库中,所以用户的 id 并不会显示出来

# 将数据对象添加到会话中
>>> db.session.add(user1)
>>> db.session.add(user2)
>>>

# 只有将会话提交后,数据才会写到 database.db 文件中
>>> db.session.commit()
>>> 

# 再次查看
>>> print(user1.id)
1
>>> print(user2.id)
2

3.3 表的行数据修改

下面,继续在 python shell 会话中进行操作,现在把 user1 重新命名,如下所示:

>>> user1.username='miaomiao'
>>> db.session.commit()
>>> print(user1)
User('miaomiao')
>>> 

3.4 表的行数据删除

在数据库中还可以使用 delete() 进行数据行的删除,如下所示:

>>> db.session.delete(user1)
>>> db.session.commit()
>>> user1 = User.query.filter_by(username="miaomiao").first()
>>> print(user1)
None
>>>

3.5 表的查询

通过 query 可以查询模型中的所有的内容,接上个过程,如下:

>>> User.query.all()
[User('miaoge')]
>>> 

# 现在为用户 2‘maoge’创建一篇日志
>>> u2=User.query.get(2)
>>> p2=Post(title='这是一篇关于数据库的日志',author=u2)
>>> db.session.add(p2)
>>> db.session.commit()
>>> User.query.all()
[User('miaoge')]
>>> Post.query.filter_by(author=u2).all()
[Post('这是一篇关于数据库的日志')]
>>> 

4. python shell 上下文

刚开始在终端中输入 flask shell,进入 shell 后,输入数据库模型等时它并没有定义,如下所示:

# 在终端中输入 flask shell 后,进入 shell
# 输入模型 User 时,发现它并没有被定义
>>> User
Traceback (most recent call last):
  File "<console>", line 1, in <module>
NameError: name 'User' is not defined
>>> 
# 这就是因为刚开始进入 shell 时,它没有一个上下文,需要导入模型及其实例 

这就需要每次进入 flash shell 环境时导入数据库的实例以及模型,这是非常浪费时间的。为了避免每次进入时导入,可以使用 app.shell_context_processor 装饰器,将定义的函数注册为一个 shell 上下文函数,当在终端中输入 flask shell 时,该函数会在 shell 中返回被注册的内容。在 routes.py 文件中添加如下代码:

# routes.py 文件中的内容

#!coding:utf8
# 在这里需要将 app 导入,以供 @app 装饰器使用
from database_demo import db, app

...
...

# 添加 shell 上下文函数
# app.shell_context_processor 装饰器将 generate_shell_context 函数注册为一个 shell 上下文函数
@app.shell_context_processor
def generate_shell_context():
    return {'db': db, 'User': User, 'Post': Post}

在终端中重新输入 flask shell,进入 shell 后查看效果:

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ flask shell
/home/meyou/flask-plan/miao_venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and'
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
App: database_demo [production]
Instance: /home/meyou/flask-plan/flask-course-primary/day4/instance
>>> db
<SQLAlchemy engine=sqlite:////home/meyou/flask-plan/flask-course-primary/day4/database_demo/database.db>
>>> User
<class 'database_demo.routes.User'>
>>> 

这时,所需要的模型都存在 python shell 里,而不需要每次进入 shell 时导入 db 及其数据模型。

5. 使用 Flask-Migrate 实现数据库迁移

在前面讲过创建数据库的方法,如果数据库中存在表或是需要修改数据库中模型,使用 db.create_all() 是不会更新数据库表的,需要先使用 db.drop_all() 删除旧表,再创建表。但是,原来表中的数据就都被删掉了,这时就出现了数据库迁移的概念。开发过程中,随着需求的变化,需要对表加入一些新的字段,但是旧表中又保存了很多数据,此时就需要创建新表,并将旧表中的数据迁移至新表中,Flask_Migrate 这个插件就能帮我们快速完成数据从旧表到新表的迁移过程。

5.1 Flask_Migrate 的安装

还是在虚拟环境中安装 Flask_Migrate 插件,如下:

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ pip install Flask_Migrate

5.2 Flask_Migrate 的配置

在__init__.py 文件中进行如下配置:

# __init__.py 文件中的内容

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# 从 flask_migrate 中导入 Migrate 类
from flask_migrate import Migrate

import os

app = Flask(__name__)
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'database.db')
db = SQLAlchemy(app)

# 通过类 Migrate 创建 migrate 实例
migrate = Migrate(app, db)

from database_demo.routes import *

5.3 数据库的迁移过程

Flask_Migrate 插件中添加了 flask db 命令来管理数据库迁移的所有事情,首先是迁移前的准备工作:

# 使用 flask db init 创建 migration 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ flask db init
/home/meyou/flask-plan/miao_venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and'
  Creating directory /home/meyou/flask-plan/flask-course-primary/day4/migrations
  ... done
  Creating directory /home/meyou/flask-plan/flask-course-
  primary/day4/migrations/versions ... done
  Generating /home/meyou/flask-plan/flask-course-primary/day4/migrations/README
  ... done
  Generating /home/meyou/flask-plan/flask-course-primary/day4/migrations/env.py
  ... done
  Generating /home/meyou/flask-plan/flask-course-
  primary/day4/migrations/script.py.mako ... done
  Generating /home/meyou/flask-plan/flask-course-
  primary/day4/migrations/alembic.ini ... done
  Please edit configuration/connection/logging settings in '/home/meyou/flask-
  plan/flask-course-primary/day4/migrations/alembic.ini' before proceeding.

# 注意,如果在虚拟环境中没有设置环境变量 FLASK_APP=run.py,则在使用命令 flask db init 时,会提醒需要先设置环境变量。在第一课最后讲到如何设置环境变量

# 在 day4 目录下已经生成 migrations 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ ls
database_demo  migrations  __pycache__  run.py  run.pyc

使用 flask db migrate 命令检测 Post,User 类中是否添加了新的字段:

# 使用 flask db migrate 命令创建迁移脚本
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ flask db migrate
/home/meyou/flask-plan/miao_venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and'
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.env] No changes in schema detected.

# 提示 info 消息没有任何改变 

我们在 routes.py 文件中的 User,Post 模型中添加字段,这里,User 表中添加了 email 字段,Post 表中添加了 content 字段。然后再使用 flask db migrate 命令来执行,如下:

# routes.py 文件中的内容
#!coding:utf8
from database_demo import db, app


class User(db.Model):
    __table__name = 'user'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), nullable=True)
    posts = db.relationship('Post', backref='author', lazy=True)

    def __repr__(self):
        return f"User('{self.username}')"


class Post(db.Model):
    __table__name = 'post'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), unique=True, nullable=False)
    content = db.Column(db.Text, nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    def __repr__(self):
        return f"Post('{self.title}')"


# 解释,添加 email 和 content 字段,unique 必须设置为空或是不写 unique,否则在用 flask db migrate 创建迁移脚本时会报错 

再次使用 flask db migrate 命令检测 Post,User 类中是否添加了新的字段:

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ flask db migrate
/home/meyou/flask-plan/miao_venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and'
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'post.content'
INFO  [alembic.autogenerate.compare] Detected added unique constraint 'None' on '['content']'
INFO  [alembic.autogenerate.compare] Detected added column 'user.email'
INFO  [alembic.autogenerate.compare] Detected added unique constraint 'None' on '['email']'
  Generating /home/meyou/flask-plan/flask-course-
  primary/day4/migrations/versions/f49f801bddb6_.py ... done

# 提示检测到有内容改变,添加了列 content 和列 email 字段 

最后使用 flask db upgrade 命令完成数据库的迁移过程,如下所示:

# flask db upgrade 可以把改变应用到数据库中,而不改变其中保存的数据
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day4$ flask db upgrade
/home/meyou/flask-plan/miao_venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and'
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 8436eb139f65, empty message

6. 总结

学习完今天的内容,我们掌握了如下技能:

1. 学习了 Flask_SQLAlchemy 的基本使用方法
2. 学习了数据库模型及表之间关系的建立
3. 学习了数据库的增删改查基本操作
4. 学习了 python shell 上下文的使用
5. 学习了使用 Flask_Migrate 完成数据库的迁移

下一课的教程,猫姐将带领大家一起学习用户登录功能的实现。今天的内容就到这里,喜欢的同学们可以在下面点赞留言,或是访问我的博客地址:http://www.catonlinepy.tech/ 加入我们的 QQ 群进一步交流学习!

7. 代码的获取

大家可以到 github 上获取今天教程的所有代码:https://github.com/miaojie19/…

具体下载代码的命令如下:

# 使用 git 命令下载 flask-course-primary 仓库所有的代码
git clone https://github.com/miaojie19/flask-course-primary.git

# 下载完成后,进入 day4 目录下面,即可看到今天的代码
cd flask-course-primary
cd day4

退出移动版