数据库

5.1 SQL数据库

    关系型数据库把数据存储在表中,表模拟程序中不同的实体。例如,订单管理程序的数据库中可能有表customers、products和orders。

    表的列数是固定的,行数是可变的。列定义表所表示的实体的数据属性。例如,customers表中可能有name、address、phone等列。表中的行定义各列对应的真实数据。

    主键,其值为表中各行的唯一标识符。外键,引用同一个表或不同表中某行的主键。行之间的这种联系称为关系,这是关系型数据库模型的基础。

    图5-1展示了一个简单数据库的关系图。这个数据库中有两个表,分别存储用户和用户角色。连接两个表的线代表两个表之间的关系。

    在这个数据库关系图中,roles表存储所有可用的用户角色,每个角色都使用一个唯一的id值(即表的主键)进行标识。users表包含用户列表,每个用户也有唯一的id值。除了id主键之外,roles表中还有name列,users表中还有username列和password列。users表中的role_id列是外键,引用角色的id,通过这种方式为每个用户指定角色。

    一旦在roles表中修改完角色,所有通过role_id引用这个角色的用户都能立即看到更新。

5.2 NoSQL数据库

    所有不遵循上节所述的关系模型的数据库统称为NoSQL数据库。NoSQL数据库一般使用集合代替表,使用文档代替记录。NoSQL数据库采用的设计方式是联结变得困难,所以大多数数据库根本不支持这种操作。对于结果如图5-1所示的NoSQL数据库,若要列出各用户及其角色,就需要在程序中执行联结操作,即先读取每个用户的role_id,再在roles表中搜索对应的记录。


5.3 使用SQL还是NoSQL

    SQL数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证数据的一致性。NoSQL数据库放宽了对这种一致性的要求,从而获得性能上的优势。

5.4 Python数据库框架

Flask可以根据自己喜好选择使用MySQL、Postgres、SQLite、Redis、MongoDB或者CouchDB。

选择数据库框架时,你要考虑很多因素。

  • 易用性
  • 性能
  • 可移植性
  • Flask集成度

5.5 使用Flask-SQLAlchemy管理数据库

    Flask-SQLAlchemy是一个Flask扩展,简化了在Flask程序中使用SQLAlchemy的操作。

    SQLAlchemy是一个强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy提供了高层ORM,也提供了使用数据库原生SQL的底层功能。

pip install flask-sqlalchemy



    程序使用的数据库URL必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URI键中。配置对象中还有一个很有用的选项,即SQLALCHEMY_COMMIT_ON_TEARDOWN键,将其设为True时,每次请求结束后都会自动提交数据库中的变动。其他配置选项的作用参考Flask-SQLAlchemy的文档。Flask-SQLAlchemy文档

示例5-1 hello.py:配置数据库

#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

db = SQLAlchemy(app)

5.6 定义模型

示例5-2 hello.py:定义Role和User模型

#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

db = SQLAlchemy(app)

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)

    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer,primary_key=True)
    username = db.Column(db.String(64),unique=True,index=True)

    def __repr__(self):
        return '<User %r>' % self.username




5.7 关系

    图5-1所示的关系图表示用户和角色之间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每个用户都只能有一个角色。

    图5-1中的一对多关系在模型类中的表示方法如示例5-3所示。

示例5-3 hello.py:关系

class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    db.relationship()中的backref参数向User模型中添加一个role属性,从而定义反向关系。这一属性可替代role_id访问Role模型,此时获取的是模型对象,而不是外键的值。


5.8 数据库操作

5.8.1 创建表


在hello.py中添加:

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

最终hello.py:

#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)
    users = db.relationship('User',backref='role')

    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer,primary_key=True)
    username = db.Column(db.String(64),unique=True,index=True)
    role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))

    def __repr__(self):
        return '<User %r>' % self.username


更新现有数据库表的粗暴方式是先删除旧表在重新创建:

>>> db.drop_all()
>>> db.create_all()

5.8.2 插入行


    这些新建对象的id属性并没有明确设定,因为主键是由Flask-SQLAlchemy管理的。现在这些对象只存在于Python中,还未写入数据库。因此id尚未赋值:

>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None

    通过数据库会话管理对数据库所做的改动,在Flask-SQLAlchemy中,会话由db.session表示。准备把对象写入数据库之前,先要将其添加到会话中:

>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)

    或者简写成:

>>> db.session.add_all([admin_role, mod_role, user_role,
... user_john, user_susan, user_david])

    为了把对象写入数据库,我们要调用commit()方法提交会话:

>>> db.session.commit()

    再次查看id属性,现在它们已经赋值了:

>>> print(admin_role.id)
1
>>> print(mod_role.id)
2
>>> print(user_role.id)
3

5.8.3 修改行

>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()

5.8.4 删除行

>>> db.session.delete(mod_role)
>>> db.session.commit()

5.8.5 查询行


    使用过滤器可以配置query对象进行更精确的数据库查询。下面这个例子查找角色为"User"的所有用户:


    若要查看SQLAlchemy为查询生成的原生SQL查询语句,只需把query对象转换成字符串:


    如果你退出了shell会话,前面这些例子中创建的对象就不会以Python对象的形式存在,而是作为各自数据库表的行。如果你打开了一个新的shell会话,就要从数据库中读取行,再重新创建Python对象。下面这个例子发起了一个查询,加载名为"User"的用户角色:

>>> user_role = Role.query.filter_by(name='User').first()



    关系和查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对多关系:


    这个例子中的user_role.users查询有个小问题。执行user_role.users表达式时,隐含的查询会调用all()返回一个用户列表。query对象是隐藏的,因此无法指定更精确的查询过滤器。就这个特定示例而言,返回一个按照字母顺序排序的用户列表可能更好。在示例5-4中,我们修改了关系的设置,加入了lazy='dynamic'参数,从而禁止自动执行查询。

