上手指南

这是一篇写给刚入坑同学的指南,将介绍 GINO 的基本部分。阅读之前,请先了解以下知识点:

  • 关系型数据库,尤其是 PostgreSQL
  • Python 异步编程

您不需要对 SQLAlchemy 有所了解。

介绍

简单来说,GINO 可以在您的异步应用中帮助您完成 SQL 语句的生成及执行,您只需要通过友好的对象化 API 来操作您的数据即可,无需亲自编写 SQL 与数据库交互。

因为异步编程并不会使您的程序变快——如果说不拖累的话——而且还会增加复杂度和风险,所以也许您并不需要 GINO 或者说是异步数据库连接。跳坑之前请先阅读 Why Asynchronous ORM?

安装

稳定版

请在终端中执行以下命令以安装 GINO:

$ pip install gino

以上就是安装 GINO 的推荐方式,因为这种方式始终会去安装最新的稳定版。

如果您还没有安装过 pip,您可以参阅 Python installation guide

从源码安装

GINO 的源码可以从 Github repo 下载。

您可以 git clone:

$ git clone git://github.com/fantix/gino

或者下载 tarball

$ curl  -OL https://github.com/fantix/gino/tarball/master

拿到源码后,您可以这样安装:

$ python setup.py install

声明模型

开始之前,我们需要先创建一个 Gino 的全局实例,通常叫做 db

from gino import Gino

db = Gino()

db 可以被当做是数据库的一个代表,后续大部分的数据库交互都将通过它来完成。

“Model” 是 GINO 中的一个基本概念,它表示继承自 db.Model 的用户定义类。每个 Model 的子类代表了数据库中的一张表,而这些类的对象则代表了对应表中的一行数据。如果您曾经使用过其它 ORM 产品,对这种映射关系应该不感到陌生。现在我们尝试定义一个 model:

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer(), primary_key=True)
    nickname = db.Column(db.Unicode(), default='noname')

By declaring this User class, we are actually defining a database table named users, with two columns id and nickname. Note that the fixed __tablename__ property is required. GINO suggests singular for model names, and plural for table names. Each db.Column property defines one column for the table, where its first parameter indicates the column type in database, while the rest is for other column attributes or constraints. You can find a mapping of database types to db types here in the SQLAlchemy documentation.

注解

SQLAlchemy 是 Python 中一个强大的非异步 ORM 库,而 GINO 就是基于其构建的。通过不同的 SQL 方言实现,SQLAlchemy 支持包括 PostgreSQL 和 MySQL 在内的许多流行的 RDBMS,以至于有时相同的 Python 代码可以不经修改地运行在不同的数据库上。GINO 自然也承袭了这一特性,但目前暂仅支持 PostgreSQL(通过 asyncpg)。

如果需要定义涵盖多个列的数据库约束或索引,您仍然可以通过 model 类属性的方式来定义,属性名称虽未被用到,但不能重复。例如:

class Booking(db.Model):
    __tablename__ = 'bookings'

   day = db.Column(db.Date)
   booker = db.Column(db.String)
   room = db.Column(db.String)

   _pk = db.PrimaryKeyConstraint('day', 'booker', name='bookings_pkey')
   _idx1 = db.Index('bookings_idx_day_room', 'day', 'room', unique=True)
   _idx2 = db.Index('bookings_idx_booker_room', 'booker', 'room')

It is also possible to define model constraints and indexes outside the model class if that is preferred. For more details on constraints and indexes, see here in the SQLAlchemy documentation.

由于一些限制,目前不允许在父类中直接使用类属性的方式来单独定义数据库约束和索引,__table_args__ 也是一样的。GINO 提供了 declared_attr() 来实现比如 mixin 类这样的功能,更多信息请参阅其 API 文档。

建立连接

前面的声明只是定义了映射关系,并非实际在数据库中创建了这些表结构。为了使用 GINO 来创建表,我们需要先与数据库建立连接。这里我们先为本指南创建一个 PostgreSQL 的数据库实例:

$ createdb gino

然后,告诉我们的 db 对象去连接这个数据库:

import asyncio

async def main():
    await db.set_bind('postgresql://localhost/gino')

asyncio.get_event_loop().run_until_complete(main())

