SQLAlchemy 基础知识 - (autoflush 和 autocommit)(relationship 和 backref)(flask migrate迁移数据)

全栈工程师开发手册 (作者:栾鹏)
python教程全解

SQLAlchemy 基础

下面是一段官方 SQLAlchemy 使用示例,我们从这个例子出发,认识 SQLAlchemy。

from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker


# sqlite3/mysql/postgres engine
# 请先自己在 MySQL 中创建一个名为 test_tmp 的 database
engine = create_engine('mysql://root@localhost/test_tmp', echo=False)

Base = declarative_base()

Session = sessionmaker(bind=engine)

session1 = Session()
session2 = Session()

SessionNoAutoflush = sessionmaker(bind=engine, autoflush=False)
session3 = SessionNoAutoflush()


class User(Base):
    __tablename__  = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String(64))

autoflush 和 autocommit

session 是什么?

目前还不知道怎样直接给 session 下定义,但是我们可以通过它的一些用途来认识它, 在脑海里脑补出这个东西。

  1. session 会在需要的时候(比如用户读取数据、更新数据时)和数据库进行通信,获取数据对象,并有一个池子来维护这些对象,保证你访问数据时不出现意外的问题
  2. session 和连接(connection) 不等同,session 通过连接和数据库进行通信
  3. session 是 Query 的入口,当你想要发起查询的时候,一般用法是:session.Query(Model).filter_by(...).first()

如果不完全理解它,也没关系,有个大概印象即可,以后碰到具体的问题再具体分析, 到时候就可以针对性解决。

官方介绍 session 的资料:https://docs.sqlalchemy.org/en/13/orm/session_basics.html#what-does-the-session-do

autoflush 参数

首先,学习两个概念:flush 和 commit。

  • flush 的意思就是将当前 session 存在的变更发给数据库,换句话说,就是让数据库执行 SQL 语句。
  • commit 的意思是提交一个事务。一个事务里面可能有一条或者多条 SQL 语句
  • SQLAlchemy 在执行 commit 之前,肯定会执行 flush 操作;而在执行 flush 的时候,不一定执行 commit,这个主要视 autocommit 参数而定,后面会详细讲

当 autoflush 为 True 时(默认是 True),session 进行查询之前会自动把当前累计的修改发送到数据库(注意:autoflush 并不是说在 session.add 之后会自动 flush),举个例子(结合开始的代码):

# 创建了一个对象,这时,这个对象几乎没有任何意义,session 不知道它的存在
>>> user = User(name='cosven')
>>> 
# session1.add 这个对象之后,它被 session 放到它的对象池里面去了,但这时不会发送任何 SQL 语句给数据库,数据库目前仍然不知道它的存在
>>>  session1.add(user)
>>>  
# session1.Query 执行之前,由于 autoflush 是 True,session1 会先执行 session1.flush(),然后再发送查询语句
# 当 session 进行 flush 操作时,session 会先建立(选)一个和数据库的连接,然后将创建 user 的 SQL 语句发送给数据库
# 所以,这个查询是能查到 user 的
>>> session1.query(User).filter_by(name='cosven').first()
<__main__.User object at 0x1108f04e0>

如果 session 的 autoflush 为 False 的话,session 进行查询之前不会把当前累计的修改发送到数据库,而直接发送查询语句,所以下面这个查询是查不到对象的。

>>> session3.add(User(name='haha'))
>>> session3.query(User).filter_by(name='haha').first()  # None

再重复的总结一下:

session.flush 的意义:session 计算自己积累的变更,将变更对应的 SQL 语句发送给数据库。 autoflush 的意义:session 在进行查询之前,自动的进行一次 flush 操作。

autocommit 参数

commit 对应的概念是事务(transaction),默认情况下,session 参数 autocommit 的值是 False,SQLAlchemy 也推荐将它设置为 False。

注:MySQL client 默认是将 autocommit 设为 True 的,所以我们在 cli 中执行一条 SQL 语句,数据库的数据就会发生变化

