05. 数据库

5.1 SQL数据库


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

5.2 NoSQL数据库


NoSQL:所有不遵循关系模型的数据库

5.4 Python数据库框架


Python的数据库抽象层包:SQLAlchemy,MomgoEngine
ORM:对象关系型映射

5.5 使用Flask-SQLAlchemy管理数据库

在Flask-SQLAlchemy中,数据库试用URL指定;

数据库引擎 URL
MySQL mysql://username:password@hostname/database
Postgres postgresql://username:password@hostname/database
SQLite(windows) sqlite:///c:/absolute/path/to/database
SQLite(Unix) sqlite:///absolute/path/to/database

sqlite不需要使用服务器,因此不用指定hostname、username和password。database是硬盘上的文件名;

程序使用的数据库URL必须保存在配置对象的SQLALCHEMY_DATABASE_URI键中;
配置对象中还有一个有用的选项,SQLALCHEMY-COMMIT-ON-TEARDOWN键,设置为True后,请求成功后会自动提交数据库中的变动;

# 配置一个简单的SQLite数据库
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
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
# 创建SQLAlchemy类的实例
db = SQLAlchemy(app)

5.6 定义模型

在ORM(对象关系型映射)中,模型一般是一个Python类,类中的属性对应数据库表中的列;

# 利用Flask-SQLAlchemy创建的数据库实例
class Role(db.Model):
    __tablename__ = 'roles'      # 定义在数据库中使用的表名
    id = db.Column(db.Interger, primary_key = True)
    name = db.Column(db.String(64), unique = True)
    user = db.relationship('User', backref = 'role')
    
    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tabelname__ = 'user'
    id = db.Column(db.Interger, primary_key = True)
    username = db.Column(db.String(64), unique = True, index = True)
    def __repr__(self):
        return '<User %r>' % self.username

以上代码中,tablename定义数据库中使用的表名。
其余的类变量都是该模型的属性,被定义为db.column类的实例;db.column类构造函数的第一个参数是数据库列和模型属性的类型(P69),其余参数指定其他属性的配置选项;

Flask-SQLAlchemy要求每个模型都要定义主键,这一列经常命名为id。

选项名 说明
primary_key True,即为主键
unique True,此列不允许出现重复的值
index True,为这列创建索引,提升查询效率
nullable True,允许使用空值
default 为这列定义默认值

5.7 关系

# role和user的一对多关系的表示方法
class Role(db.model):
    # ...
    users = db.relationship('User', backref = 'role')

class User(db.model):
    # ...
    role_id = db.column(db.Interger, db.ForeignKey('roles.id'))
关系型数据库

以上代码中,创建了一个从role到user的一对多关系;
添加到User模型中的role_id列被定义为外键。外键中的db.ForeignKey('roles.id')表明这列的值是roles表中的id值;
添加到Role模型中的users属性代表这个关系的面向对象视角;针对Role类的实例,user属性返回与角色相关联的用户组成的列表。db.relationship第一个参数是这个关系的另一端是什么模型。其他关系选项如下:

选项名 说明
backref 在关系的另一个模型中添加反向引用
primaryjoin 明确指定两个模型之间的联结条件
lazy 指定如何加载相关记录,select,immediate,joined,subquery,noload,dynamic
userlist 设置为False,不使用列表,而是用标量值

常见的关系主要有一对多关系,多侧模型增加外键;
多对一关系对调两个表即可,也可以把外键和db.relationship都放在多这一侧;

5.8数据库操作

创建表

(venv) $ python hello.py shell
>>> from hello import db
>>> db.create_all()
>>> db.drop_all()

插入行

首先创建一些角色和用户

>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderate')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role) 
>>> user_susan = User(username='susan', role=user_role) 
>>> user_david = User(username='david', role=user_role)
# 以上新建对象的id属性没有明确设定,因为主键由Flask-SQLAlchemy管理,
# 这些对象只存在在Python中,未写入数据库

# db.session.add()将对象先写入会话中
>>> db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])

# db.session.commit() 提交会话
# 提交后,每个对象的id会被赋值
>>> db.session.commit()

数据库会话也可以使用db.session.rollback()回滚

修改行

在数据库会话上调用add()方法也能更新模型

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

删除行

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

查询行

>>> User.query.all()
>>> User.query.filter_by(role=user_role).all()

如果退出了shell对话,前面例子中创建的对象会作为各自数据表中的行。如果打开了新的shell对话,就要从数据库中读取行,再重新创建Python对象。
常用的SQLAlchemy查询过滤器

过滤器 说明
filter() 将过滤器添加到原查询上
filter_by() 将等值过滤器添加到原查询上
limit()

常用的SQLAlchemy查询执行函数

方法 说明
all() 以列表形式返回所有结果
first() 返回查询的第一个结果,没有结果返回None
first_or_404() 返回查询的第一个结果,没有则返回404
get() 返回指定主键对应的行,没有结果返回None
get_or_404() 返回指定主键对应的行,没有结果返回404
count() 返回查询结果的数量
paginate() 返回一个paginate对象

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

下例为把用户输入的名字写入数据库的代码

import os
from flask import Flask, render_template, session, redirect, url_for
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
from flask_sqlalchemy import SQLAlchemy

# get abspath of this file
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

manager = Manager(app)
bootstrap = Bootstrap(app)
moment = Moment(app)
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)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

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

# define a form class
class NameForm(Form):
    name = StringField('Input your name', validators=[Required()])
    submit = SubmitField('SUBMIT')


@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()                     # create a form instance
    if form.validate_on_submit():
        # .query.filter_by().first(): get the correct User, if not, return None
        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
        return redirect(url_for('index'))
    return render_template('index.html',
                           form=form,
                           name=session.get('name'),
                           known=session.get('known', False))


if __name__ == '__main__':
    manager.run()

小结:

  1. from flask import session: session作为用户会话,是一种私有存储,存在每个连接到服务器的客户端中;
  2. return render_template()中使用session.get('name')是为了防止没有键的情况下异常返回,get返回None
  3. 视图函数逻辑:表单后台验证成功后,检索数据库中username等于输入的值的行;如果不存在,就将通过实例化User创建这一行,并添加到session中供传入数据库;

5.10 集成Python Shell

避免每次启动shell都要导入数据库实例和模型

# python shell
# do not need to `from addToSQL import Role, User, app, db`
# make_shell_context register app, db ...
def make_shell_context():
    return dict(app=app, db=db, Role=Role, User=User)
manager.add_command("shell", Shell(make_context=make_shell_context))

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

更新表的唯一方式是先删除旧表,不过这样会丢失所有数据

创建迁移仓库

from flask_migrate import Migrate, MigrateCommand
migrate=Migrate(app, db)
manager.add_command('db', MigrateCommand)

创建迁移脚本

数据库迁移用迁移脚本表示。脚本中有upgrade()和downgrade()两个函数。
使用migrate子命令自动创建迁移脚本
python hello.py db migrate -m "migrate 1st"

更新数据库

将迁移应用到数据库
python hello.py db upgrade

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容