If this runs successfully, then you are connected to the newly created database. Here postgresql indicates the database dialect to use (the default driver is asyncpg, you can explicitly specify that with postgresql+asyncpg://, or simply asyncpg://), localhost is where the server is, and gino is the name of the database. Check here for more information about how to compose this database URL.

注解

在底层,set_bind() 调用了 create_engine() 来创建 engine,并将其绑定到 db 对象上。GINO engine 与 SQLAlchemy engine 类似,但 GINO engine 是异步的,而后者是阻塞式的。关于如何使用 engine,请参考 GINO 的 API 文档。

建立连接之后,我们就可以用 GINO 在数据库中创建我们的表了(在同一个 main() 函数里):

await db.gino.create_all()

警告

这里是 db.gino.create_all,而不是 db.create_all,因为 db 继承自 SQLAlchemy 的 MetaData,而 db.create_all 是 SQLAlchemy 的阻塞式方法,无法适用于绑定的 GINO engine。

实际应用中,create_all() 通常不是最理想的解决方案,我们推荐使用诸如 Alembic 这样的工具来管理数据库表结构。

如果您想显式地断开与数据库的连接,您可以这么做:

await db.pop_bind().close()

继续之前,让我们重新看一下前面所有的代码:

import asyncio
from gino import Gino

db = Gino()


class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer(), primary_key=True)
    nickname = db.Column(db.Unicode(), default='noname')


async def main():
    await db.set_bind('postgresql://localhost/gino')
    await db.gino.create_all()

    # further code goes here

    await db.pop_bind().close()


asyncio.get_event_loop().run_until_complete(main())

增删改查

为了操作数据库中的数据,GINO 提供了基本的基于对象的增删改查功能。

让我们从创建一个 User 对象开始:

user = await User.create(nickname='fantix')
# This will cause GINO to execute this SQL with parameter 'fantix':
# INSERT INTO users (nickname) VALUES ($1) RETURNING users.id, users.nickname

正如之前所说,user 对象代表了数据库中新插入的这一行数据。您可以通过 user 对象上的之前定义的列属性来访问每一列的值:

print(f'ID:       {user.id}')           # 1
print(f'Nickname: {user.nickname}')     # fantix

另外,您也可以先在内存中创建一个 user 对象,然后再将其插入到数据库中:

user = User(nickname='fantix')
user.nickname += ' (founder)'
await user.create()

想要通过主键来获取一个 model 对象,您可以使用 model 的类方法 get()。比如,重新获取刚才插入的同一行数据:

user = await User.get(1)
# SQL (parameter: 1):
# SELECT users.id, users.nickname FROM users WHERE users.id = $1

常规的 SQL 查询则是通过类属性 query 来完成。比如,获取数据库中所有的 User 对象的列表:

all_users = await db.all(User.query)
# SQL:
# SELECT users.id, users.nickname FROM users

或者,您也可以使用 querygino 扩展。比如,下面的代码可以实现一样的效果:

all_users = await User.query.gino.all()
# SQL:
# SELECT users.id, users.nickname FROM users

注解

实际上,User.query 是一个普通的 SQLAlchemy 查询对象,SQLAlchemy 的阻塞式执行方法依然存在其上,因此 GINO 向所有 SQLAlchemy 的“Executable”对象注入了一个 gino 扩展,以便在不影响 SQLAlchemy 原有 API 的基础上,让直接异步地执行这些查询对象更容易,而不用每次都通过 engine 或 db 对象来执行。

现在让我们尝试增加一些过滤器。比如,查找出所有 ID 小于 10 的用户:

founding_users = await User.query.where(User.id < 10).gino.all()
# SQL (parameter: 10):
# SELECT users.id, users.nickname FROM users WHERE users.id < $1

Read more here about writing queries, because the query object is exactly from SQLAlchemy core.

警告

当您拿到一个 model 对象时,这个对象就已经彻底与数据库分离了,完全成为内存中的一个普通对象。这就意味着,即使数据库中对应的行发生了变化,对象的值仍然不会受到丝毫影响。类似地,如果您修改了该对象的值,数据库也不会受到任何影响。

并且,GINO 也不会追踪 model 对象,因此重复查询同一行数据将会得到两个独立的、拥有相同值的对象,修改其中一个的值不会幽灵般地影响到另一个的值。

不同于传统 ORM 的 model 对象通常是有状态的,GINO 的 model 对象则更像是用对象封装的 SQL 查询结果,这是 GINO 为了适应异步编程而特意设计的简易性,也是“GINO 不是 ORM”名字的来源。

