使用异步ORM aiomysql.sa

前言

因为系统分析与设计大作业的web框架我选择了使用aiohttp这个异步框架,所以ORM的选择上要做出改变。sqlalchemy是不支持异步IO的,所以用sqlalchemy搭配aiohttp并不可行,最终我选择了整合了sqlalchemy的aiomysql.sa,这是一个支持mysql的异步ORM。

相关的学习链接
官方文档
用法参考

从官方文档上我们可以看到

image_1ced39bc8n731hlgijuectil9.png-12.2kB

aiomysql的api接口跟aiopg是非常类似的,可以在学习中参考一下aiopg,同时aiomysql.sa是整合了sqlalchemy的,也可以参考sqlalchemy的文档。为什么我要这么说呢,因为aiomysql.sa官方提供的文档描述相当有限,在学习过程中必须参考少之又少的资料,并且这些资料可能因为版本问题还不一定是对的。


基本用法

python的异步io很多都是基于python的协程实现的,asyncio就是,同时aiomysql.sa也是底层由协程实现,所以我们是使用aiomysql.sa过程中,都需要定义各种操作为协程

1.初始化引擎

初始化引擎跟mysql.connector很类似,但是返回一个engine实例
image_1ced3mktqg6t1hr35mg1p1qed0m.png-28.8kB
例子

1
2
3
async def init_engine():
engine = await aiomysql.sa.create_engine(user = config["user"], db = config["database"], host = config["host"], password = config["password"])
return engine

2.引擎获取与事务
这部分我看了下文档和github上的用法参考,感觉都不怎么正确,可能因为版本问题,自己总结了如下

1
2
3
4
async with engine.acquire() as conn:
trans = await conn.begin()
await conn.execute(food.insert().values(name = "test_food"))
await trans.commit()

引擎资源的获取需要engine.acquire()来获取,返回值为一个SAconnection实例,可以进行execute(query, *multiparams, **params)操作。

最好在with语句里面使用acquire,这样可以自动释放engine资源。

aiomysql.sa支持事务,它无论是insert delete还是select都需要事务的提交,没错你没看错,select它都必须你提交事务,不然会报错

1
Failed to release a connection with transaction started at 'aiomysql'

同时这个报错有点意思,如果你没有主动提交事务,它这个报错一定会出现的,但是如果你之前的增删查着操作出现了错误,它会导致整个事务失败,最终这个事务没办法成功提交,也会报这个错误。也就是说如果出现这个错误,并一定就是你事务没有主动提交,而是有可能你的增删查着操作出现了错误导致事务的失败。

事务的获取和提交,事务的获取可以通过SAconnection.begin()获取这个事务

1
trans = await conn.begin()

事务提交

1
await trans.commit()

嵌套事务
文档中这样描述嵌套事务

Nested calls to begin() on the same SAConnection will return new Transaction objects that represent an emulated transaction within the scope of the enclosing transaction

在同一个SAconnection中嵌套调用begin的话,会在外层事务中间返回一个模仿的事务对象

嵌套事务的特点

Calls to Transaction.commit() only have an effect when invoked via the outermost Transaction object, though the Transaction.rollback() method of any of the Transaction objects will roll back the transaction

只有当最外层的事务被调用,那么事务才会被提交,例子如下

1
2
3
4
trans = yield from conn.begin()   # outermost transaction
trans2 = yield from conn.begin() # "inner"
yield from trans2.commit() # does nothing
yield from trans.commit() # actually commits

最后说一数据结构的声明,因为在sqlalchemy中我们一般比较习惯使用class User(Base)这样的方式来定义我们的数据库表结构,因为这符合我们面向对象编程的习惯,同时定义了这些class之后我们可以在稍后的增删查着操作中使用这些class。但是在aiomysql.sa中这样做并不好。官方文档中这样解释
image_1ced511agkr510uc104oevv1qr013.png-59.7kB

就是说Question.query.filter_by(question_text=’Why’).first()或者session.query(TableName).all()之类的查询并不支持异步,所以把表结构定义为class并没有什么帮助,正确的做法是定义为Table对象。

关于class和Table对象的关系我之前一篇博客有说过,就是创建一个class就会自动生成一个同名的Table对象,并且使用一个Mapper对象将这两个对象映射在一起。

所以我们在aiomysql.sa中定义我们的数据库表结构大多形如下面

1
2
3
4
5
6
7
8
9
10
11
12
13
food = sa.Table(
"food",
meta,
sa.Column("id", sa.Integer, primary_key = True),
sa.Column("name", sa.String(50), unique = True, nullable = False),
sa.Column("picture", sa.String(50)),
sa.Column("price", sa.Integer, nullable = False),
sa.Column("description", sa.String(50)),
sa.Column("rating", sa.Float),
sa.Column("amount", sa.Integer, nullable = False),
sa.Column("likes", sa.Integer, default = 0),
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tag.id"), nullable = False)
)

基本用法基本就是上面所说的了,具体可以参考一下我github上系统分析与设计的web后台