Flask 极致细节:4. 数据库的搭建

Flask 极致细节:4. 数据库的搭建

此章节主要介绍如何专业地使用pymysql, flask-sqlalchemy, flask-migrate对数据库进行搭建,版本更新,回退等。这里的数据库工具主要为sqlite,本文也对遇到的一些问题进行了记录与解决。这里,我们一如既往地分享所有代码,并附有非常详细的注解。

喜欢的朋友点个赞哦:)
代码链接:https://pan.baidu.com/s/133iY9EMcMUmeBOD_SNburg
密码:nwh9


文章目录

  • Flask 极致细节:4. 数据库的搭建
    • 1. 项目结构
    • 2. 搭建数据库所需配置
      • 2.1 依赖的安装
      • 2.2 配置的一个简单的写法
      • 2.3 配置更为正规的写法
        • 2.3.1 config文件写法
        • 2.3.2 SQLAlchemy写法
        • 2.3.3 Migrate写法
    • 3. 创建模型
      • 3.1 将类和表连接起来
      • 3.2 使用模型
      • 3.3 VSCode中db文件的可视化
      • 3.4 表的修改和新增
      • 3.5 可能会遇到的问题
    • 4. 小节


1. 项目结构

--项目名
  |---static (静态)
  |---templates (模板)
    |---user
      |---register.html
      |---show.html
    |---base.html
  |---apps
    |---users
      |---model.py
      |---view.py
      |---__init__.py
    |---__init__.py
    |---test.db (我们本章节中生成的db文件)
  |---app.py (运行/启动)
  |---ext (第三方相关文件夹)
    |---__init__.py (目前主要用于实例化SQLAlchemy类)
  |---migrations (当我们执行新建db指令的时候会自动生成)
    |---versions (数据库每次更新都会生成一个对应的py文件,里面具体描述数据库具体哪里更新)
      |---6a0664ea9230_2022011604.py
      |---6f9731aacfd8_2022011605.py
    |---XXX (不具体罗列了)
  |---requirements.txt (所有安装包以及版本)
  |---settings.py (参数配置文件)
  |---readme.md (说明文档)

关于代码的运行环境,我们可以创建虚拟环境:virtualenv [venv]。接下来进入虚拟环境:.\[venv]\Scripts\activate。如是自己创建的新虚拟环境,还需要安装依赖:pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。相关的配置以及解释请参见前一篇博文:Flask 极致细节:0. VS Code 手动新建Flask项目。

2. 搭建数据库所需配置

我们一般会将数据存于SQL中,在flask中也是如此。

2.1 依赖的安装

首先,我们需要安装一些依赖,在requirements.txt中:

pymysql==1.0.2
flask-sqlalchemy==2.5.1
flask-migrate==3.1.0

我们在虚拟环境中直接pip install即可。

pymysql就是将数据保存至SQL的包,flask-sqlalchemy 用来实现ORM映射。一般来说,我们处理SQL数据,
总是需要使用一些基本的增(add),删(delete),改(update),查(select)的SQL语言,但有了这个映射
关系,我们就不用输入这些语句了,可以有相关的类/对象直接映射过去。flask-migrate是用来使用ORM发布命令的工具。

然后,我们需要在settings.py中做一些相关配置。

2.2 配置的一个简单的写法

如果你在网上搜索搜索flask_sqlalchemy + sqlite使用方法之类的文档,大致上你会找到类似这种网页。写的也是很好的,这里引用其参数的配置以及调用:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
db = SQLAlchemy(app)
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))

一共是四步,这里都写在app.py文件中了。

第一步是实例化Flask类(app)。第二步是app的参数设置。第三部是实例化SQLAlchemy类。第四步是实例化Migrate类。在下一章中,我们会详细解释每一步的意义。但从理解代码的层面上,上述简单直白的例子很有效,但从项目扩展的角度,可以参考下一章的结构。

2.3 配置更为正规的写法

2.3.1 config文件写法

还记得我们之前的章节中,settings.py里面是这么写的:

ENV = 'development'
DEBUG = True

调用配置文件的代码:

app = Flask(__name__)
app.config.from_object(settings)

我们先将settings.py文件写的更正规一些,包含一个基类以及在开发环境与生产环境不停情况下不同的参数设置。

class Config:
    DEBUG = True
    # 书写规则(mysql):你需要使用的数据库(mysql)+驱动(pymysql);://用户名:用户密码@host:port/databasename
    # SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:[email protected]:3306/databasename'
    # 书写规则(sqlite):sqlite:///databasename (相对路径)
    SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = True