示例5-4 hello.py:动态关系

class Role(db.Model):
# ...
users = db.relationship('User', backref='role', lazy='dynamic')
# ...

    这样配置关系之后,user_role.users会返回一个尚未执行的查询,因此可以在其上添加过滤器:

>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>]
>>> user_role.users.count()
2

5.9 在视图函数中操作数据库

示例5-5 hello.py:在视图函数中操作数据库

#coding:utf8
from flask import Flask,render_template
from flask_wtf import Form
from flask_bootstrap import Bootstrap
app=Flask(__name__)
bootstrap=Bootstrap(app)

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username = form.name.data)
            db.session.add(user)
            session['known'] = False
        else:
            session['known'] = True
        session['name'] = form.name.data
        form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html',
        form = form, name = session.get('name'),
        known = session.get('known', False))

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

    提交表单后,程序会使用filter_by()查询过滤器在数据库中查找提交的名字。变量known被写入用户会话中,因此重定向后,可以把数据传给模板,用来显示自定义的欢迎消息。注意,要想让程序正常运行,你必须按照前面介绍的方法,在Python shell中创建数据库表。

示例5-6 templates/index.html

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
    {% if not known %}
    <p>Pleased to meet you!</p>
    {% else %}
    <p>Happy to see you again!</p>
    {% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

5.10 集成Python shell

    每次启动shell会话都要导入数据库实例和模型,这真是份枯燥的工作。为了避免一直重复导入,我们可以做些配置,让Flask-Script的shell命令自动导入特定的对象。

    若想把对象添加到导入列表中,我们要为shell命令注册一个make_context回调函数,如示例5-7所示。

示例5-7 hello.py:为shell命令添加一个上下文

#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
from flask import Flask,render_template
from flask_wtf import Form
from flask_bootstrap import Bootstrap
from flask_script import Shell,Manager
app=Flask(__name__)
bootstrap=Bootstrap(app)
manager=Manager(app)

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)
    users = db.relationship('User',backref='role',lazy='dynamic')

    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer,primary_key=True)
    username = db.Column(db.String(64),unique=True,index=True)
    role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))

    def __repr__(self):
        return '<User %r>' % self.username

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username = form.name.data)
            db.session.add(user)
            session['known'] = False
        else:
            session['known'] = True
        session['name'] = form.name.data
        form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html',
        form = form, name = session.get('name'),
        known = session.get('known', False))

def make_shell_context():
    return dict(app=app,db=db,User=User,Role=Role)
manager.add_command("shell",Shell(make_context=make_shell_context))

if __name__ == '__main__':
    #app.run(debug=True)
    manager.run()

make_shell_context()函数注册了程序、数据库实例以及模型,因此这些对象能在直接导入shell:

$ python hello.py shell
>>> app
<Flask 'app'>
>>> db
<SQLAlchemy engine='sqlite:home/flask/flasky/data.sqlite'>
>>> User
<class 'app.User'>

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

    SQLAlchemy的主力开发人员编写了一个迁移框架,称为Alembic(http://alembic.zzzcomputing.com/en/latest/)。除了直接使用Alembic之外,Flask程序还可使用Flask-Migrate(http://flask-migrate.readthedocs.io/en/latest/)扩展。这个扩展对Alembic做了轻量级包装,并集成到Flask-Script中,所有操作都通过Flask-Script命令完成。

5.11.1 创建迁移仓库

pip install flask-migrate

示例5-8 hello.py:配置Flask-Migrate

#coding:utf8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
from flask import Flask,render_template
from flask_wtf import Form
from flask_bootstrap import Bootstrap
from flask_script import Shell,Manager
from flask_migrate import Migrate,MigrateCommand
app=Flask(__name__)
bootstrap=Bootstrap(app)


basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)

migrate=Migrate(app,db)
manager=Manager(app)

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)
    users = db.relationship('User',backref='role',lazy='dynamic')

    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer,primary_key=True)
    username = db.Column(db.String(64),unique=True,index=True)
    role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))

    def __repr__(self):
        return '<User %r>' % self.username

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username = form.name.data)
            db.session.add(user)
            session['known'] = False
        else:
            session['known'] = True
        session['name'] = form.name.data
        form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html',
        form = form, name = session.get('name'),
        known = session.get('known', False))

def make_shell_context():
    return dict(app=app,db=db,User=User,Role=Role)
manager.add_command('db',MigrateCommand)

if __name__ == '__main__':
    #app.run(debug=True)
    manager.run()


这个命令会创建migrations文件夹,所有迁移脚本都存放其中。

5.11.2 创建迁移脚本

    在Alembic中,数据库迁移用迁移脚本表示。脚本中有两个函数,分别是upgrade()和downgrade()。upgrade()函数把迁移中的改动应用到数据库中,downgrade()函数则将改动删除。Alembic具有添加和删除改动的能力,因此数据库可重设到修改历史的任意一点。

    我们可以使用revision命令手动创建Alembic迁移,也可使用migrate命令自动创建。手动创建的迁移只是一个骨架,upgrade()和downgrade()函数都是空的,开发者要使用Alembic提供的Operations对象指令实现具体操作。自动创建的迁移会根据模型定义和数据库当前状态之间的差异生成upgrade()和downgrade()函数的内容。

migrate子命令用来自动创建迁移脚本:


5.11.3 更新数据库

    检查并修正好迁移脚本之后,我们可以使用db upgrade命令把迁移应用到数据库中:


    对第一个迁移来说,其作用和调用db.create_all()方法一样。但在后续的迁移中,upgrade命令能把改动应用到数据库中,且不影响其中保存的数据。














  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值