这里复习一下一个基础知识点:在一个事务被提交之前,事务里面的修改只对当前事务可见,其它事务看不见。什么意思?我们看个例子

# ps: session1 的 autocommit 参数为 False, autoflush 参数为 True
# 当 session1 执行 add 操作时,
>>> session1.add(User(name='miao'))

# session1 中是可以查到这个 user 的
>>> session1.query(User).filter_by(name='miao').first()
<__main__.User object at 0x1108f00000>

# session3 中查不到
>>> session3.query(User).filter_by(name='miao').first()  # None

# 让 session1 提交一下当前的事务
>>> session1.commit()

# 再从 session3 中查
>>> session3.query(User).filter_by(name='miao').first() is not None
True

事务不仅可以提交,还可以 rollback,这里就不讲。

relationship & backref

falsk 官方文档 http://flask-sqlalchemy.pocoo.org/2.1/models/ 后发现,以前试图孤立地理解backref是问题之源,backref是与relationship配合使用的。

一对多关系

db.relationship()用于在两个表之间建立一对多关系。例如书中 roles 表中一个 User 角色,可以对应 users 表中多个实际的普通用户。实现这种关系时,要在“多”这一侧加入一个外键,指向“一”这一侧联接的记录。


class Role(db.Model):
# ...

users = db.relationship('User', backref='role')

class User(db.Model):

# ...

role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

relationship & ForeighKey

大多数情况下, db.relationship() 都能自行找到关系中的外键, 但有时却无法决定把 哪一列作为外键。 例如, 如果 User 模型中有两个或以上的列定义为 Role 模型的外键, SQLAlchemy 就不知道该使用哪列。如果无法决定外键,你就要为 db.relationship() 提供额外参数,从而确定所用外键。(见书 P49)

relationship & backref

通过db.relationship(),Role 模型有了一个可以获得对应角色所有用户的属性users。默认是列表形式,lazy='dynamic'时返回的是一个 query 对象。即relationship提供了 Role 对 User 的访问。

backref正好相反,提供了 User 对 Role 的访问。

不妨设一个 Role 实例为 user_role,一个 User 实例为 u。relationship 使 user_role.users 可以访问所有符合角色的用户,而 backref 使 u.role 可以获得用户对应的角色。

示例


$ p manage.py shell


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


>>> user_role


[<Role u'User'>]

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

>>> user_role

<Role u'User'>

>>> user_role.users

<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x1087c1050>

>>> user_role.users.order_by(User.username).all()

[<User u'alice78'>, <User u'andrea86'>, <User u'hmr'>]

>>> Role.query.all()

[<Role u'Moderator'>, <Role u'Administrator'>, <Role u'User'>]

>>> user_role.users.count()
3

>>> u = User.query.filter_by(username='hmr').first()

>>> u

<User u'hmr'>

>>> u.role

<Role u'User'>


一对一关系

除了一对多之外, 还有几种其他的关系类型。一对一关系可以用前面介绍的一对多关系表示, 但调用 db.relationship() 时要把 uselist 设为 False , 把“多”变成“一”。

多对多关系

如果你想要用多对多关系,你需要定义一个用于关系的辅助表。对于这个辅助表, 强烈建议 使用模型,而是采用一个实际的表:

tags = db.Table('tags',
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
    db.Column('page_id', db.Integer, db.ForeignKey('page.id'))
)
class Page(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    tags = db.relationship('Tag', secondary=tags,
        backref=db.backref('pages', lazy='dynamic'))
classs Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)

这里我们配置 Page.tags 加载后作为标签的列表,因为我们并不期望每页出现 太多的标签。而每个 tag 的页面列表( Tag.pages )是一个动态的反向引用。 正如上面提到的,这意味着你会得到一个可以发起 select 的查询对象。

SQLAlchemy MySQL 调试小技巧

为 MySQL 打开查询 log