class DevelopmentConfig(Config):
    ENV = 'development'

class ProducttionConfig(Config):
    ENV = 'production'
    DEBUG = False

我们对于,比如,开发环境下config的调用就可以是:

app = Flask(__name__)
app.config.from_object(settings.DevelopmentConfig)

接下来,我们需要配置数据库的连接路径。也就是这行:

# 书写规则(mysql):你需要使用的数据库(mysql)+驱动(pymysql);://用户名:用户密码@host:port/databasename
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:[email protected]:3306/databasename'
# 书写规则(sqlite):sqlite:///databasename (相对路径)
SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True

注意它的书写规则。

2.3.2 SQLAlchemy写法

接下来,我们在主目录下新建一个ext包/文件夹。这个包/文件夹里面包括了第三方相关的扩展。在__init__函数中,我们创建一个映射对象:

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

我们点击SQLAlchemy看里面的源代码,便会发现有两种创建映射对象的方式

1
app = Flask(__name__)
db = SQLAlchemy(app)
2
db = SQLAlchemy()
def create_app():
    app = Flask(__name__)
    db.init_app(app)
    return app

我们选择第二种,而上一章节使用的是第一种方式。

我们找到apps文件夹下的__init__.py文件(因为这个文件被用来创建Flask对象),然后加上db.init_app(app)这句话:

def create_app():
    app = Flask(__name__,template_folder="../templates",static_folder="../static")       # app是一个核心对象。因为很多东西都会和这个app建立联系,所以这里我们把它独立出来。
    app.config.from_object(settings.DevelopmentConfig)
    # 创建一个映射对象
    db.init_app(app)
    # 蓝图的导入。整个项目结构就像是树的分支一样。
    app.register_blueprint(user_bp)
    print(app.url_map)
    return app
2.3.3 Migrate写法

Flask-Migrate一般用于数据库的迁移。

“在开发时,以删除表再重建的方式更新数据库简单直接,但明显的缺陷是会丢掉数据库中的所有数据。在生产环境下,没有人想把数据都删除掉,这时需要使用数据库迁移工具来完成这个工作。SQLAlchemy的开发者Michael Bayer写了一个数据库迁移工作—Alembic来帮助我们实现数据库的迁移,数据库迁移工具可以在不破坏数据的情况下更新数据库表的结构。”(摘自此博客)

我们找到apps文件夹下的__init__.py文件(因为这个文件被用来创建Flask对象),然后加上migrate = Migrate(app, db)这句话:

def create_app():
    app = Flask(__name__,template_folder="../templates",static_folder="../static")       # app是一个核心对象。因为很多东西都会和这个app建立联系,所以这里我们把它独立出来。
    app.config.from_object(settings.DevelopmentConfig)
    # 创建一个映射对象
    db.init_app(app)
    # 创建Mcigrate对象
    migrate = Migrate(app, db)
    # 蓝图的导入。整个项目结构就像是树的分支一样。
    app.register_blueprint(user_bp)
    #print(app.url_map)
    return app

3. 创建模型

3.1 将类和表连接起来

接下来,我们需要将类和表连接起来。即,我们需要准备写进表中的数据,并放在一个类里面。

apps/users/model.py,在上一章案例中是这么写的:

class UserCls():
    def __init__(self,username,password,phone=None) -> None:
        self.username = username
        self.password = password
        self.phone = phone
    
    def __str__(self):
        return self.username

也就是说,我们原本只是定义了一个user类,里面包含了简单的username,password,phone。
那么现在,我们如果要把这个类和db migration相连接,首先,我们让这个类做一个继承:
class UserCls(db.Model)。回顾一下,db是我们对SQLAlchemy的一个实例化:db = SQLAlchemy()
如果我们进入这个类,我们找到关于db.Model的一些描述:

This class also provides access to all the SQLAlchemy functions and classes
from the :mod:`sqlalchemy` and :mod:`sqlalchemy.orm` modules.  So you can
declare models like this::

    class User(db.Model):
        username = db.Column(db.String(80), unique=True)
        pw_hash = db.Column(db.String(80))

You can still use :mod:`sqlalchemy` and :mod:`sqlalchemy.orm` directly, but
note that Flask-SQLAlchemy customizations are available only through an
instance of this :class:`SQLAlchemy` class.  Query classes default to
:class:`BaseQuery` for `db.Query`, `db.Model.query_class`, and the default
query_class for `db.relationship` and `db.backref`.  If you use these
interfaces through :mod:`sqlalchemy` and :mod:`sqlalchemy.orm` directly,
the default query class will be that of :mod:`sqlalchemy`.

