使用Flask-SQLAlchemy管理数据库

发布于 2020-02-02  15 次阅读


一、写在前面

想了想,整体笔记这个表述还是太过于笼统了,单独拎出来也方便查询复盘

这里我们只是涉及到Flask-SQLAlchemy的使用层次上,深入的架构关系处理按下不表

以下内容参考摘抄于《Flask+Web开发实战》,是本好书,感谢作者李辉

二、初始配置

1. 安装

> pip install flask-sqlclchemy

2. 扩展的初始化:

实例化Flask-SQLAlchemy提供的SQLAlchemy类,传入程序实例app,完成

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)

3. 连接数据库服务器<

3.1 配置SQLite数据库URI
import os
 ...
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///' + os.path.join(app.root_path, 'db.sqlite3'))
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

这里图方便用的就是SQLite,只需要提供文件路径即可[这里是app同路径下db.sqlite3],但在生产环境下更换到其他类型的DBMS时,数据库URL会包含敏感信息,所以这里优先从环境变量DATABASE_URL获取,【注意不同操作系统下斜杆数不一致】所以也就玩具项目图方便用下,其他情况下还是用Mysql等更专业的吧

3.2 配置Mysql数据库URI
import os
 ...
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL','mysql://root:password@127.0.0.1:3306/database_name')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

需要提前开启Mysql服务器以及创建好database_name

安装并初始化Flask-SQLAlchemy后,启动程序时会看到命令行下有一行警告信息。这是因为Flask-SQLAlchemy建议你设置SQLALCHEMY_TRACK_MODIFICATIONS 配置变量,这个配置变量决定是否追踪对象的修改,这用于Flask-SQLAlchemy的事 件通知系统。这个配置键的默认值为None,如果没有特殊需要,我们可以把它设为 False来关闭警告信息:

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

三、定义数据库模型

这里我就演示了下几个常用的

from datetime import datetime
 ...
class Note(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    author = db.Column(db.String(25), nullable=False)
    created_time = db.Column(db.DateTime, default=datetime.now, nullable = False)
    body = db.Column(db.Text,default='Write Something')

默认情况下,Flask-SQLAlchemy会根据模型类的名称生成一个表名称,生成规则如下:

Message --> message # 单个单词转换为小写
FooBar --> foo_bar # 多个单词转换为小写并使用下划线分隔

Note类对应的表名称即note。如果你想自己指定表名称,可以通过定义 __tablename__属性来实现。

四、创建数据库表

定义好模型后就可以直接创建了

import click
 ...
@app.cli.command()
@click.option('--drop', is_flag=True, help='Create after drop.')
def initdb(drop):
    if drop:
        click.confirm('This operation will delete the database, do you want to continue?', abort=True)
        db.drop_all()
        click.echo('Drop tables.')
    db.create_all()
    click.echo('Initialized database.')

这里自定义了个flask命令flask initdb,用click还添加了个--drop的参数

但是我们这里为了让数据库中实际的表同步数据模型的变化【添加或删除字段,修改字段的名称和类型之类的】是直接暴力的db.drop_all()后再重新db.create_all(),会直接清空表中原有的数据,迁移表原有数据后文中写

五、数据库操作

为了方便,接下来使用flask shell进行演示,在此之前我们先将自己定义的数据库模型类以及db手动传入到上下文中

# 传入shell上下文
@app.shell_context_processor
def make_shell_context():
    return dict(db=db, Note=Note)

终端进入项目所在路径执行以下命令

> flask initdb --drop
This operation will delete the database, do you want to continue? [y/N]: y
Drop tables.
Initialized database.
> flask shell
Python 3.7.5 (default, Nov 29 2019, 17:17:51) 
[Clang 11.0.0 (clang-1100.0.20.17)] on darwin
App: app [production]
Instance: /Users/Mosaic/Programming/Flask/instance
>>>

用上一小节的命令flask initdb --drop我们先同步好了模型表,然后进入flask shell

下文中>>>开头的都代表是在flask shell模式中

增删改查

>>> note1 = Note(body='hello')
>>> note2 = Note(body='world')
>>> db.session.add(note1) # 增
>>> db.session.add(note2)
>>> db.session.commit() # 每次更改都要commit才能同步到数据库
>>> db.session.delete(note1) # 删
>>> db.session.commit()
>>> note1.body = 'giao!' # 改
>>> db.session.commit()
>>> Note.query.filter_by(body='giao!').first() == note1 # 查
True

一般来说,一个完整的查询遵循下面的模式: <模型类>.query.<过滤方法>.<查询方法>

和filter()方法相比,filter_by()方法更易于使用。在filter_by() 方法中,你可以使用关键字表达式来指定过滤规则。更方便的是,你可以在这个过 滤器中直接使用字段名称。下面的示例使用filter_by()过滤器完成了同样的任务:

Note.query.filter_by(body='SHAVE').first()
# 等价于 Note.query.filter(Note.body='SHAVE').first()

其他的不一一举例了

六、建立模型之间几种基础的关系模式

本来只是想总结下这个知识点的...想想算了,还是写完整点吧

1.单向和双向关系

class Writer(db.Model):    
  	id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(70), unique=True)
    books = db.relationship('Book', back_populates='writer')
    # books = db.relationship('Book')
class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), index=True)
    writer_id = db.Column(db.Integer, db.ForeignKey('writer.id'))
    writer = db.relationship('Writer', back_populates='books')
    # writer = db.relationship('Writer')    

这里我们定义了两个模型,都定义了db.relationship

relationship()函数的第一个参数为关系另一侧的模型名称,在Writer中它会告诉 SQLAlchemy将Writer类与Book类建立关系