有时我们仅需要获取一个对象,比如验证登录时,使用用户名来查找一个用户。这时,可以使用这种便捷的写法:

user = await User.query.where(User.nickname == 'fantix').gino.first()
# SQL (parameter: 'fantix'):
# SELECT users.id, users.nickname FROM users WHERE users.nickname = $1

如果数据库中没有叫“fantix”的用户,则 user 会被置为 None

又有时,我们会需要获取一个单独的值,比如 ID 为 1 的用户的名字。此时可以使用 model 的类方法 select()

name = await User.select('nickname').where(User.id == 1).gino.scalar()
# SQL (parameter: 1):
# SELECT users.nickname FROM users WHERE users.id = $1
print(name)  # fantix

又比如,查询用户数量:

population = await db.func.count(User.id).gino.scalar()
# SQL:
# SELECT count(users.id) AS count_1 FROM users
print(population)  # 17 for example

接下来,让我们尝试对数据做一些修改,下面的例子会穿插一些前面用过的查询操作。

# create a new user
user = await User.create(nickname='fantix')

# get its name
name = await User.select('nickname').where(
    User.id == user.id).gino.scalar()
assert name == user.nickname  # they are both 'fantix' before the update

# modification here
await user.update(nickname='daisy').apply()
# SQL (parameters: 'daisy', 1):
# UPDATE users SET nickname=$1 WHERE users.id = $2 RETURNING users.nickname
print(user.nickname)  # daisy

# get its name again
name = await User.select('nickname').where(
    User.id == user.id).gino.scalar()
print(name)  # daisy
assert name == user.nickname  # they are both 'daisy' after the update

这里的 update() 是我们碰到的第一个 model 实例上的 GINO 方法,它接受多个自定义命名参数,参数名对应着 model 的字段名,而参数值则为期望修改成的新的值。连着写的 apply() 则会将这一修改同步到数据库中。

注解

GINO 显式地将“修改内存中对象的值”与“修改数据库中的行”拆分成了两个方法: update()apply()update() 负责修改内存中的值,并且将改动记录在返回的 UpdateRequest 对象中;紧接着调用的 UpdateRequest 对象的 apply() 方法则会将这些记录下的改动通过 SQL 更新到数据库中。

小技巧

UpdateRequest 对象还有一个方法也叫 update(),它与 model 对象上的 update() 方法的功能是一样的,只不过前者还会将新的改动记录与当前 UpdateRequest 已记录的改动合并在一起,并且返回同一个 UpdateRequest 对象。这意味着,您可以连着写多个 update() 调用,最后用一个 apply() 结尾,或者仅仅是通过 UpdateRequest 对象来完成内存对象的多次改动。

Model 对象上的 update() 方法只能操作该对象对应的数据库中的一行数据,而如果您想要批量更新多行数据的话,您可以使用 model 类上的 update() 类方法。用法略有不同:

await User.update.values(nickname='Founding Member ' + User.nickname).where(
    User.id < 10).gino.status()
# SQL (parameter: 'Founding Member ', 10):
# UPDATE users SET nickname=($1 || users.nickname) WHERE users.id < $2

name = await User.select('nickname').where(
    User.id == 1).gino.scalar()
print(name)  # Founding Member fantix

这里不再有 UpdateRequest 了,所有的操作又回到了普通的 SQLAlchemy 用法,更多细节可以参考 SQLAlchemy 的文档

最后,删除一行数据与更新一行数据有些类似,但要简单很多:

user = await User.create(nickname='fantix')
await user.delete()
# SQL (parameter: 1):
# DELETE FROM users WHERE users.id = $1
print(await User.get(user.id))  # None

提示

还记得内存对象的事情吗?在最后一行的 print() 中,尽管数据库中已经没有这一行数据了,但是 user 对象依然在内存中,它的值也都没有变化,所以这里仍然可以用 user.id

或者批量删除(千万不要忘记写 where!是不是整个表都不想要了?):

await User.delete.where(User.id > 10).gino.status()
# SQL (parameter: 10):
# DELETE FROM users WHERE users.id > $1

有了基本的 CRUD,您应该已经可以用 GINO 做出一些不可思议的东西来了。这篇上手指南到此结束,要了解更多请继续阅读文档的剩余部分。祝编程愉快!