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',)]

SQL Expression Language Tutorial 学习笔记一的更多相关文章

  1. SQL Expression Language Tutorial 学习笔记二

    11. Using Textual SQL 直接使用 SQL 如果实在玩不转, 还是可以通过 test() 直接写 SQL. In [51]: s = text( ...: "SELECT ...

  2. 《SQL 反模式》 学习笔记

    第一章 引言 GoF 所著的的<设计模式>,在软件领域引入了"设计模式"(design pattern)的概念. 而后,Andrew Koenig 在 1995 年造了 ...

  3. Django 1.7 Tutorial 学习笔记

    官方教程在这里 : Here 写在前面的废话:)) 以前学习新东西,第一想到的是找本入门教程,按照书上做一遍.现在看了各种网上的入门教程后,我觉得还是看官方Tutorial靠谱.书的弊端一说一大推 本 ...

  4. 深度学习 Deep Learning UFLDL 最新 Tutorial 学习笔记 1:Linear Regression

    1 前言 Andrew Ng的UFLDL在2014年9月底更新了. 对于開始研究Deep Learning的童鞋们来说这真的是极大的好消息! 新的Tutorial相比旧的Tutorial添加了Conv ...

  5. sql优化详细介绍学习笔记

    因为最近在面试,发现sql优化这个方面问的特别特别的多.之前都是零零星星,不够全面的了解一点,刚刚在网上查了一下,从 http://blog.csdn.net/zhushuai1221/article ...

  6. sql server 2016 JSON 学习笔记

    虽然现在win服务器已经几乎不用了,但是网上看到2016开始原生支持json 还是想试试 建立一个表  id int , json varchar(2000) json字段中输入数据 {"r ...

  7. 《SQL基础教程》+ 《SQL进阶教程》 学习笔记

    写在前面:本文主要注重 SQL 的理论.主流覆盖的功能范围及其基本语法/用法.至于详细的 SQL 语法/用法,因为每家 DBMS 都有些许不同,我会在以后专门介绍某款DBMS(例如 PostgreSQ ...

  8. Programming Language A 学习笔记(二)

    1. 语法糖--元组的"名称引用"与"位置引用": (e1,...,en) <=> {1=e1,...,n=en} 类型:t1 * - * tn & ...

  9. sql server自定义函数学习笔记

    sql server中函数分别有:表值函数.标量函数.聚合函数.系统函数.这些函数中除系统函数外其他函数都需要用户进行自定义. 一.表值函数 简单表值函数 创建 create function fu_ ...

随机推荐

  1. String 类实现 以及>> <<流插入/流提取运算符重载

    简单版的String类,旨在说明>> <<重载 #include <iostream> //#include <cstring>//包含char*的字符 ...

  2. Javascript常见性能优化

    俗话说,时间就是生命,时间就是金钱,时间就是一切,人人都不想把时间白白浪费,一个网站,最重要的就是体验,而网站好不好最直观的感受就是这个网站打开速度快不快,卡不卡. 当打开一个购物网站卡出翔,慢的要死 ...

  3. Android 手机震动

    1.添加震动权限 <uses-permission android:name="android.permission.VIBRATE"/> 2.获取震动服务 Vibra ...

  4. Linux系统启动内幕

    经过对Linux系统有了一定了解和熟悉后,想对其更深层次的东西做进一步探究.这当中就包括系统的启动流程.文件系统的组成结构.基于动态库和静态库的程序在执行时的异同.协议栈的架构和原理.驱动程序的机制等 ...

  5. 生产环境的gitlab大版本升级思路(从7.x升级到8.x)

    之前在生产环境部署的gitlab是7.x版本的,提供给公司内部的员工来使用,大概有350个用户左右,gitlab从8.x版本之后内置了CI和CD的集成,所以就考虑到升级版本的问题 通过参考和总结git ...

  6. 盘古分词修改支持mono和lucene.net3.03

    盘古分词平台兼容性 在使用Lucece.net,需要一个中文的分词组件,比较好的是盘古分词,但是我希望能够在mono的环境下运行,就使用moma检查了一下盘古分词 Assembly Version M ...

  7. vmware新建Ubuntu时,提示此主机不支持 Intel VT-x

    有两种解决方式 一.BIOS中打开CPU虚拟选项,不同厂商主板配置不同: 以下以个人thinkpad T460P电脑为例: 1.关机,开机,在启动时,按F1今天 BIOS 设置页面: 2.选择 Sec ...

  8. HUSTM 1601 - Shepherd

    题目描述 Hehe keeps a flock of sheep, numbered from 1 to n and each with a weight wi. To keep the sheep ...

  9. OpenCV学习笔记之课后习题练习3-4

    练习:创建一个大小为100*100的三通道RGB图像.将它的元素全部置0.使用指针算法以(20,5)与(40,20)为顶点绘制一个绿色平面. 参考博文:blog.csdn.net/qq_2077736 ...

  10. NEFU 84 - 五指山 - [exgcd求解一元线性同余方程]

    题目链接:http://acm.nefu.edu.cn/JudgeOnline/problemShow.php?problem_id=84 Time Limit:1000ms Memory Limit ...