SET GLOBAL log_output = "FILE"; the default.
SET GLOBAL general_log_file = "/path/to/your/mysql.log";
SET GLOBAL general_log = 'ON';

然后在 shell 中 tail -f mysql.log,这样一来,当 MySQL 收到请求时,你就能看到一条日志, 这样可以方便你判断 session 执行什么操作时,会发送 SQL 语句,什么时候建立连接。

日志示例:

2018-11-08T15:12:41.332513Z    53 Query commit
2018-11-08T15:12:41.333753Z    53 Query rollback
2018-11-08T15:12:45.999996Z    43 Query select * from user

将上面的脚本导入 python 或者 ipython

python -i test.py

使用 flask migrate 来迁移数据结构

Flask Migrate 基于 Alembic,Alembic 是 SQLAlchemy 作者开发的数据迁移工具。
文档主页:https://flask-migrate.readthedocs.io/en/latest/

安装

pip install Flask-Migrate

在安装完成之后需要在代码中添加如下代码

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

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

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

在添加代码之后需要运行

flask db init

来初始化项目,命令运行之后会在项目中生成 migrations 文件夹,该文件夹需要添加到版本控制。

之后可以使用 migrate 命令来初始化迁移

flask db migrate

迁移脚本不会检测models 的所有变更, Alembic 目前无法检测表名修改,列名修改,其他限制可以在Alembic网站查看。

之后可以应用该迁移

flask db upgrade

运行该命令来将修改应用到数据库,以后对model的每一次修改需要重复 migrate 和 upgrade 命令。如果要在不同机器之间同步数据库结构,只需要同步 migrations 文件夹,并且在另一台机器上运行 flask db upgrade 即可。

Flask Migrate 也支持直接使用脚本的方式运行,具体可参考官方的文档,非常易懂。

自动生成的可能会有些错误,下面给几个示例

from alembic import op
import sqlalchemy as sa
from sqlalchemy_utils import EncryptedType




batch_op.add_column(sa.Column("extra_json", sa.Text(), nullable=True))
batch_op.drop_column("extra_json")

op.create_table(
        "user_attribute",
        sa.Column("created_on", sa.DateTime(), nullable=True),
        sa.Column("changed_on", sa.DateTime(), nullable=True),
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column("user_id", sa.Integer(), nullable=True),
        sa.Column("welcome_dashboard_id", sa.Integer(), nullable=True),
        sa.Column("created_by_fk", sa.Integer(), nullable=True),
        sa.Column("changed_by_fk", sa.Integer(), nullable=True),
        sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
        sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
        sa.ForeignKeyConstraint(["user_id"], ["ab_user.id"]),
        sa.ForeignKeyConstraint(["welcome_dashboard_id"], ["dashboards.id"]),
        sa.PrimaryKeyConstraint("id"),
    )
op.drop_table("user_attribute")

batch_op.alter_column(
            "position_json",
            existing_type=sa.Text(),
            type_=MediumText(),
            existing_nullable=True,
        )
        
op.add_column("dashboards", sa.Column("slug", sa.String(length=255), nullable=True))
try:
    op.create_unique_constraint("idx_unique_slug", "dashboards", ["slug"])
except:
    pass

op.drop_constraint(None, "dashboards", type_="unique")
op.drop_column("dashboards", "slug")

sa.Column('metadata_last_refreshed', sa.DateTime(), nullable=True),
sa.Column('cache_timeout', sa.Integer(), nullable=True),
sa.Column('broker_user', sa.String(length=255), nullable=True),
sa.Column('broker_pass', EncryptedType(), nullable=True),
sa.Column('position_json', sa.Text(), nullable=True),
sa.Column('impersonate_user', sa.Boolean(), nullable=True),
sa.Column('delivery_type', sa.Enum('attachment', 'inline', name='emaildeliverytype'), nullable=True),

参考:https://blog.csdn.net/mr_hui_/article/details/83217566
https://zhuanlan.zhihu.com/p/48994990
http://einverne.github.io/post/2018/05/flask-migrate-tutorial.html

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值