如果db.relationship只被定义在Writer中,而Book中没有,就叫单向关系【只能从Writer查询到books,不能从Book方向查询自己的Writer】,两边都有就是双向关系【双方可以互查】

而back_populates参数是用来连接两边的relationship的,值为对面关系名字,从而实现未commit之前两边关系的同步【例如删掉了Writer A的Book B,Book B的Writer A也会被同步删除】,但是不管加没加back_populates参数,commit之后两边关系总会同步好的【之所以加重未commit之前是因为自己一个个测试出来的...当初对back_populates这个参数有点疑惑,涉及到了后面的级联】

其他参数如下,不一一举例了,backref是隐式双向,不推荐,uselist可以用在一对一关系中,secondary用在多对多关系中

【下面几种关系都默认使用双向关系】

2.一对多/多对一

定义关系的第一步是创建外键。外键是(foreign key)用来在A表存储B表的主键值以便和B表建立联系的关系字段。因为外键只能存储单一数据(标量),所以外键总是在“多”这一侧定义,多本书属于同一个作者,所以我们需要为每本书添加外键存储作者的主键值以指向对应的作者。在Book模型中,我们定义一 个writer_id字段作为外键:

class Book(db.Model):
  ...
		writer_id = db.Column(db.Integer, db.ForeignKey('writer.id'))

这个字段使用db.ForeignKey类定义为外键,传入关系另一侧的表名和主键字段名,即writer.id。实际的效果是将book表的writer_id的值限制为Writer表的id列的值。它将用来存储Writer表中记录的主键值

再加上双向关系,一个一对多/多对一就完成了

class Writer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(70), unique=True)
    books = db.relationship('Book', back_populates='writer')

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), index=True)
    writer_id = db.Column(db.Integer, db.ForeignKey('writer.id')) 
    writer = db.relationship('Writer', back_populates='books')

当Writer查询books时,会返回所有Book.writer_id与Writer.id一致的Book【返回列表】

而当Book查询Writer时,会返回Writer.id与writer_id一致的Writer【返回单个值】

3.一对一

一对一就是再一对多/多对一的基础上加上了个参数,下面假设一个作者一生只写一本书,一本书也只能有一个作者

class Writer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(70), unique=True)
    book = db.relationship('Book', back_populates='writer', uselist=False)

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), index=True)
    writer_id = db.Column(db.Integer, db.ForeignKey('writer.id')) 
    writer = db.relationship('Writer', back_populates='books')

在Writer的relationship加上了uselist=False,这将表示只能返回单个值了

4.多对多

需要额外定义一个表来处理两边的外键对应关系

association_table = db.Table('association',db.Column('student_id', db.Integer, db.ForeignKey('student.id')),db.Column('teacher_id', db.Integer, db.ForeignKey('teacher.id'))
)
class Student(db.Model):
    id = db.Column(db.Integer, primary_key=True) 
    name = db.Column(db.String(70), unique=True) 
    grade = db.Column(db.String(20))
    teachers = db.relationship('Teacher',
    secondary=association_table, back_populates='students')
class Teacher(db.Model):
    id = db.Column(db.Integer, primary_key=True) 
    name = db.Column(db.String(70), unique=True) 
    office = db.Column(db.String(20))

七、迁移数据库

我们使用Flask-Migrate来实现保留数据更改表

1.安装

> pip install flask-migrate

2.实例化

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
 ...
db = SQLAlchemy(app)
migrate = Migrate(app, db) # 在db对象创建后调用

3.创建迁移环境

在开始迁移数据之前,需要先使用下面的命令创建一个迁移环境:

> flask db init

4.生成迁移脚本

使用migrate子命令可以自动生成迁移脚本:

> flask db migrate -m "add note timestamp"
    ...
INFO [alembic.autogenerate.compare] Detected added column 'message.timestamp
Generating /Path/to/your/database/migrations/versions/c52a02014635_add note_timestamp.py ... done

这条命令可以简单理解为在flask里对数据库(db)进行迁移(migrate)。

-m 选项用来添加迁移备注信息。

从上面的输出信息我们可以看到,Alembic检测出了 模型的变化:表note新添加了一个timestamp列,并且相应生成了一个迁移脚本 c52a02014635_add_note_timestamp.py

【迁移命令是由Alembic自动生成的,其中可能包含错误,所以有必要在生成后检查一下。】

因为每一次迁移都会生成新的迁移脚本,而且Alembic为每一次迁移都生成了修订版本(revision)ID,所以数据库可以恢复到修改历史中的任一点。正因为如此,迁移环境中的文件也要纳入版本控制。

有些复杂的操作无法实现自动迁移,这时可以使用revision命令手动创建迁移脚本。这同样会生成一个迁移脚本,不过脚本中的upgrade()和downgrade()函数都是空的。你需要使用Alembic提供的Operations对象指令在这两个函数中实现具体操作,具体可以访问Alembic官方文档查看。

5.更新数据库

生成了迁移脚本后,使用upgrade子命令即可更新数据库:

> flask db upgrade 
...INFO [alembic.runtime.migration] Running upgrade -> c52a02014635, add note timestamp

如果还没有创建数据库和表,这个命令会自动创建;如果已经创建,则会在不损坏数据的前提下执行更新。

如果你想回滚迁移,那么可以使用downgrade命令(降级),它会撤销最后一 次迁移在数据库中的改动,这在开发时非常有用。比如,当你执行upgrade命令后,发现某些地方出错了,这时就可以执行flask db downgrade命令进行回滚,删除对应的迁移脚本,重新生成迁移脚本后再进行更新(upgrade)。

八、最后

还有一些什么级联操作,事件监听之类的没有提到......

至少今天不想写了,写笔记的时间比学习内容时间都要多是好事还是坏事,emmmmm,不过思路还是清晰了,管他呢~


一只正在转生画师的技术宅