根据上面的描述,我们将apps/users/model.py改写为:

from datetime import datetime
from ext import db

class UserCls(db.Model):

    id = db.Column(db.Integer,primary_key=True,autoincreament=True)
    username = db.Column(db.String(20), nullable=False, unique=True)
    password = db.Column(db.String(20), nullable=False)
    phone = db.Column(db.String(20), unique=True)
    rdatetime = db.Column(db.DateTime, default=datetime.now)
    
    def __str__(self):
        return self.username

更详细的解释和案例,请参见此文档。

3.2 使用模型

在Terminal中,我们尝试输入flask db --help可以看到:

Commands:
  branches        Show current branch points
  current         Display the current revision for each database.
  downgrade       Revert to a previous version
  edit            Edit a revision file
  heads           Show current available heads in the script directory
  history         List changeset scripts in chronological order.
  init            Creates a new migration repository.
  list-templates  List available templates.
  merge           Merge two revisions together, creating a new revision file
  migrate         Autogenerate a new revision file (Alias for 'revision...
  revision        Create a new revision file.
  show            Show the revision denoted by the given symbol.
  stamp           'stamp' the revision table with the given revision;...
  upgrade         Upgrade to a later version

Flask有一个自己的命令集,以flask开头,作用于terminal中。

创建迁移环境:在开始迁移数据之前,需要先使用下面的命令创建一个迁移环境:flask db init。这条命令会在你得主目录下创建一个migrations文件夹,里面包含了自动生成的配置文件和迁移版本文件夹。Terminal中的信息:

Creating directory XXX\flask_4_SQL\migrations ...  done
Creating directory XXX\flask_4_SQL\migrations\versions ...  done
Generating XXX\flask_4_SQL\migrations\alembic.ini ...  done
Generating XXX\flask_4_SQL\migrations\env.py ...  done
Generating XXX\flask_4_SQL\migrations\README ...  done
Generating XXX\flask_4_SQL\migrations\script.py.mako ...  done
Please edit configuration/connection/logging settings in 'D:\\yichao\\learning\\flask\\flask_4_SQL\\migrations\\alembic.ini' before proceeding.

细节:为什么这个文件夹的名字叫migrations?当我们实例化Migrate的时候(migrate = Migrate(app, db)),我们点进去看源代码,会发现他做了一个默认的保存路径,名为migrations

def __init__(self, app=None, db=None, directory='migrations', **kwargs):
        self.configure_callbacks = []
        self.db = db
        self.directory = str(directory)
        self.alembic_ctx_kwargs = kwargs
        if app is not None and db is not None:
            self.init_app(app, db, directory)

生成迁移脚本:使用migrate子命令可以自动生成迁移脚本:flask db migrate -m "Initial migration."。Terminal中的信息:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user_cls'
Generating XXX\flask_4_SQL\migrations\versions\4331428227f6_initial_migration.py ...  done

我们找到4331428227f6_initial_migration.py这个文件(注意,这个文件就是我们刚才migrate指令自动生成的)。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('user_cls',
    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
    sa.Column('username', sa.String(length=20), nullable=False),
    sa.Column('password', sa.String(length=20), nullable=False),
    sa.Column('phone', sa.String(length=20), nullable=True),
    sa.Column('rdatetime', sa.DateTime(), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('phone'),
    sa.UniqueConstraint('username')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('user_cls')
    # ### end Alembic commands ###

我们看到,这里有一个upgrade函数以及一个downgrade函数。就像这两个函数中的注释所说的,迁移命令是有Alembic自动生成的,其中可能包含错误,所以有必要在生成后检查一下。

更新数据库:接下来,我们在terminal中输入flask db upgrade以更新数据库。Terminal中的信息:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 4331428227f6, Initial migration.

我们看到,在apps文件夹下生产了一个test.db文件。回顾一下settings.py文件中对这个db文件的定义:SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
由于我们app的启动程序写在了apps/__init__.py文件中,所以生成的test.db文件也存于这个apps这个文件夹下。

3.3 VSCode中db文件的可视化

我们看到,test.db已经生成。接下来,我们在VSCode Extension中下载一个SQLite Viewer,然后使用这个软件打开test.db文件,我们会看到如下图:

在这里插入图片描述

由于我们现在只是新建了一个数据库,所以里面还没有数据。

3.4 表的修改和新增

当我们对原表做了修改,或者新建了一张表,比如,在model.py下,我们对原表增加一个userid,并且新增一张表UserInfoCls

class UserCls(db.Model):
    # 通过db.Column 来映射表中的列。
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    username = db.Column(db.String(20), nullable=False, unique=True)
    password = db.Column(db.String(20), nullable=False)
    phone = db.Column(db.String(20), unique=True)
    rdatetime = db.Column(db.DateTime, default=datetime.now)
    userid = db.Column(db.Integer, unique=True)
    
    def __str__(self):
        return self.username

class UserInfoCls(db.Model):
    # 通过db.Column 来映射表中的列。
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    realname = db.Column(db.String(20), nullable=False, unique=True)
    gender = db.Column(db.Boolean, deafult=False)

    def __str__(self) -> str:
        return self.realname

这个时候,我们需要重复这两条语句即可:

flask db migrate -m "Add UserInfoCls table. Add userid in UserCls table."
flask db upgrade

再复述一遍,第一条指令的目的是自动生成迁移脚本,第二条指令就是更新数据库。

当我们执行完第一条指令,terminal中显示:

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_cls'
INFO  [alembic.autogenerate.compare] Detected added column 'user_cls.userid'
INFO  [alembic.autogenerate.compare] Detected added unique constraint 'None' on '['userid']'
Generating XX\flask_4_SQL\migrations\versions\25c9334a25e1_add_userinfocls_table_add_userid_in_.py ...  done

你可以从terminal中明显看到增加了一张表,以及user_cls中增加userid。而在migrations/versions/文件夹下,也出现了两个py文件,分别对应着两张表的下一步更新动作。

最后,执行flask db upgrade。可能你会遇到报错,请查看下一章节。如果成功,terminal中显示:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade c03ebf7103de -> 6a0664ea9230, 2022011604

可视化test.db,你会发现多了一个表,并且第一个表多了一列。

3.5 可能会遇到的问题

在执行flask db migrate的时候,系统反馈错误:ERROR [flask_migrate] Error: Target database is not up to date.。这个问题可能是因为数据库迁移的版本出现了冲突。解决问题的办法是输入flask db stamp head。terminal中:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running stamp_revision a6802163fd21 -> 02e2ae90a2fe

另外一个问题,当我们使用Sqlite,migrate成功后,flask db upgrade失败,显示错误ValueError: Constraint must have a name。解决方案见此链接。

我们查看出错的字生成py文件:

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('user_cls', sa.Column('userid', sa.Integer(), nullable=False))
    op.create_unique_constraint(None, 'user_cls', ['userid'])
    # ### end Alembic commands ###

我们发现,create_unique_constraint的第一个参数值是None,因而导致错误。

首先,我们在ext/__init__().py文件中增加定义命名惯例,做出如下修改:

from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import MetaData

naming_convention = {
    "ix": 'ix_%(column_0_label)s',
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(column_0_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s"
}

db = SQLAlchemy(metadata=MetaData(naming_convention=naming_convention))

然后,在apps/__init__().py文件创建Migrate对象中修改为:

migrate = Migrate(app, db, render_as_batch=True)

这里的render_as_batch对于Sqlite很重要,原因我还没搞懂,但不这么设置会出错。

当我们再次执行flask db migrate并且成功后,我们会发现

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('user_cls', schema=None) as batch_op:
        batch_op.create_unique_constraint(batch_op.f('uq_user_cls_phone'), ['phone'])
        batch_op.create_unique_constraint(batch_op.f('uq_user_cls_userid'), ['userid'])
        batch_op.create_unique_constraint(batch_op.f('uq_user_cls_username'), ['username'])

    with op.batch_alter_table('user_info_cls', schema=None) as batch_op:
        batch_op.create_unique_constraint(batch_op.f('uq_user_info_cls_realname'), ['realname'])

    # ### end Alembic commands ###

这个时候我们再执行update指令就不会有问题了。

4. 小节

我们总结一下到目前为止相关的一些数据库命令:

flask db init       初始化,产生一个文件夹migrations,一个项目只需要init一次就可以
flask db migrate    自动产生一个版本文件
flask db upgrade    更新数据库
flask db downgrade  数据库回退到上一个版本

migrations/versions文件夹下,我们能看到好几个py文件,他们分别对应了每次我们对数据库做修改的记录。
test.db中有一个alembic version表,包含了当前db的版本,我们可以使用flask db downgrade进行数据库的版本回退。
Flask 极致细节:4. 数据库的搭建_第1张图片

你可能感兴趣的:(flask,flask,数据库,python)