美文网首页
04.数据库

04.数据库

作者: SingleDiego | 来源:发表于2021-06-30 16:36 被阅读0次

    本章的主题是重中之重!因为一个网站的大多数应用都需要持久化存储数据,并高效地执行的增删查改的操作,数据库为此而生。






    Flask 中的数据库

    Flask 本身并不自带数据库,你可以自由选择数据库插件来实现数据库的功能,这是 Flask 有意为之,为了让开发者让人拥有更大的主动权。

    绝大多数的数据库都提供了 Python 客户端包,它们之中的大部分都被封装成 Flask 插件以便更好地和 Flask 应用结合。

    数据库被划分为两大类,遵循关系模型的一类是关系数据库,另外的则是非关系数据库,简称 NoSQL。虽然两类数据库都是伟大的产品,但我认为关系数据库更适合具有结构化数据的应用程序,例如用户列表,用户动态等,而 NoSQL 数据库往往更适合非结构化数据。

    本应用可以像大多数其他应用一样,使用任何一种类型的数据库来实现,但是出于上述原因,我将使用关系数据库。

    在本章节,将使用到 Flask-SQLAlchemy 这个扩展,这个插件把 SQLAlchemy 包做了一层封装以便在 Flask 中调用更方便,类似 SQLAlchemy 这样的包叫做 Object Relational Mapper,简称 ORM。 ORM 允许应用程序使用高级实体(如类,对象和方法)而不是表和 SQL 来管理数据库。

    SQLAlchemy 不只是某一款数据库软件的 ORM,而是支持包含 MySQL、PostgreSQL 和 SQLite 在内的很多数据库软件。在开发的时候使用简单易用且无需另起服务的 SQLite,需要部署应用到生产服务器上时,则选用更健壮的 MySQL 或 PostgreSQL 服务,并且不需要修改应用代码,只需修改应用配置就可以了。

    现在我们先完成插件的安装,激活虚拟环境之后,利用如下命令安装:

    (venv) $ pip install flask-sqlalchemy
    






    数据库迁移

    回顾我们学习数据库的流程,都是定义一个数据库,再创建数据库然后使用它,如果在数据库创建后,我们要对数据库的结构进行修改,或者修改完成后又想到退回修改前的版本,那么就需要数据库迁移了。

    数据库迁移的原理就是用一个文件来记录数据库各个版本的结构,根据记录文件,可以创建或退回某个版本的数据库。

    插件 Flask-Migrate。 这个插件是 Alembic 的一个 Flask 封装,是 SQLAlchemy 的一个数据库迁移框架。

    现在我们先去安装该插件:

    (venv) $ pip install flask-migrate
    

    后面我们再会用到这个插件。






    Flask-SQLAlchemy 配置

    在配置文件 config.py 添加两个新的配置项:

    # config.py
    
    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, 'app.db')
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    

    SQLALCHEMY_DATABASE_URI 定义了 Flask-SQLAlchemy 获取应用的数据库的路径。这里使用了 or 语法,先从环境变量中获得,如果不存在则使用默认值。默认值被设置为 basedir 变量表示的顶级目录下的一个名为 app.db 的文件路径。

    SQLALCHEMY_TRACK_MODIFICATIONS 配置项用于定义数据发生变更之后是否发送信号给应用,现在不需要这项功能,因此将其设置为 False

    要使用 Flask-SQLAlchemyFlask-Migrate 先要将应用实例化之后进行实例化和注册操作。

    app/__init__.py 文件中修改工厂函数:

    # app/__init__.py
    
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from flask_migrate import Migrate
    from config import Config
    
    
    db = SQLAlchemy()
    migrate = Migrate()
    
    
    def create_app():
        app = Flask(__name__)
    
        # 加载配置
        app.config.from_object(Config)
    
        # 初始化各种扩展库
        db.init_app(app)
        migrate.init_app(app, db)
        
        # 引入蓝图并注册
        from app.routes import main_routes
        app.register_blueprint(main_routes)
    
        return app
    
    from app import models
    

    最后在底部导入 models 模块,这个模块将会用来定义数据库结构,之所以要在最底部导入是为了避免循环引用。






    数据库模型

    定义数据库中一张表及其字段的类,通常叫做数据模型。ORM(SQLAlchemy) 会将类的实例关联到数据库表中的数据行。

    首先从用户模型开始,利用 SQL Designer 工具,画一张图来设计用户表的各个字段:

    数据库示意图

    id 字段通常存在于所有模型并用作主键。每个用户都会被数据库分配一个 id 值,并存储到这个字段中。大多数情况下,主键都是数据库自动赋值的,我只需要提供 id 字段作为主键即可。

    usernameemailpassword_hash 字段被定义为字符串(数据库术语中的 VARCHAR),并指定其最大长度,以便数据库可以优化空间使用率。 usernameemail 字段的用途不言而喻。

    password_hash 字段值得提一下,为想确保网站采用安全最佳实践,不应该将用户密码明文存储在数据库中。 明文存储密码的问题是,如果数据库被攻破,攻击者就会获得密码,这对用户隐私来说可能是毁灭性的。 如果使用哈希密码,这就大大提高了安全性。

    用户表构思完毕之后,我将其用代码实现,并存储到新建的模块 app/models.py 中,代码如下:

    # 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)
    

    上面创建的 User 类继承自 db.Model,它是 Flask-SQLAlchemy 中所有模型的基类。 这个类将表的字段定义为类属性,字段被创建为 db.Column 类的实例,它传入字段类型以及其他可选参数






    创建数据库迁移存储库

    现在我们用模型类定义了初始数据库结构,下一步我们要根据这个模型类来创建数据库。而随着开发的不断深入,我们会不断新增、修改或删除数据库结构,Alembic(Flask-Migrate 使用的迁移框架)将以一种不需要重新创建数据库的方式进行数据库结构的变更。

    为了实现它,Alembic 维护一个数据库迁移存储库,它是一个存储迁移脚本的目录。每当对数据库结构进行更改后,都需要向存储库中添加一个包含更改的详细信息的迁移脚本。当应用这些迁移脚本到数据库时,它们将按照创建的顺序执行。

    Flask-Migrate 通过 flask 命令运行它的子命令。比如之前已经运行过的 flask run,这是一个 Flask 本身的子命令。 Flask-Migrate 添加了 flask db 子命令来管理与数据库迁移相关的所有事情。 那么让我们通过运行 flask db init 来创建 microblog 的迁移存储库:

    (venv) $ flask db init
    Creating directory ...\flask demo\migrations ...  done
    Creating directory ...\flask demo\migrations\versions ...  done
    Generating ...\flask demo\migrations\alembic.ini ... done
    Generating ...\flask demo\migrations\env.py ...  done
    Generating ...\flask demo\migrations\README ...  done
    Generating ...\flask demo\migrations\script.py.mako ...  done
    Please edit configuration/connection/logging settings in 
    '... \\flask demo\\migrations\\alembic.ini' before proceeding.
    

    请记住,flask 命令依赖于 FLASK_APP 环境变量来知道 Flask 应用入口在哪里。要运行以上命令请先确认设置了 FLASK_APP = microblog.py

    执行完毕后,应用的顶级目录就创建了一个 migrations 文件夹,数据库迁移的相关文件都会在里面。 该文件夹中包含一个名为 versions 的子目录以及若干文件,记录了数据库版本的相关信息。从现在起,这些文件就是你项目的一部分了,应该添加到代码版本管理中去。






    第一次数据库迁移

    User 数据库模型的迁移存储库生成后,是时候创建第一次数据库迁移了。有两种方法来创建数据库迁移:手动或自动。 要自动生成迁移,Alembic 会将数据库模型定义的数据库模式与数据库中当前使用的实际数据库模式进行比较。然后,使用对需要更改的部分进行填充迁移脚本。当前情况是,由于之前没有数据库,自动迁移将把整个 User 模型添加到迁移脚本中。

    使用 flask db migrate 命令完成第一次数据库迁移:

    (venv) $ flask db migrate
    
    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 C:\Users\Administrator\Desktop\flask demo\migrations\versions\8f87e577f3a1_.py ...  done
    

    通过命令输出,你可以了解到 Alembic 在创建迁移的过程中执行了哪些逻辑。前两行是常规信息,通常可以忽略。 之后的输出表明检测到了一个 user 表和两个索引。 然后它会告诉你迁移脚本的输出路径。 8f87e577f3a1 是自动生成的一个用于迁移的唯一标识(你运行的结果会有所不同)。

    现在路径 migrations\versions 之下生成了一个 8f87e577f3a1.py 文件,记录的就是当前版本的数据库结构。如果查看它的代码,就会发现它有两个函数叫 upgrade()downgrade()upgrade() 函数用于应用迁移,downgrade() 函数用于回滚迁移。

    flask db migrate 命令不会对数据库进行任何更改,只会生成迁移脚本。 要将更改应用到数据库,必须使用 flask db upgrade 命令。

    (venv) $ 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  -> 8f87e577f3a1, empty message
    

    因为本应用使用 SQLite,所以 upgrade 命令检测到数据库不存在时,会创建它(在这个命令完成之后,你会注意到一个名为 app.db 的文件,即 SQLite 数据库)。 在使用类似 MySQL 和 PostgreSQL 的数据库服务时,必须在运行 upgrade 之前在数据库服务器上创建数据库。






    数据库升级和降级流程

    假设你的开发计算机上存有应用的源代码,并且还将其部署到生产服务器上,运行应用并上线提供服务。而应用在下一个版本必须对模型进行更改,例如需要添加一个新表。 如果没有迁移机制,这将需要做许多工作。

    通过数据库迁移机制的支持,在你修改应用中的模型之后,将生成一个新的迁移脚本(flask db migrate),测试无误后,将迁移脚本添加到源代码管理并提交到服务器。在服务器运行 flask db upgrade 就很容易完成数据库的更新建构。

    正如我前面提到的,flask db downgrade 命令可以回滚上次的迁移。 虽然在生产系统上不太可能需要此选项,但在开发过程中可能会发现它非常有用。你可能已经生成了一个迁移脚本并将其应用,只是发现所做的更改并不完全是你所需要的。在这种情况下,可以降级数据库,删除迁移脚本,然后生成一个新的来替换它。






    数据库关系

    关系数据库擅长存储数据项之间的关系。思考下用户发表动态的情况,用户的资料在 user 表有一条记录,这条用户动态在 post 表中有一条记录。我们要考虑的是标记这一条 post 记录属于哪一位 user

    现在扩展数据库来存储用户动态,以查看两个表中各条记录的关系。这是一个新表 post 的设计

    数据库示意图

    post 表将具有必须的 id、用户动态的 body 和时间戳 timestamp字段。 除了这些预期的字段之外,我还添加了一个 user_id 字段,它指向userid,也就是记录了这条 post 属于哪一位 user。这个 user_id 字段被称为外键

    修改后的 app/models.py 如下:

    # app/models.py
    
    from datetime import datetime
    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))
        posts = db.relationship('Post', backref='author', lazy='dynamic')
    
        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)
    

    Post 类的 timestamp 字段将被编入索引,如果你想按时间顺序检索用户动态,这将非常有用。 我们还对其添加了一个 default 参数,并传入了 datetime.utcnow 函数。在服务应用中使用 UTC 日期和时间是推荐做法。 这可以确保你使用统一的时间戳,无论用户位于何处,这些时间戳会在显示时转换为用户的当地时间。

    Post 类的 user_id 字段被初始化为 user.id 的外键,这意味着它引用了来自用户表的 id 值。本处的 user 是数据库表的名称,Flask-SQLAlchemy 自动设置类名为小写来作为对应表的名称。

    User 类有一个新的 posts 字段,用 db.relationship 初始化。这不是实际的数据库字段,而是用户和其动态之间关系的高级视图,因此它不在数据库图表中。

    对于一对多关系, db.relationship 字段通常在“一”的这边定义,并用作访问“多”的便捷方式。例如有一个用户实例 u,表达式 u.posts 将返回该用户发表过的所有动态。

    db.relationship 的第一个参数表示代表关系“多”的类名。 backref 参数定义了代表“多”的类的实例反向调用“一”的时候的属性名称。现在用户动态有了一个属性 post.author,调用它将返回给该用户动态的用户实例。lazy 参数定义了这种关系调用的数据库查询是如何执行的,这个我会在后面讨论。

    一旦我变更了应用模型,就需要生成一个新的数据库迁移:

    (venv) $ 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 ...\migrations\versions\290b8a59b3d3_posts_table.py ...  done
    

    -m 为可选参数,只是为迁移添加了一个简短的注释。

    最后更新数据库:

    (venv) $ 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 8f87e577f3a1 -> 290b8a59b3d3, posts table
    

    如果你对项目使用了版本控制,记得将新的迁移脚本添加进去并提交。






    数据库关系模型演示

    定义好数据库关系模型,并创建数据库后,我们来看看怎么使用它。由于应用还没有任何数据库逻辑,所以让我们在 Python Shell 中来测试。

    首先我们要在 microblog.py 中实现一个函数,它通过添加数据库实例和模型来创建了一个 shell上下文环境:

    # microblog.py
    
    from app import create_app, db
    from app.models import User, Post
    
    app = create_app()
    
    @app.shell_context_processor
    def make_shell_context():
        return {'db': db, 'User': User, 'Post': Post}
    

    app.shell_context_processor 装饰器将该函数注册为一个 shell 上下文函数。 当 flask shell 命令运行时,它会调用这个函数并在 shell 会话中注册它返回的项目。

    在添加 shell上下文处理器函数后就可以开始我们的演示:

    (venv) $ flask shell
    >>> db
    <SQLAlchemy engine=sqlite:///...\flask demo\app.db>
    >>> User
    <class 'app.models.User'>
    >>> Post
    <class 'app.models.Post'>
    

    现在创建一个新用户:

    >>> u = User(username='john', email='john@example.com')
    >>> db.session.add(u)
    >>> db.session.commit()
    

    对数据库的更改是在会话的上下文中完成的,你可以通过 db.session 进行访问验证。 允许在会话中累积多个更改,一旦所有更改都被注册,你可以发出一个指令 db.session.commit() 来以原子方式写入所有更改。

    添加另一个用户:

    >>> u = User(username='susan', email='susan@example.com')
    >>> db.session.add(u)
    >>> db.session.commit()
    

    执行返回所有用户的查询:

    >>> users = User.query.all()
    >>> users
    [<User john>, <User susan>]
    
    >>> for u in users:
    ...     print(u.id, u.username)
    ...
    1 john
    2 susan
    

    所有模型都有一个 query 属性,它是运行数据库查询的入口。最基本的查询就是返回该类的所有元素,使用 all() 方法即可做到。请注意,添加这些用户时,它们的 id 字段依次自动设置为 1 和 2。

    另外一种查询方式是使用用户的主键(即:id)直接获取用户实例:

    >>> u = User.query.get(1)
    >>> u
    <User john>
    

    现在添加一条用户动态:

    >>> u = User.query.get(1)
    >>> p = Post(body='my first post!', author=u)
    >>> db.session.add(p)
    >>> db.session.commit()
    

    不需要为手动设置 timestamp 字段的值,因为我们设置了该字段使用默认值。

    回想一下,我们在 User 类中使用 db.relationship 为用户添加了 posts 属性,在 Post 类中为用户动态添加了 author 属性。所以现在可以直接使用 author 虚拟字段来调用其作者,而不必通过用户 ID 来处理。SQLAlchemy 在这方面非常出色,因为它提供了对关系和外键的高级抽象。

    现在 Post 表的记录如下:

    id body timestamp user_id
    1 my first post! 2021-06-30 02:46:09.341436 1

    user_id 字段为 1,表示这条动态是属于 id 为 1 的用户的。

    下面演示关系数据库多表联查。

    通过用户查询动态:

    >>> u = User.query.get(1)
    >>> u
    <User john>
    >>> posts = u.posts.all()
    >>> posts
    [<Post my first post!>]
    
    >>> u = User.query.get(2)
    >>> u
    <User susan>
    >>> u.posts.all()
    []
    

    通过动态查询作者:

    >>> p = Post.query.get(1)
    >>> p
    <Post my first post!>
    >>> p.author
    <User john>
    

    以上只是最简单的演示,更多的内容可以看 flask-sqlalchemy 官方文档

    完成这些演示内容之后,我们还是要把刚才插入的数据库记录删除掉,为下一章节做准备:

    >>> 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()
    






    本文源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-04

    相关文章

      网友评论

          本文标题:04.数据库

          本文链接:https://www.haomeiwen.com/subject/zslqultx.html