SQL Expression Language Tutorial 学习笔记一
http://docs.sqlalchemy.org/en/latest/core/tutorial.html
Google 翻译了一下 SQLAlchemy Expression Language, 翻译不通的就凭感觉改, 关键还是代码.
先定一个小目标, 基础的增删改查过一遍, 把当前项目中的裸 SQL 全部替换为 SQLAlchemy.
1. SQLAlchemy 简介
SQLAlchemy, 目前 Python ORM 当之无愧的第一把交椅. 分为两部分, 第一部分是 SQL Expression Language, 可以认为是 DBAPI, 为不同的数据库提供统一的操作抽象, 第二部分是 ORM, 建立在 SQL Expression Language 的基础之上. 两者都要学, 之前 ORM 用的比较多, 但实现复杂的逻辑, 感觉不如直接写 SQL. SQL Expression Language 提供了这样的灵活性, 有一定的入门门槛, 掌握之后收益多多.
2. 版本检查
In [1]: import sqlalchemy In [2]: sqlalchemy.__version__ Out[2]: '1.1.4'
3. 连接数据库
In [3]: from sqlalchemy import create_engine In [6]: engine = create_engine('sqlite:///:memory:', encoding='utf-8',echo=True) ...:
这算是第一个不太舒服的地方, 我觉得 create_engine 比较陌生. 不如 DBUtils 里面数据库连接池函数 PooledDB 函数名符合直觉.
create_engine 返回一个 Engine 实例, Engine 中内建一个数据库连接池, 它是操作数据库的核心接口, 为不同数据库和 API 提供统一的抽象.
create_engine 的第一个参数是 Database Urls, 以 MySQL 为例, mysql-python 是其默认DBAPI. SQLite 是 Python 自带数据库, 就不介绍了.
# default engine = create_engine('mysql://scott:tiger@localhost/foo') # mysql-python engine = create_engine('mysql+mysqldb://scott:tiger@localhost/foo') # MySQL-connector-python engine = create_engine('mysql+mysqlconnector://scott:tiger@localhost/foo') # OurSQL engine = create_engine('mysql+oursql://scott:tiger@localhost/foo')
4. 定义和创建 Tables
这一步本质是建立 SQLAlchemy 与数据表的映射关系.
SQL Expression Language 大多数情况根据表中的字段构建它的表达式. 一个 Column 对象表示一个字段, 每一个 Column 对象都会跟一个 Table 对象相关联. Table 对象和它子对象的集合称为 database metadata, MetaData 对象可以认为是记录 Table 对象的目录. 通过 table reflection(反射) 还可以对已经存在数据库提取其 Table 对象集合.
这是第二个不舒服的地方, metadata 元数据, 既然是类似 Table 的目录, 为什么不干脆叫 catalog. 其实看英英翻译, metadata 的释义是 a set of data that describes and gives information about other data. 那这就说通了.
In [9]: from sqlalchemy import Table, Column, Integer, String, MetaData, Foreign ...: Key In [10]: metadata = MetaData() In [11]: users = Table('users', metadata, ...: Column('id', Integer, primary_key=True), ...: Column('name', String), ...: Column('fullname', String), ...: ) In [12]: addresses = Table('addresses', metadata, ...: Column('id', Integer, primary_key=True), ...: Column('user_id', Integer, ForeignKey('users.id')), ...: Column('email_address', String, nullable=False) ...: )
这里面有两种关系, 第一个种 Column 和 Table, 第二种是 Table 和 MetaData, 实例化的 MetaData 和 Column 都作为 Table 实例化的参数, 以此相关联.
Table 实例化的第一个参数是数据表名, 第二个参数是 MetaData 对象, 之后的是 Column 对象. 另外, Column 的实例化参数, 基本上继承了 SQL, 符合直觉和习惯, 我最开始不理解的一点也只是 metadata.
先不考虑从已有的数据库加载数据表, 假设要创建数据表. 现在有一个 metadata, 有一个 engine, 那么是谁来创建? metadata 是 Tables 的目录, engine 指向我们当前的数据库, 直觉上应该由谁来发起创建数据表的操作?
In [13]: metadata.create_all(engine) 2017-02-05 20:35:52,499 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
这是第三处不舒服的地方, create_all 的方法在 metadata 上, 而 engine 是该函数的参数. 如果是 engine.create_all(metadata.tables) 会不会更容易接受一些. 或者 metadata.tables.create_all(engine) 也能接受. 关键还是因为 metadata 的中文翻译元数据太抽象导致的. 这些不舒服的细节对于初学者学习阻力很大, 我倾向于想明白缘由, 哪怕没有也编一个能自圆其说的, 情感上能接受了, 就比死记硬背好多了.
最后, 如果使用 MySQL 数据库, Column('email_address', String) 会报错, String 要加长度参数, 比如 String(50).
5. 插入表达式
讲如何构造插入表达式
In [15]: ins = users.insert() In [16]: ins Out[16]: <sqlalchemy.sql.dml.Insert object at 0x110a61ad0> In [17]: str(ins) Out[17]: 'INSERT INTO users (id, name, fullname) VALUES (:id, :name, :fullname)'
user.insert() 从字面上理解是执行了插入动作, 参数为空, 返回值是一个对象, 这个类定义了 __str__ 方法.
注意到, 默认情况下是全字段插入, 通常 id 是个自增字段, 只需要插入 name 和 fullname. 这时候需要 values 方法进行限制.
In [18]: ins = users.insert().values(name='jack', fullname='Jack Jones') In [19]: str(ins) Out[19]: 'INSERT INTO users (name, fullname) VALUES (:name, :fullname)' In [20]: ins.compile().params Out[20]: {'fullname': 'Jack Jones', 'name': 'jack'}
因为返回值是对象, 所以可以链式调用, 注意到 __str__ 方法并没有用参数格式化 SQL, 参数的值是存储在 ins 对象中, 但是仅在 SQL 执行的时候才会进行格式化, 参数的值可以在编译后看到.
虽然 user.insert() 字面上是执行了插入动作, 但是动作并没有执行, 更像是只构造了 SQL, 看来需要一个媒介来承载这个动作.
6. 执行单条语句
先介绍执行的一般方法, 前面讲到 engine 内建一个数据库连接池, 现在要获取一个数据库连接, 就使用 connect 方法.
In [21]: conn = engine.connect() In [22]: conn Out[22]: <sqlalchemy.engine.base.Connection at 0x110a8ba50>
connect 方法返回一个 Connection 对象, Connection 对象有一个 execute 方法, 把 Insert 对象作为参数传递给它, 就可以执行 SQL 语句.
In [23]: result = conn.execute(ins) 2017-02-06 09:38:37,630 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, fullname) VALUES (?, ?) 2017-02-06 09:38:37,631 INFO sqlalchemy.engine.base.Engine ('jack', 'Jack Jones') 2017-02-06 09:38:37,637 INFO sqlalchemy.engine.base.Engine COMMIT
说一下返回值 result, 这是一个 ResultProxy 对象, 就类似于 MySQLdb 里的 Cursor, ResultProxy 对象存储了一些比较重要的信息, 就 insert 而言, 可以看到本次插入生成的 primary_key
In [24]: result.inserted_primary_key
Out[24]: [1]
7. 执行多条语句
使用 value 来构造 insert 的方法其实不常用, 下面给出更普通的方法
In [25]: ins = users.insert() In [26]: conn.execute(ins, id=2, name='wendy', fullname='Wendy Williams') 2017-02-06 16:45:09,498 INFO sqlalchemy.engine.base.Engine INSERT INTO users (id, name, fullname) VALUES (?, ?, ?) 2017-02-06 16:45:09,501 INFO sqlalchemy.engine.base.Engine (2, 'wendy', 'Wendy Williams') 2017-02-06 16:45:09,511 INFO sqlalchemy.engine.base.Engine COMMIT Out[26]: <sqlalchemy.engine.result.ResultProxy at 0x110a8bb90>
批量执行, 实际调用的是 executemany
In [28]: conn.execute(addresses.insert(), [ ...: {'user_id': 1, 'email_address': 'jack@yahoo.com'}, ...: {'user_id': 1, 'email_address': 'jack@msn.com'}, ...: {'user_id': 2, 'email_address': 'www@www.org'}, ...: {'user_id': 2, 'email_address': 'wendy@aol.com'}, ...: ]) 2017-02-06 16:51:19,314 INFO sqlalchemy.engine.base.Engine INSERT INTO addresses (user_id, email_address) VALUES (?, ?) 2017-02-06 16:51:19,314 INFO sqlalchemy.engine.base.Engine ((1, 'jack@yahoo.com'), (1, 'jack@msn.com'), (2, 'www@www.org'), (2, 'wendy@aol.com')) 2017-02-06 16:51:19,316 INFO sqlalchemy.engine.base.Engine COMMIT Out[28]: <sqlalchemy.engine.result.ResultProxy at 0x110b6e410>
有两点需要注意, 第一, 插入的字典列表, keys 要保持一致, 因为 SQLAlchemy 默认使用第一个字典作为模板; 第二, insert(), update(), delete() 都支持批量执行.
8. Selecting
In [29]: from sqlalchemy.sql import select In [30]: s = select([users]) In [31]: result = conn.execute(s) 2017-02-06 17:04:32,831 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.fullname FROM users 2017-02-06 17:04:32,832 INFO sqlalchemy.engine.base.Engine ()
从 sqlalchemy.sql import select, 这一点与 insert 不同. 并且注意到 select 的参数是一个列表.
返回值 result 仍然是个 ResultProxy, 并且同样具有 fetchone() 和 fetchoneall() 方法. 不过, result 是可遍历的.
In [32]: for row in result: ...: print(row) ...: (1, u'jack', u'Jack Jones') (2, u'wendy', u'Wendy Williams') In [33]: for row in result: ...: print(row) ...:
值得注意的是, result 与 cursor 类似, 第二次遍历, 发现结果为空.
在使用 MySQLdb 的时候, 通常会在 cursorclass 做一个选择, 是 MySQLdb.cursors.DictCursor 还是默认的 MySQLdb.cursors.BaseCursor, 前者返回的是字典元组, 后者返回的是元组元组. 现在, 使用 SQLAlchemy, 可以同时支持两种方式.
In [34]: result = conn.execute(s) 2017-02-06 17:47:04,263 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.fullname FROM users 2017-02-06 17:47:04,265 INFO sqlalchemy.engine.base.Engine () In [35]: row = result.fetchone() In [36]: print("name:", row['name'], "; fullname:", row['fullname']) ('name:', u'jack', '; fullname:', u'Jack Jones') In [37]: row = result.fetchone() In [38]: print("name:", row[1], "; fullname", row[2]) ('name:', u'wendy', '; fullname', u'Wendy Williams')
row 的 key, 除了显示的字符串, 其实可以直接使用 Column 对象
In [40]: for row in conn.execute(s): ...: print("name:", row[users.c.name], "; fullname:", row[users.c.fullna ...: me]) ...: 2017-02-06 17:53:59,481 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.fullname FROM users 2017-02-06 17:53:59,482 INFO sqlalchemy.engine.base.Engine () ('name:', u'jack', '; fullname:', u'Jack Jones') ('name:', u'wendy', '; fullname:', u'Wendy Williams')
有个好的习惯是, result 对象用完随手关闭, 甚至可以说必要, 因为对于一些后端数据库, 对此很挑剔.
In [41]: result.close()
如果要指定 select 的字段, 可以使用 Column 对象
In [42]: s = select([users.c.name, users.c.fullname]) In [43]: result = conn.execute(s) 2017-02-06 19:57:08,927 INFO sqlalchemy.engine.base.Engine SELECT users.name, users.fullname FROM users 2017-02-06 19:57:08,928 INFO sqlalchemy.engine.base.Engine () In [44]: for row in result: ...: print(row) ...: (u'jack', u'Jack Jones') (u'wendy', u'Wendy Williams') In [45]: result.close()
现在有一个问题, 如果把不同的 Table 传给 select 会怎样?
In [46]: for row in conn.execute(select([users, addresses])): ...: print(row) ...: 2017-02-06 20:15:27,877 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.fullname, addresses.id, addresses.user_id, addresses.email_address FROM users, addresses 2017-02-06 20:15:27,879 INFO sqlalchemy.engine.base.Engine () (1, u'jack', u'Jack Jones', 1, 1, u'jack@yahoo.com') (1, u'jack', u'Jack Jones', 2, 1, u'jack@msn.com') (1, u'jack', u'Jack Jones', 3, 2, u'www@www.org') (1, u'jack', u'Jack Jones', 4, 2, u'wendy@aol.com') (2, u'wendy', u'Wendy Williams', 1, 1, u'jack@yahoo.com') (2, u'wendy', u'Wendy Williams', 2, 1, u'jack@msn.com') (2, u'wendy', u'Wendy Williams', 3, 2, u'www@www.org') (2, u'wendy', u'Wendy Williams', 4, 2, u'wendy@aol.com')
totally a mess. 输出了两张表的笛卡尔积. 我们需要一个 where 条件, Select.where 提供了这个方法.
In [16]: s = select([users, addresses]).where(users.c.id == addresses.c.user_id) ...: In [17]: for row in conn.execute(s): ...: print(row) ...: 2017-02-06 20:40:42,646 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.fullname, addresses.id, addresses.user_id, addresses.email_address FROM users, addresses WHERE users.id = addresses.user_id 2017-02-06 20:40:42,646 INFO sqlalchemy.engine.base.Engine () (1, u'jack', u'Jack Jones', 1, 1, u'jack@yahoo.com') (1, u'jack', u'Jack Jones', 2, 1, u'jack@msn.com') (2, u'wendy', u'Wendy Williams', 3, 2, u'www@www.org') (2, u'wendy', u'Wendy Williams', 4, 2, u'wendy@aol.com')
这里符合直觉, 不过需要注意的是 users.c.id == addresses.c.user_id, 这实际上是一个类 BinaryExpression.
In [18]: users.c.id == addresses.c.user_id
Out[18]: <sqlalchemy.sql.elements.BinaryExpression object at 0x10aae2110>
In [19]: str(users.c.id == addresses.c.user_id)
Out[19]: 'users.id = addresses.user_id'
9. Operators
如果 users.c.id == addresses.c.user_id 是一个类, 那么 >, <, >=, <=, + 这些是不是也是同一个类? 答案是的.
In [21]: print(users.c.id == 7) users.id = :id_1 In [22]: (users.c.id == 7).compile().params Out[22]: {u'id_1': 7} In [23]: print(users.c.id != 7) users.id != :id_1 In [24]: print(users.c.id == None) users.id IS NULL In [25]: print('fred' > users.c.name) users.name < :name_1
In [27]: print(users.c.id + addresses.c.id)
users.id + addresses.id
In [28]: print(users.c.name + users.c.fullname)
users.name || users.fullname
注意到, BinaryExpression 这个类也有 compile 方法, 其实与 Insert 类似, 他们都继承于 ColumnElement 类.
另外, + 如果两边都是数字就是相加, 否则就是字符串拼接, || 不是或, 搁 mysql 里面, 是 concat.
不排除有实在找不到对应的 operator 的情况, 这时候可以使用 ColumnOperators.op() 方法
In [30]: print(users.c.name.op('=')('foo')) users.name = :name_1 In [31]: print(users.c.name.op('==')('foo')) users.name == :name_1 In [32]: print(users.c.name.op('tiddlywinks')('foo')) users.name tiddlywinks :name_1
10. Conjunctions
在 sql 里面, where 后经常跟着 and, or, not, like, 在 SQLAlchemy 中, 它们变成了 and_, or_, not_, ColumnOperators.like()
In [35]: print(and_( ...: users.c.name.like('j%'), ...: users.c.id == addresses.c.user_id, ...: or_( ...: addresses.c.email_address == 'wendy@aol.com', ...: addresses.c.email_address == 'jack@yahoo.com' ...: ), ...: not_(users.c.id > 5) ...: )) users.name LIKE :name_1 AND users.id = addresses.user_id AND (addresses.email_address = :email_address_1 OR addresses.email_address = :email_address_2) AND users.id <= :id_1
所有 email 地址在 AOL 或 MSN, 名字首字母在 m 和 z 之间, 将他们的 fullname 和 email 地址相加, 以','相分隔, 并将该字段重命名为 title. 这里要用到两个方法, ColumnOperators.between() 和 ColumnOperators.label()
In [44]: s = select([(users.c.fullname + ',' + addresses.c.email_address).label( ...: 'title')]).where( ...: and_( ...: users.c.id == addresses.c.user_id, ...: users.c.name.between('m', 'z'), ...: or_( ...: addresses.c.email_address.like('%@aol.com'), ...: addresses.c.email_address.like('%@msn.com') ...: ) ...: ) ...: ) In [45]: conn.execute(s).fetchall() 2017-02-06 21:33:29,564 INFO sqlalchemy.engine.base.Engine SELECT users.fullname || ? || addresses.email_address AS title FROM users, addresses WHERE users.id = addresses.user_id AND users.name BETWEEN ? AND ? AND (addresses.email_address LIKE ? OR addresses.email_address LIKE ?) 2017-02-06 21:33:29,564 INFO sqlalchemy.engine.base.Engine (',', 'm', 'z', '%@aol.com', '%@msn.com') Out[45]: [(u'Wendy Williams,wendy@aol.com',)]
and_ 的替代是使用 where 的链式调用.
In [42]: s = select([(users.c.fullname + ',' + addresses.c.email_address).label( ...: 'title')]).\ ...: where(users.c.id == addresses.c.user_id).\ ...: where(users.c.name.between('m', 'z')).\ ...: where( ...: or_( ...: addresses.c.email_address.like('%@aol.com'), ...: addresses.c.email_address.like('%@msn.com') ...: ) ...: ) In [43]: conn.execute(s).fetchall() 2017-02-06 21:30:48,623 INFO sqlalchemy.engine.base.Engine SELECT users.fullname || ? || addresses.email_address AS title FROM users, addresses WHERE users.id = addresses.user_id AND users.name BETWEEN ? AND ? AND (addresses.email_address LIKE ? OR addresses.email_address LIKE ?) 2017-02-06 21:30:48,624 INFO sqlalchemy.engine.base.Engine (',', 'm', 'z', '%@aol.com', '%@msn.com') Out[43]: [(u'Wendy Williams,wendy@aol.com',)]