前言
本文分为两个部分,sqlalchemy的非ORM部分和ORM部分。一般使用是推荐使用ORM,这将让你的代码更python,更易读易懂。但ORM部分要用好,很多非ORM部分知识是需要的,下面根据笔者的实践列出一些非ORM部分非常常用的一些知识部分:
- 创建Engine 理解创建Engine部分将有助于你在很多地方理解如何配置sqlalchemy的数据库连接
- 列的属性 列的数据类型 这两个是必看的,你在定义ORM模型的时候必然要参考这个
- select语句和理解RowProxy对象 帮助你理解如何在query中编写select语句和出来的结果如何使用
安装
sqlalchemy的安装简单用pip命令安装之即可:
pip install sqlalchemy
引用惯例
在后面都默认有如下引用:
from sqlalchemy import *
后面将不会再提及,也就是凡是 from sqlalchemy import what
的所有语句也都归于如上一条引用。实践中并不推荐这种的全局引用。
简介
通过sqlalchemy连接具体的某个数据库,前面有一些准备工作要做,参考 sqlalchemy architecture 一文的描述:
和数据库直接相连的是我们熟悉的那些DBAPI接口模块,比如: sqlite3, pymysql, psycopg2等,然后中间的核心层有Engine,连接池,方言,SQL表达语言和类型系统。core层很重要,实际上有些模块是完全建构在core层之上的,不一定要用ORM方法。
非ORM风格
创建Engine
创建一个 Engine
对象实际上对应的就是和数据库的连接操作。具体是通过 create_engine
函数创建的Engine对象,其一开始并没有实际连接数据库,只有具体要求某个操作的时候才会去连接。
连接sqlite3
连接sqlite3 in-memory
engine = create_engine('sqlite://')
但是推荐采用如下写法:
engine = create_engine('sqlite:///:memory:')
这样后面谈及的sqlalchmy_utils的 database_exists
函数也能正常工作。
连接sqlite3 on-disk
engine = create_engine('sqlite:///sqlite3_learning_example.db')
上面的db文件是创建在命令行当前工作目录下的,也就是相对路径表达。此外还可以如下写上绝对路径表达:
engine = create_engine('sqlite:////absolute/path/to/foo.db')
在三个斜杠线的基础上还需要加上一个斜杠线。作为这种形式的通用表达,前面必定有两个斜杠线,然后第二个斜杠线和第三个斜杠线之间是登录信息的描述,因为sqlite3没有这些信息,所以空了,如下所示:
dialect://username:password@host:port/database
或者某个方言系统再加上某个驱动:
dialect+driver://username:password@host:port/database
这里的方言可以有:
- sqlite: 默认的driver的官方的 sqlite3 模块,这个应该不需要改动。
-
mysql: 默认的dirver是 mysql-python ,但推荐使用 pymysql ,你需要用pip安装之。
engine = create_engine('mysql+pymysql://root@localhost/test')
-
postgresql: 默认的driver是 psycopg2 ,这个还行。
- oracle: 默认的driver是 cx_oracle 。
- mssql: 默认的driver是 pyodbc 。
连接mysql
engine = create_engine('mysql+pymysql://localhost/mysql_db')
确保你安装了pymysql:
pip install pymysql
连接postgresql
engine = create_engine('postgres://rick:foo@localhost:5432/pg_db')
MetaData对象
MetaData对象你可以看作比Table层更高一级的抽象,里面存放着Table对象的一些metadata描述信息。一个简单的理解是将一个MetaData对象看作sqlalchemy内部的database概念。
创建一个unbound MetaData对象
通过 MetaData()
默认创建的就是一个unbound MetaData对象。
metadata = MetaData()
bind一个Engine对象
你可以如下将一个unbound MetaData对象具体 bind
一个Engine对象。
engine = create_engine('sqlite://')
metadata = MetaData()
metadata.bind = engine
或者在上面创建的时候就指定:
engine = create_engine('sqlite://')
metadata = MetaData(engine)
或者你还可以直接engine的URL表达来后台自动创建一个engine,于是有:
metadata = MetaData('sqlite://')
对于初学者用的最多的还是 BoundMetaData
,通过上面谈及的方法创建了一个 BoundMetaData
对象之后,某个Table对象关联了该 BoundMetaData
对象,然后该Table对象就可以直接通过:
table.create()
来创建自身了。
测试数据库是否存在
这里关于 sqlalchemy_utils
的想法来自 这个网页 。
from sqlalchemy import *
from sqlalchemy_utils import database_exists, create_database
def init_sqlalchemy(dburl,echo=True):
engine = create_engine(dburl,echo=echo)
if not database_exists(engine.url):###确保目标数据库是存在的。
create_database(engine.url)
metadata = MetaData(bind = engine)
return metadata
metadata = init_sqlalchemy('sqlite:///test.db')
这里的 sqlalchemy_utils
需要额外安装,在这里主要是利用其 database_exists
函数来检测某个数据库是否存在,然后如果不存在的话则用 create_database
函数创建之。
上面的 init_sqlalchemy
函数最重要的一个参数就是那个 dburl
,具体其细节前面已有所叙述,正是照它来创建的Engine,并基于这个Engine对象来创建的MetaData对象,一般将这个MetaData对象bind之前的那个engine,然后返回该metadata即可,后面主要需要使用这个metadata。
然后后面实际操作就以创建一个Table对象开始了,其他database的操作,建议如同上面处理的一样,都提到顶层用sqlalchemy_utils模块来处理之。类似的还有 drop_database : 删除database,参数如create_database也是某个Engine对象的url。
创建一个Table对象
下面是一个完整的例子,最后创建了一个Table表格。这里的 db
也就是前面谈及的MetaData对象,我们看到在创建Table对象的时候第一个参数是具体创建的SQL表格的名字,第二个就是该表格bind的某个MetaData对象,也可以简单理解为该表格对象存入该MetaData对象代表的database中。然后后面调用 db.create_all()
,所有这些bind到该db上的表格都将创建。你还可以用 users.create()
来单独创建某个表格。
from sqlalchemy import *
from sqlalchemy_utils import database_exists, create_database
def init_sqlalchemy(dburl,echo=True):
engine = create_engine(dburl,echo=echo)
if not database_exists(engine.url):###确保目标数据库是存在的。
create_database(engine.url)
metadata = MetaData(bind = engine)
return metadata
db = init_sqlalchemy('sqlite:///test.db')
users = Table('users', db,
Column('user_id', Integer, primary_key=True),
Column('name', String(40)),
Column('age', Integer),
Column('password', String),
)
db.create_all()
利用已存在的Table
如果某个数据库的某个Table已经存在了,那么你就没有必要如上去创建一个Table对象了,只需要如下做就可以获得该Table对象了:
users = Table('users',db,autoload=True)
具体就是将 autoload
设置为True即可。这里的db就是所谓的metadata,然后这里必须是bind了的metadata对象,若还未bind,则还需要加上autoload_with参数。
实际在数据库中创建表格
具体可以整个metadata对象,调用 create_all
方法来创建所有表格(其也有 checkfirst
参数。):
db.create_all()
或者该Table对象具体调用 create
方法来自我创建之。在应用推荐加上 checkfirst=True
设置,这样就算数据库中该表格已经存在也不会报错。如下所示:
users.create(checkfirst=True)
类似的还有如下用法用于安装删除某个表格,即使该表格不存在也不会报错:
users.drop(checkfirst=True)
这里代码改成了这个样子了:
try:
users = Table('users',db,autoload=True)
except sqlalchemy.exc.NoSuchTableError:
users = Table('users', db,
Column('user_id', Integer, primary_key=True),
Column('name', String(40)),
Column('age', Integer),
Column('password', String),
)
注意这个 NoSuchTableError
,如果通过 autoload=True
来获取该Table对象而其在数据库中并不存在,则将抛出这个异常。
列的属性设置
创建表格对象后面一系列的参数就是具体各个列Column对象,其第一个参数是具体列的名字,然后第二个参数该列所存储的值的类型,后面还可以跟其他一些可选项作为属性的进一步修饰。具体如下所示:
- primary_key: 设置该列为主键列或者称之为主键约束
- unique: 该列加上唯一约束,即该列的值不可重复。主键约束是一种特殊的唯一约束。
- nullable: 该列可不可为空
- default: 该列的默认值设置
- index: 该列是否加入索引
- auto_increment: Integer的列数值自动递增
- ForeignKey('brand.id'): 设置外键约束
- CheckConstraint('amount > 0'): 设置Check约束
-
onupdate: 最常见的用法如下:
onupdate=datetime.utcnow
应该意思是若update了则调用某个callable对象吧。
列的数据类型声明
下面对各个列存储的值的可能类型描述详细介绍之,更多信息请参看文档查看之。
Class name | Python Type | SQL Type (for SQLitedriver) | Arguments |
---|---|---|---|
String | string | TEXT or VARCHAR | length |
Integer | int | INTEGER | none |
SmallInteger | int | SMALLINT | none |
Numeric | float,Decimal | NUMERIC | precision=10, length=2 |
Float | float | NUMERIC | precision=10 |
DateTime | datetime.datetime | TIMESTAMP | none |
time | datetime.time | TIME | none |
Date | datetime.date | DATE | none |
Binary | byte string | BLOB | length |
Boolean | bool | BOOLEAN | none |
Unicode | unicode | TEXT or VARCHAR | length |
大致就这些,然后sqlalchemy还有一些类名大致和上面的某个等同,只是多了一个使用上的名字。
- FLOAT: 等同于 Numeric
- TEXT: 等同于 String
- DECIMAL: 等同于 Numeric
- INT: 等同于 Integer
- INTEGER: 等同于 Integer
- TIMESTAMP: 等同于 DateTime
- DATETIME: 等同于 DateTime
- CLOB: 等同于 String
- VARCHAR: 等同于 String
- CHAR: 等同于 String
- NCHAR: 等同于 Unicode
- BLOB: 等同于 Binary
- BOOLEAN: 等同于 Boolean
mysql方言的额外类型
Class name | Python type | SQL type | Arguments |
---|---|---|---|
MSEnum | string | ENUM | values |
MSTinyInteger | int | TINYINT | length |
MSBigInteger | int | BIGINT | length |
MSDouble | float | DOUBLE | length=10,precision=2 |
MSTinyText | string | TINYTEXT | none |
MSMediumText | string | MEDIUMTEXT | none |
MSLongText | string | LONGTEXT | none |
MSNVarChar | unicode | NATIONAL VARCHAR | length |
MSTinyBlob | byte string | TINYBLOB | none |
MSMediumBlob | byte string | MEDIUMBLOB | none |
MSLongBlob | byte string | LONGBLOB | none |
MSBinary | byte string | BINARY | length |
MSVarBinary | byte string | VARBINARY | length |
MSSet | set | SET | set values |
MSYear | int | YEAR | length |
MSBit | long | BIT | length |
postgresql额外的类型
Class name | Python type | SQL type | Arguments |
---|---|---|---|
PGArray | any TypeEngine | type engine[] | TypeEngine |
PGBigInteger | int,long | BIGINT | none |
PGInet | none | INET | none |
PGInterval | none | INTERVAL | none |
Oracle额外的类型
Class name | Python type | SQL type | Arguments |
---|---|---|---|
Oracle | byte string | RAW | length |
JSON支持
这个现在特别值得提一下,现在主流数据库已经都开始支持JSON数据了。sqlalchemy有JSON这个字段类型了,然后postgresql很早就支持了,mysql版本 > 5.7.5 也是支持的了,现在估计大多已经超过这个版本了。
很是好用,强烈推荐读者去了解下:
json_data_field = Column(JSON)
insert语句
上面谈及的Table对象调用 insert
方法即可产生一个临时表达语句对象(大概类似的东西,这个词是我杜撰的。),比如在执行 i = users.insert()
之后:
>>> type(i)
<class 'sqlalchemy.sql.dml.Insert'>
>>> str(i)
'INSERT INTO users (user_id, name, age, password) VALUES (?, ?, ?, ?)'
>>>
这个i临时表达语句对象有 execute
方法,其可以接受一些参数,比如如下所示:
i = users.insert()
i.execute(name='Mary', age=30, password='secret')
这个语句执行之后,该数据就被插入进数据库了。你还可以用execute方法来插入多个值,如下所示:
i.execute({'name': 'John', 'age': 42},
{'name': 'Susan', 'age': 57},
{'name': 'Carl', 'age': 33})
然后如果我们需要使用 insert ignore
这样的语句,则需要这样处理:
i = users.insert().prefix_with('or ignore')
'INSERT or ignore INTO users (user_id, name, age, password) VALUES (?, ?, ?, ?)'
>>>
上面的例子是sqlite的情况,mysql那边则需要写成 .prefix_with('ignore')
这样的形式。
然后额外值得一提的是: 要真正做到重复刷,primary_key,也就是这里的 user_id
需要具体指定为多少,因为这里的ignore的逻辑就是基于主键列不重复的。
delete语句
delete语句的使用也类似上面insert语句所谈及的,除了根据SQL delete语句的实际情况,其为第一个可选参数为where过滤字句,如下所示:
d = users.delete(users.c.password == None)
>>> str(d)
'DELETE FROM users WHERE users.password IS NULL'
我们注意到上面 users.c.password
的用法,这里的细节后面再讨论,大体意思就是users这个表格的password这一列其值等于None(对应NULL),然后python中的 is None
这种写法试了一下并不行。
上面的delete语句是将users表格中password为空的行都删除,然后如果在构建delete语句时,不填任何where语句,则是表格所有记录都将被删除。
d = users.delete()
update语句
然后是update语句,下面来更新那几个user的password。 update
语句的参数设置如下:
update(whereclause=None, values=None, inline=False, **kwargs)
我们可以看到其第一个可选参数和delete一样是 whereclause
where过滤字句,然后第二个values要跟一个字典值,用来表示具体设置的某些值。下面演示逐步构建update语句的风格,这种风格同样适用于 insert
, update
, select
语句的构建。
u1 = users.update()
u2 = u1.where(users.c.name == 'John').values(password='123456')
>>> str(u1)
'UPDATE users SET user_id=?, name=?, age=?, password=?'
>>> str(u2)
'UPDATE users SET password=? WHERE users.name = ?'
这里str显示参数并没有给打进去,我们 u2.execute()
执行的话就会看到实际效果了。
select语句
select语句和前面谈论的 insert
等语句的构建过程类似,只是因为SQL中select语句情况较为复杂,然后select语句还需要考虑具体查询的返回值问题,所以东西很多。
首先我们看下面这个函数:
def show_squery(squery):
res = squery.execute()
for r in res:
print(r)
select语句执行之后的返回结果叫做什么 ResultProxy
对象,其可以直接用for语句来迭代。不带任何参数的select语句返回Table的所有行:
>>> show_squery(users.select())
(1, 'Mary', 30, 'secret')
(2, 'John', 42, '123456')
(3, 'Susan', 57, None)
(4, 'Carl', 33, None)
或者:
>>> show_squery(users.select(users.c.name=='John'))
(2, 'John', 42, '123456')
这是 and_
或 &
的用法:
>>> show_squery(users.select(and_(users.c.age < 40 , users.c.name != 'Mary')))
(4, 'Carl', 33, None)
>>> show_squery(users.select((users.c.age < 40) & (users.c.name != 'Mary')))
(4, 'Carl', 33, None)
类似的还有 or_
或 |
做逻辑或的意思 ; 或者 not_
或 "~" 做逻辑非的意思。
此外还有 startswith
, like
, endswith
users.select(users.c.name.startswith('M'))
还有 between
, in_
:
users.select(users.c.age.between(30,39))
users.select(users.c.name.in_('Mary', 'Susan'))
ResultProxy对象
select语句执行后返回的ResultProxy对象除了可以直接迭代外还有如下这些方法。
- fetchone: 取一行,具体是所谓的
RowProxy
对象,其可用api后面会描述之。 - fetchmany: 取多行,具体返回的是一个列表,其内装着
RowProxy
对象。 - fetchall: 取所有行,如果fetchmany不指定size则等同于取所有行,返回的是一个列表,其内装着
RowProxy
对象。 -
scalar:
-
keys:
-
rowcount:
-
close:
RowProxy对象
对ResultProxy对象进行迭代,或者fetchone,fetchmany,fetchall方法,就可以获得RowProxy对象,其对应的就是数据库的一行记录。该对象api操作很是灵活,具体你可以像操作一个字典来操作它,也可以类似操作namedtuple般的来操作它,还可以如同列表一般用这样 [0]
的索引方法提取某一列,如下所示:
s = users.select()
rs = s.execute()
row = rs.fetchone()
>>> row[0]
1
>>> row.name
'Mary'
>>> row['password']
'secret'
多表连接
现在代码情况如下所示:
import sqlalchemy
from sqlalchemy import *
from sqlalchemy_utils import database_exists, create_database
def init_sqlalchemy(dburl,echo=True):
engine = create_engine(dburl,echo=echo)
if not database_exists(engine.url):###确保目标数据库是存在的。
create_database(engine.url)
metadata = MetaData(bind = engine)
return engine,metadata
engine,db = init_sqlalchemy('sqlite:///test.db')
try:
users = Table('users',db,autoload=True)
except sqlalchemy.exc.NoSuchTableError:
users = Table('users', db,
Column('id', Integer, primary_key=True),
Column('name', String(40)),
Column('age', Integer),
Column('password', String),
)
users.create(checkfirst=True)
insert_query = users.insert().prefix_with('or ignore')
insert_query.execute(id=1,name='Mary', age=30, password='secret')
insert_query.execute({'id':2,'name': 'John', 'age': 42},
{'id':3,'name': 'Susan', 'age': 57},
{'id':4,'name': 'Carl', 'age': 33})
delete_query = users.delete()
update_query = users.update()
update_query = update_query.where(users.c.name == 'John').values(password='123456')
update_query.execute()
def run(query):
query.execute()
def show_squery(squery):
res = squery.execute()
for r in res:
print(r)
try:
emails = Table('emails',db,autoload=True)
except sqlalchemy.exc.NoSuchTableError:
emails = Table('emails', db,
Column('id', Integer, primary_key=True),
Column('address', String),
Column('user_id', Integer,ForeignKey('users.id')),
)
emails.create(checkfirst=True)
insert_query = emails.insert().prefix_with('or ignore')
insert_query.execute(
{'address': 'mary@example.com', 'user_id': 1},
{'address': 'john@nowhere.net', 'user_id': 2},
{'address': 'john@example.org', 'user_id': 2},
{'address': 'carl@nospam.net', 'user_id': 4},
)
交叉连接或笛卡尔积
下面是交叉连接的情况:
>>> show_squery(select([users,emails]))
2015-10-28 20:27:21,721 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.age, users.password, emails.id, emails.address, emails.user_id
FROM users, emails
2015-10-28 20:27:21,721 INFO sqlalchemy.engine.base.Engine ()
(1, 'Mary', 30, 'secret', 1, 'mary@example.com', 1)
(1, 'Mary', 30, 'secret', 2, 'john@nowhere.net', 2)
(1, 'Mary', 30, 'secret', 3, 'john@example.org', 2)
(1, 'Mary', 30, 'secret', 4, 'carl@nospam.net', 4)
(2, 'John', 42, '123456', 1, 'mary@example.com', 1)
(2, 'John', 42, '123456', 2, 'john@nowhere.net', 2)
(2, 'John', 42, '123456', 3, 'john@example.org', 2)
(2, 'John', 42, '123456', 4, 'carl@nospam.net', 4)
(3, 'Susan', 57, None, 1, 'mary@example.com', 1)
(3, 'Susan', 57, None, 2, 'john@nowhere.net', 2)
(3, 'Susan', 57, None, 3, 'john@example.org', 2)
(3, 'Susan', 57, None, 4, 'carl@nospam.net', 4)
(4, 'Carl', 33, None, 1, 'mary@example.com', 1)
(4, 'Carl', 33, None, 2, 'john@nowhere.net', 2)
(4, 'Carl', 33, None, 3, 'john@example.org', 2)
(4, 'Carl', 33, None, 4, 'carl@nospam.net', 4)
内连接
下面是内连接的情况:
>>> show_squery(select([users,emails],users.c.id == emails.c.user_id))
2015-10-28 20:39:09,173 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.age, users.password, emails.id, emails.address, emails.user_id
FROM users, emails
WHERE users.id = emails.user_id
2015-10-28 20:39:09,173 INFO sqlalchemy.engine.base.Engine ()
(1, 'Mary', 30, 'secret', 1, 'mary@example.com', 1)
(2, 'John', 42, '123456', 2, 'john@nowhere.net', 2)
(2, 'John', 42, '123456', 3, 'john@example.org', 2)
(4, 'Carl', 33, None, 4, 'carl@nospam.net', 4)
sqlalchemy还有一种更智能的内连接用法:
>>> show_squery(join(users, emails).select())
2015-10-28 20:40:17,502 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.age, users.password, emails.id, emails.address, emails.user_id
FROM users JOIN emails ON users.id = emails.user_id
2015-10-28 20:40:17,502 INFO sqlalchemy.engine.base.Engine ()
(1, 'Mary', 30, 'secret', 1, 'mary@example.com', 1)
(2, 'John', 42, '123456', 2, 'john@nowhere.net', 2)
(2, 'John', 42, '123456', 3, 'john@example.org', 2)
(4, 'Carl', 33, None, 4, 'carl@nospam.net', 4)
外连接
外连接如下所示,和写入顺序有关。具体是第一个连接第二个,满足过滤条件的则data收进来,没有的则用NULL填充。
>>> show_squery(outerjoin(users, emails).select())
2015-10-28 20:41:16,610 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.name, users.age, users.password, emails.id, emails.address, emails.user_id
FROM users LEFT OUTER JOIN emails ON users.id = emails.user_id
2015-10-28 20:41:16,610 INFO sqlalchemy.engine.base.Engine ()
(1, 'Mary', 30, 'secret', 1, 'mary@example.com', 1)
(2, 'John', 42, '123456', 3, 'john@example.org', 2)
(2, 'John', 42, '123456', 2, 'john@nowhere.net', 2)
(3, 'Susan', 57, None, None, None, None)
(4, 'Carl', 33, None, 4, 'carl@nospam.net', 4)
>>> show_squery(outerjoin(emails, users).select())
2015-10-28 20:43:56,590 INFO sqlalchemy.engine.base.Engine SELECT emails.id, emails.address, emails.user_id, users.id, users.name, users.age, users.password
FROM emails LEFT OUTER JOIN users ON users.id = emails.user_id
2015-10-28 20:43:56,590 INFO sqlalchemy.engine.base.Engine ()
(1, 'mary@example.com', 1, 1, 'Mary', 30, 'secret')
(2, 'john@nowhere.net', 2, 2, 'John', 42, '123456')
(3, 'john@example.org', 2, 2, 'John', 42, '123456')
(4, 'carl@nospam.net', 4, 4, 'Carl', 33, None)
ORM风格
sqlalchemy模块的面向对象封装部分改动较大,参考资料1和2里面的内容很多都过时了,没办法只好看嚼官方文档了。
首先我们看到下面这段代码:
import sqlalchemy
from sqlalchemy import *
from sqlalchemy_utils import database_exists, create_database
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///:memory:',echo=True)
if not database_exists(engine.url):###确保目标数据库是存在的。
create_database(engine.url)
metadata = MetaData(bind = engine)
Base = declarative_base(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String)
fullname = Column(String)
password = Column(String)
def __init__(self,name,fullname,password):
self.name = name
self.fullname = fullname
self.password = password
def __repr__(self):
return '<User {}>'.format(self.name)
我们调用 User
类的 __table__
,其实质就是前面no-orm风格提及的Table对象。
>>> User.__table__
Table('users', MetaData(bind=Engine(sqlite:///:memory:)), Column('id', Integer(), table=<users>, primary_key=True, nullable=False), Column('name', String(), table=<users>), Column('fullname', String(), table=<users>), Column('password', String(), table=<users>), schema=None)
>>>
ORM层是通过Session对象来和数据库进行会话的:
from sqlalchemy.orm import *
Session = sessionmaker(bind=engine)
session = Session()
下面先将如何通过session进行数据库的CRUD(CREATE RETRIEVE UPDATE DELETE)操作分别说明一下:
通过orm层创建数据库
或者引入orm对象之后,调用其 __table__
Table对象,然后像之前的调用 create
方法即可。
UserInfo.__table__.create(bind =engine)
Warning: 单表这样创建必须没有外键关系,有则会失败。
或者如官方教程推荐的:
Base.metadata.create_all(engine)
增加记录
如下来给某个表格增加一条记录:
admin = User('admin','administor','admin')
session.add(admin) # 此外还有 add_all 方法可以一次性添加多个orm对象
session.commit()
当session add
了某一条记录,这种更改称之为on-fly更改,后面谈及的其他基于python对象的操作从而对具体某个记录的某个属性的更改也是如此,都是on-fly模式。也就是只有在执行了 session.commit()
之后,所有的更改才会实际刷入数据库,而之前的更改虽然没有实际刷入数据库,但后面代码的查询等等操作都是基于这种改动之后新的(可以看作以某种形式的基于内存的)数据库的。
如果你了解SQL的transaction的概念,就清楚SQL数据库的实现通过transaction来实现数据提交的安全保障——如果一次transaction提交失败,那么将会rollback回滚之,从而保证SQL数据库不会mess up。session有 rollback
方法可以主动回滚,这样on-fly的没有commit的所有transaction都会被丢弃。当session commit
之后,这次的transaction成功提交了就完成了,下次又是另外一个新的transaction。
查询记录
同之前谈及的no-orm风格中提到的select语句查询不同,orm风格的查询语句更加的精简了,但仍然没有脱离select语句查询的本质,熟悉SQL的select语句能够帮助我们更好地学习下面的查询语句。
查询的起步是:
session.query(User)
其返回的是orm子模块里面的Query对象。User是具体要查询的某个类(对应某个表格)。简单的理解是将这个 query
方法看作select操作的 select * from User
。 这个Query对象是个可迭代对象,迭代过程中上面返回的就是 User 对象。
若写成这样的形式:
session.query(User.name, User.fullname)
则大致对应的是 select name,fullname from User
。
具体如下所示:
>>> for i in session.query(User):
... print(i)
...
<User admin>
>>> guest = User('guest','guest','123456')
>>> session.add(guest)
>>> for name,fullname in session.query(User.name,User.fullname):
... print(name,fullname)
...
admin administor
guest guest
在学习SQL的select语句的时候我们学到在 select what from what
语句之后还可以跟上where字句,order by字句等等。sqlalchemy的orm封装同样支持这样的额外操作,具体就是在上面的query语句的基础上进一步操作。经过这些额外的操作返回的同样还是Query对象,也就是你可以写上 session.query(User).filter_by(what).filter(what).order_by(what)
。这样看上去有点长的语句,只要你熟悉SQL语句,并知道在做些什么,那么是完全没有问题的。
过滤排序等操作
- filter方法: filter方法对应select语句的where字句。下面是官方文档的一些例子,复制到这里看看熟悉一下即可,大多是什么含义一般都是清楚的:
query.filter(User.name == 'ed')
query.filter(User.name != 'ed')
query.filter(User.name.like('%ed%'))
query.filter(User.name.in_(['ed', 'wendy', 'jack']))
query.filter(~User.name.in_(['ed', 'wendy', 'jack']))#not in
query.filter(User.name == None)
query.filter(User.name != None)
query.filter(and_(User.name == 'ed', User.fullname == 'Ed Jones'))
query.filter(or_(User.name == 'ed', User.name == 'wendy'))
- filter_by方法: filter_by方法类似filter方法,除了如上面的User.name要写成name,也就是直接引用表格的列名。
query.filter_by(name = 'ed')
- order_by方法: 对应select语句的order by字句。
order_by(User.id)[1:3]
然后上面还揭示了 Query 对象很重要的一个特性,其支持python的切片操作。
返回结果
Query对象还可以通过下面这些方法还获得返回结果:
-
all(): 返回一个列表,包含所有的结果。
-
first(): 返回第一个结果。如果没有结果则返回None。
-
one(): 严格只有一个结果,如果有多个结果,将抛出 MultipleResultsFound 异常,如果没有结果,将抛出 NoResultFound 异常。
-
scalar(): 参考了 这个网页第五条 ,执行查询,如果有多条记录命中,则抛出MultipleResultsFound 异常,如果没有命中,则返回None,如果命中数为一条记录,则返回该记录的 第一列 的值。
- count(): 返回命中记录数。
更多的查询例子
session.query(Game).filter(Game.release_date >= '1999-01-01')
text函数
text函数用于支持 filter
和 order_by
方法支持原生的SQL语句表达。大致如下所示,了解下即可:
from sqlalchemy import text
session.query(User).filter(text("id<224")).order_by(text("id"))
更改记录
更改记录经过ORM封装之后变得很简单了,就是查询之后获得对应的python对象,然后直接修改即可。
game = session.query(Game).get(1)
game.name = 'Super Mario Brothers'
session.commit()
删除记录
session.delete(jack)
批量修改或删除
session.query(Game).filter_by(category="RPG").update({"category": "ARPG"})
session.query(Game).filter_by(category="ARPG").delete()
ORM层的关系
SQL表格有四种关系,one-to-one, one-to-many, many-to-one, many-to-many,它们实际上都是基于SQL的外键约束和join查询。其中one-to-many和many-to-one是最需要了解清楚的关系模型,在这之上many-to-many,三个SQL表格搭建起来的关系模型也就很好理解了。推荐读者阅读 这篇文章 来更好地理解SQL表格的这四种关系模型。因为one-to-one实际上是one-to-many的特殊情形,而many-to-one实际上是one-to-many模型的反向,所以我们首先需要重点了解one-to-many模型。
one-to-many模型
sqlalchemy的orm层对one-to-many关系进行了高度封装,使得你不需要考虑SQL的join连接语法细节,只需要声明好外键约束和关系约束(可以看作sqlalchemy新加入了关系约束),然后就可以神奇的使用SQL表格one-to-many的全部特性了。
首先让我们用ORM层的join方法来暂时SQL表格的这些relationship的建立细节,然后再来具体讨论更实用的ORM层的relationship建立的写法。
这是示例代码:
import sqlalchemy
from sqlalchemy import *
from sqlalchemy_utils import database_exists, create_database
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///test.db',echo=True)
if not database_exists(engine.url):
print('create new database')
create_database(engine.url)
metadata = MetaData(bind = engine)
Base = declarative_base(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String)
password = Column(String)
def __init__(self,name,password):
self.name = name
self.password = password
def __repr__(self):
return '<User {}>'.format(self.name)
class Email(Base):
__tablename__ = 'emails'
id = Column(Integer,primary_key=True)
email = Column(String)
user = Column(Integer,ForeignKey('users.id'))
def __init__(self,email,user):
self.email = email
self.user = user
def __repr__(self):
return '<Email {}>'.format(self.email)
Base.metadata.create_all(checkfirst=True)### create table
from sqlalchemy.orm import *
Session = sessionmaker(bind=engine)
session = Session()
admin = User('admin','admin')
session.add(admin)
session.add_all([User('Mary','secret'),
User('John','123456'),
User('Susan','123456'),
User('Carl','123456')])
session.add_all([Email('mary@example.com',2),
Email('john@nowhere.net',3),
Email('john@example.org',3),
Email('carl@nospam.net',4)])
session.commit()
然后用sqliteman观察数据库情况如下:
session.query(User,Email)
返回的是笛卡尔积的形式:
>>> session.query(User,Email).all()
[(<User admin>, <Email mary@example.com>), (<User admin>, <Email john@nowhere.net>), (<User admin>, <Email john@example.org>), (<User admin>, <Email carl@nospam.net>), (<User Mary>, <Email mary@example.com>), (<User Mary>, <Email john@nowhere.net>), (<User Mary>, <Email john@example.org>), (<User Mary>, <Email carl@nospam.net>), (<User John>, <Email mary@example.com>), (<User John>, <Email john@nowhere.net>), (<User John>, <Email john@example.org>), (<User John>, <Email carl@nospam.net>), (<User Susan>, <Email mary@example.com>), (<User Susan>, <Email john@nowhere.net>), (<User Susan>, <Email john@example.org>), (<User Susan>, <Email carl@nospam.net>), (<User Carl>, <Email mary@example.com>), (<User Carl>, <Email john@nowhere.net>), (<User Carl>, <Email john@example.org>), (<User Carl>, <Email carl@nospam.net>)]
然后调用Query对象的 join 方法执行了内连接:
>>> session.query(User,Email).join(Email).all()
[(<User Mary>, <Email mary@example.com>), (<User John>, <Email john@nowhere.net>), (<User John>, <Email john@example.org>), (<User Susan>, <Email carl@nospam.net>)]
>>>
从这里我们就可以看出一点one-to-many的影子了,注意John对应了两个Email对象。
然后我们稍加过滤条件:
>>> session.query(User,Email).join(Email).filter(User.name == 'John').all()
[(<User John>, <Email john@nowhere.net>), (<User John>, <Email john@example.org>)]
或者更明确的查询email:
>>> session.query(User,Email.email).join(Email).filter(User.name == 'John').all()
[(<User John>, 'john@nowhere.net'), (<User John>, 'john@example.org')]
此外还有这种形式:
>>> session.query(Email,User).join(User).all()
[(<Email mary@example.com>, <User Mary>), (<Email john@nowhere.net>, <User John>), (<Email john@example.org>, <User John>), (<Email carl@nospam.net>, <User Susan>)]
>>> session.query(Email,User).join(User).filter(User.name == 'John').all()
[(<Email john@nowhere.net>, <User John>), (<Email john@example.org>, <User John>)]
由于内连接虽然输出一行具体输出内容根据你的 select what
不同而不同,但具体行数和对于内容的描述上实际上就是一回事。上面是另外一种内连接顺序。然后我们利用这个反向查询某个邮箱的User也是可以的,这就是many-to-one数据模型了。
此外Query对象当然还有 outerjoin 方法,因为这里是要描述各关系模型,就略过了。下面介绍ORM层更实用的关系定义方法:
import sqlalchemy
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy_utils import database_exists, create_database
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///test.db',echo=True)
if not database_exists(engine.url):
print('create new database')
create_database(engine.url)
metadata = MetaData(bind = engine)
Base = declarative_base(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String)
password = Column(String)
email = relationship("Email",backref=backref('user'))
def __init__(self,name,password):
self.name = name
self.password = password
def __repr__(self):
return '<User {}>'.format(self.name)
class Email(Base):
__tablename__ = 'emails'
id = Column(Integer,primary_key=True)
email = Column(String)
user_id = Column(Integer,ForeignKey('users.id'))
def __init__(self,email,user_id):
self.email = email
self.user_id = user_id
def __repr__(self):
return '<Email {}>'.format(self.email)
Base.metadata.create_all(checkfirst=True)### create table
Session = sessionmaker(bind=engine)
session = Session()
admin = User('admin','admin')
session.add(admin)
session.add_all([User('Mary','secret'),
User('John','123456'),
User('Susan','123456'),
User('Carl','123456')])
session.add_all([Email('mary@example.com',2),
Email('john@nowhere.net',3),
Email('john@example.org',3),
Email('carl@nospam.net',4)])
john = session.query(User).filter(User.name == 'John').one()
e1 = session.query(Email).filter(Email.email == 'john@example.org').one()
然后我们就可以这样使用了:
>>> john.email
[<Email john@nowhere.net>, <Email john@example.org>]
>>> e1.user
<User John>
这确实很好用,而这里具体的模型就是one(user)对应many(email)的one-to-many模型。
下面重点介绍一下 relationship 这一行具体干了些什么:
email = relationship("Email",backref=backref('user'))
- 给User email属性,如上你可以这样
john.email
这样调用了。 - 指定Email对应(many)端(可以理解为这里针对Email执行了内连接操作,当然sqlalchemy具体如何处理的内部细节我还不清楚,但应该差不多就是这个过程。),这样的话具体User.email的值就通过某种机制大概如下所示
>>> session.query(User,Email).join(Email).filter(User.name == 'John').all()
>>> [(<User John>, <Email john@nowhere.net>), (<User John>, <Email john@example.org>)]
这样获得了User John所回应的几个Email对象。具体过程不清楚,但用上面这样的语句来理解应该已经八九不离十了。
one-to-one模型
one-to-one模型就是one-to-many模型的特例,所以这里先讲了,和上面比较起来区别很小的。
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String)
password = Column(String)
email = relationship("Email",backref=backref('user'),uselist=False)
def __init__(self,name,password):
self.name = name
self.password = password
def __repr__(self):
return '<User {}>'.format(self.name)
class Email(Base):
__tablename__ = 'emails'
id = Column(Integer,primary_key=True)
email = Column(String)
user_id = Column(Integer,ForeignKey('users.id'))
def __init__(self,email,user_id):
self.email = email
self.user_id = user_id
def __repr__(self):
return '<Email {}>'.format(self.email)
就加上了 uselist=False)
这一句,这样将直接返回某个Email对象。
many-to-one模型
many-to-one模型实际上和one-to-many模型就是一回事,而且如果我们如同上面的把 backref
设置好,针对多个Email对象实际上就可以直接找到某个User对象了,所以为了简单起见,我们可以就直接用one-to-many模型来理解之。
many-to-many模型
many-to-many模型有点复杂和难于理解,这是因为其还要求有一个额外的Table来管理原两个表格之间的元素的映射关系,幸好sqlalchemy官方文档专门有一小节对这个做出了一些说明。其描述的一个应用场景就是一篇博文有多个标签,然后一个标签有多篇博文(我们可以简单构建出这样一个功能,单击一个标签按钮,然后弹出所有有这些标签的文章出来)。一个博文有多个标签这很简单,一个one-to-many模型就解决了,大概就是 blog.tags
,就弹出一个list,里面装着一些标签对象。所以关键性的问题是如何实现出 tag.blogs
,就弹出一个list,里面装着一些博文对象。而在 这篇文章 的这幅图片中:
于是现在的情况变成这样的了,blog one-to-many,但to many的是一个中间表格,而tag one-to-many,这个many也是一个中间表格。我们知道所谓的many一方存储着外键约束值,所以这个中间表格就两列,左列外键引用blog,右列外键引用tag,具体每一个映射关系都要写一条记录上去。不管怎么说,看下面这个例子吧:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sqlalchemy
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy_utils import database_exists, create_database
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///test2.db',echo=True)
if not database_exists(engine.url):
print('create new database')
create_database(engine.url)
metadata = MetaData(bind = engine)
Base = declarative_base(bind=engine)
blog_tags = Table('blog_tags',Base.metadata,
Column('blog_id',Integer,ForeignKey('blogs.id')),
Column('tag_id',Integer,ForeignKey('tags.id')))
class Blog(Base):
__tablename__ = 'blogs'
id = Column(Integer,primary_key=True)
title = Column(String)
body = Column(String)
tags = relationship("Tag",secondary=blog_tags,backref=backref('blogs'))
def __init__(self,title,body):
self.title = title
self.body = body
def __repr__(self):
return '<BLog {}>'.format(self.title)
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer,primary_key=True)
tag = Column(String)
def __init__(self,tag):
self.tag = tag
def __repr__(self):
return '<Tag {}>'.format(self.tag)
Base.metadata.create_all(checkfirst=True)### create table
Session = sessionmaker(bind=engine)
session = Session()
blog1 = Blog('learning mysql','how to learning mysql')
tag1 = Tag('python')
blog2 = Blog('learning sqlalchemy','how to learning sqlalchemy')
tag2 = Tag('sql')
tag3 = Tag('sqlalchemy')
tag4 = Tag('mysql')
blog1.tags.append(tag2)
blog1.tags.append(tag4)
blog2.tags.append(tag1)
blog2.tags.append(tag2)
blog2.tags.append(tag3)
session.add_all([blog1,blog2,tag1,tag2,tag3,tag4])
session.commit()
然后生成的表格如下所示:
然后执行结果如下:
>>> blog1
<BLog learning mysql>
>>> blog1.tags
[<Tag sql>, <Tag mysql>]
>>> blog2
<BLog learning sqlalchemy>
>>> blog2.tags
[<Tag python>, <Tag sql>, <Tag sqlalchemy>]
>>> tag1
<Tag python>
>>> tag1.blogs
[<BLog learning sqlalchemy>]
>>> tag2.blogs
[<BLog learning mysql>, <BLog learning sqlalchemy>]
这里的关键就是建立这样一个中间表格:
blog_tags = Table('blog_tags',Base.metadata,
Column('blog_id',Integer,ForeignKey('blogs.id')),
Column('tag_id',Integer,ForeignKey('tags.id')))
然后建立一个这样的relationship:
tags = relationship("Tag",secondary=blog_tags,backref=backref('blogs'))
其中 secondary 参数指定你新建的那个中间表格,然后这个中间表格任何数据都不需要你操心了,只需要如上直接对 blog1.tags
这个属性(应该是一个列表)操作就行了。
具体利用ORM层来实现很简单,但我不敢想像sqlalchemy底层到底做了多少工作,不得不承认,这真是sqlalchemy Great的地方。然后值得一提的地方是原来的两个表格都没有外键约束了,可以说这个关系连接的工作完全抽象成为一个表格了。
总之,类似one-to-many一样,在one那里管理某个many方的表格,然后回引backref让many方那个对象也可以使用某个属性,然后定义一个中间表格就行了。many-to-many数据模型还是很有用的。
删除动作
manytomany前面的例子已经说明了如何append一个元素,如果要删除一个元素,调用remove方法即可:
blog.tags.remove(the_tag_object)
高级议题
cascade
定义基于关系的删除行为
items = relationship("Item", cascade="all, delete-orphan")
默认值是 save-update merge
- save-update
指一个对象 Session.add() 进入之后,和它有关的其他对象都应加进去。
- merge
和Session.merge行为有关
此外用的最多的是 all 和 delete-orphan
all 指 save-update merge refresh-expire expunge delete
- delete
和Session.delete行为有关,默认没加delete,则子对象只是parent_id那里只是赋空值,加了之后子对象也将删除。
- delete-orphan
增加delete 的删除行为,不仅子对象将被删除,而且子对象也将执行Session delete标记,也就是后面的子子对象也将删除如何和delete一起配合使用的话。
自我引用表达树状结构
用一个SQL表格就可以表达出这样的树状层级结构的(这在很大程度上弥补了python语言对于这样的树状结构的应付能力不足):
root --+---> child1
+---> child2 --+--> subchild1
| +--> subchild2
+---> child3
具体写法如下所示:
class Folder(db.Model):
__tablename__ = 'folders'
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(400),nullable=False)
description = db.Column(db.String(800))
parent_id = db.Column(db.Integer,db.ForeignKey('folders.id'))
children = db.relationship("Folder",
backref=db.backref('parent',remote_side=[id]))
具体就是认为 parent_id
是NULL的认为是最高级节点,然后每一个子节点都需要描述自己的 parent_id
是谁。这里的children是引用的自己,大体类似 one-to-many
的写法,也就是一个节点有多个子节点,这个前面将过来。唯一的区别就是设置 remote_side=[id]
,似乎这种写法也是可以的 remote_side=id
,意思是parent_id是本地local端的,然后id列是remote端的。更多信息请参看 官方文档的这里 。
面向ORM的内省机制
如果原数据库表格已经存在,在前面提及可以如下:
users = Table('users',db,autoload=True)
来自动内省某个表格,而在面向ORM写法中,也是可以的。具体请参看 官方文档的这里 。其中最核心的代码是:
engine = create_engine('sqlite:///session.db')
from sqlalchemy.ext.automap import automap_base
AutoBase = automap_base(bind = engine)
class OldTable(AutoBase):
__tablename__ = NewTable.__tablename__
但是具体schema并不是可以任意改动的,一般是继续扩展SQL数据库,然后搭建各种关系,实在有改动schema的必要,推荐采用migrate机制。下面是我写的一个简单的migrate脚本:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
from __future__ import unicode_literals
import sqlalchemy
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy_utils import database_exists, create_database
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.ext.declarative import declarative_base
new_engine = create_engine('sqlite:///new_session.db')
Base = declarative_base(bind = new_engine)
old_engine = create_engine('sqlite:///session.db')
class User(Base):
__tablename__ = 'user'
id = Column(Integer,primary_key=True)
username = Column(String(80),unique=True)
password = Column(String(80))
def __init__(self,username,password,**kwargs):
'''kwargs用于收集其他废参数'''
self.username = username
self.password = password
def __repr__(self):
return '<User {}>'.format(self.username)
def migrate_database(NewTable,fromdb,todb):
if not database_exists(fromdb.url):
raise Exception
else:
NewTable.__table__.create(checkfirst=True)### create table
AutoBase = automap_base(bind = fromdb)
class OldTable(AutoBase):
__tablename__ = NewTable.__tablename__
AutoBase.prepare(fromdb, reflect=True)
Session = sessionmaker(bind=fromdb)
old_session = Session()
Session = sessionmaker(bind=todb)
new_session = Session()
for q in old_session.query(OldTable).all():
add_one = NewTable(**q.__dict__)
new_session.add(add_one)
new_session.commit()
print('done')
if __name__ == '__main__':
migrate_database(User,old_engine,new_engine)
但是如果有多个表格加上关系之后情况变得更复杂了,上面的脚本
AutoBase.prepare(fromdb, reflect=True)
就是建立内省的模型和关系的,所以如果多个表格的话,这句话应该再放到后面些。然后后面添加新的数据因为sqlalchemy有自动处理相关关系对象的功能,这里倒问题不大,但也可能会有问题。也有其他一些模块是专门处理这个迁移数据库的问题的,但也绝不是一件轻松的事。总之SQL表格尽量设计好和可扩展性好,将自己的太多精力花在这上面是很浪费的。
面向ORM的数据继承机制
有时间补上。
额外的属性支持
所谓的额外的属性并不是基于SQL表格的某一列的属性,而是在ORM之上建立起来的额外的属性,其一般是基于SQL表格的某一列或某些列的,是ORM封装之上提供的更加便利的属性接口。
class EmailAddress(Base):
__tablename__ = 'email_address'
id = Column(Integer, primary_key=True)
_email = Column("email", String)
@hybrid_property
def email(self):
"""Return the value of _email up until the last twelve
characters."""
return self._email[:-12]
@email.setter
def email(self, email):
"""Set the value of _email, tacking on the twelve character
value @example.com."""
self._email = email + "@example.com"
某一列的额外的别名
这里所谓的某一列额外的别名指并没有创建额外的列,而是在ORM层针对某一列可以用额外的别名来做类似的操作。
from sqlalchemy.ext.declarative import synonym_for
class MyClass(Base):
__tablename__ = 'my_table'
id = Column(Integer, primary_key=True)
status = Column(String(50))
@synonym_for("status")
@property
def job_status(self):
return "Status: " + self.status
其等于:
class MyClass(Base):
__tablename__ = 'my_table'
id = Column(Integer, primary_key=True)
status = Column(String(50))
@property
def job_status(self):
return "Status: " + self.status
job_status = synonym("status", descriptor=job_status)
也就是具体该列在ORM层可以通过 status
或者 job_status
来操作。具体参考 这里 。
多列组合唯一性约束
请参看 这个网页 。
如果是ORM层,则是:
class Location(Base):
__tablename__ = 'locations'
id = Column(Integer, primary_key = True)
customer_id = Column(Integer, ForeignKey('customers.customer_id'), nullable=False)
location_code = Column(Unicode(10), nullable=False)
__table_args__ = (UniqueConstraint('customer_id', 'location_code', name='_customer_location_uc'),
)
上面flask_sqlalchemy的话可以使用 db.UniqueConstraint
。 比如:
__table_args__ = (
db.UniqueConstraint("main_directory", "sub_directory","filename",
"filext"),
)
如果是Core层,则是:
mytable = Table('mytable', meta,
# ...
Column('customer_id', Integer, ForeignKey('customers.customer_id')),
Column('location_code', Unicode(10)),
UniqueConstraint('customer_id', 'location_code', name='uix_1')
)
ORM层的内省
Table层直接利用已经存在的数据库表格就是 reflect
的概念,因为ORM层多了很多额外的东西,其中最关键的是 relationship 的概念,而sqlalchemy的Automap这一章主要就是解决ORM层内省这个问题的。更详细的讨论请参看 官方文档 。
最基本的应用就是 建立一个 automap_base
对象:
from sqlalchemy.ext.automap import automap_base
Base = automap_base()
然后运行其 prepare
方法进行内省:
Base.prepare(engine, reflect=True)
然后对应的sqlalchemy ORM层的那些类就可以如下获得了:
User = Base.classes.user
#或 Base.classes.get('user')
对于一般的属性引用,这是没有问题的。就是relationship可能还是有问题,那么我们可以预先定义一些属性,在 prepare
之前,那么预先定义的那些东西也将覆盖后面的自动reflect那部分定义,这可以起到矫正的作用。然后预先定义的那个类就是后面要使用的类了,就不要用 Base.classes.what
这种风格再获取了。
分表策略
当某个表格数据量过大的时候,那么就需要建立分表策略,具体是根据某个值取模来确定表名,先看例子吧,本例子参考了 这个网页 。
class NovelChapter(object):
_mapper = {}
@staticmethod
def model(book_id):
table_index = book_id % 100
class_name = 'NovelChapter_{0}'.format(table_index)
ModelClass = NovelChapter._mapper.get(class_name, None)
if ModelClass is None:
ModelClass = type(class_name, (Base,), {
'__module__': __name__,
'__name__': class_name,
'__tablename__': 'bh_novel_chapter_{0}'.format(table_index),
'id': Column(Integer, primary_key=True),
# 这里继续填写目标Model的字段定义
})
NovelChapter._mapper[class_name] = ModelClass
return ModelClass
上面提供了 .model
方法,核心就是利用type函数来生成一个类,也就是人们说的元类编程,具体参数如下:
class = type(classname, superclasses, attributedict)
至于 _mapper
不过是本对象的一个类对象的缓存罢了,免得重复创建。
本例子中最关键的是 __tablename__
的差异化定制。
附录
datetime数据类型
sqlalchemy中DateTime 数据类型的默认值可以跟着 datetime.datetime.utcnow 如下所示:
created_date = Column(DateTime, default=datetime.datetime.utcnow)
如何测试
参考资料
- essential sqlalchemy ; author: Rick copeland ;press:O'REILLY
- a step by step sqlalchemy tutorial
- sqlalchemy-essential-tutorial-and-techniques
- 面向django orm用户的sqlalchemy教程