SQLAlchemy模型使用

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: SQLAlchemy模型使用

阅读目录

 

                      SQLAchemy模型使用

简介:

SQLAlchemy是Python编程语言下的一款ORM框架,该框架建立在数据库API之上,使用关系对象映射进行数据库操作,简言之便是:将对象转换成SQL,然后使用数据API执行SQL并获取执行结果。

SQLAlchemy本身无法操作数据库,其必须以来pymsql等第三方插件,Dialect用于和数据API进行交流,根据配置文件的不同调用不同的数据库API,从而实现对数据库的操作,如:

1 MySQL-Python
 2     mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
 3   
 4 pymysql
 5     mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]
 6   
 7 MySQL-Connector
 8     mysql+mysqlconnector://<user>:<password>@<host>[:<port>]/<dbname>
 9   
10 cx_Oracle
11     oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...]
12   
13 更多详见:http://docs.sqlalchemy.org/en/latest/dialects/index.html

 

1.SQLAlchemy初始化和创建连接

# -*- coding: utf-8 -*-
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column
from sqlalchemy.types import String, Integer
from sqlalchemy.ext.declarative import declarative_base
#导入相应的模块
engine = create_engine("mysql+pymysql://mysql:123456@10.0.0.8:3306/test", max_overflow=5) #创建数据库连接,max_overflow指定最大连接数
DBSession = sessionmaker(engine)#创建DBSession类型
session = DBSession()#创建session对象
BaseModel = declarative_base()#创建对象的基类
class User(BaseModel): #定义User对象
    __tablename__ = 'user' #创建表,指定表名称
    #指定表的结构 
    id = Column(String, primary_key=True)
    username = Column(String, index=True)
class Session(BaseModel):
    __tablename__ = 'session'
    id = Column(String, primary_key=True)
    user = Column(String, index=True)
    ip = Column(String)
BaseModel.metadata.create_all(engine)#创建表,执行所有BaseModel类的子类
session.commit()#提交,必须

2. SQLAlchemy创建连接

SQLAlchemy 的连接创建是 Lazy 的方式, 即在需要使用时才会去真正创建. 之前做的工作, 全是"定义".连接的定义是在 engine 中做的.

回到顶部

2.1. Engine

engine 的定义包含了三部分的内容, 一是具体数据库类型的实现, 二是连接池, 三是策略(即engine 自己的实现).

所谓的数据库类型即是 MYSQL , Postgresql , SQLite 这些不同的数据库.

一般创建 engine 是使用 create_engine 方法:

engine = create_engine("mysql+pymysql://mysql:123456@10.0.0.8:3306/test")

参数字符串的各部分的意义:

"mysql+pymysql://mysql:123456@10.0.0.8:3306/test"

对于这个字符串, SQLAlchemy 提供了工具可用于处理它:

1 # -*- coding: utf-8 -*-
 2
 3 from sqlalchemy import create_engine
 4 from sqlalchemy.engine.url import make_url
 5 from sqlalchemy.engine.url import URL
 6
 7 s = 'postgresql://test@localhost:5432/bbcustom'
 8 url = make_url(s)
 9 s = URL(drivername='postgresql', username='test', password="",host="localhost", port=5432, database="bbcustom")
10
11 engine = create_engine(url)
12 engine = create_engine(s)
13
14 print engine.execute('select id from "user"').fetchall()

create_engine 函数有很多的控制参数, 这个后面再详细说.

回到顶部

2.2. Engine的策略

create_engine 的调用, 实际上会变成 strategy.create 的调用. 而 strategy 就是 engine 的实现细节. strategy 可以在 create_engine 调用时通过 strategy 参数指定, 目前官方的支持有三种:

  • plain, 默认的
  • threadlocal, 连接是线程局部的
  • mock, 所有的 SQL 语句的执行会使用指定的函数

mock 这个实现, 会把所有的 SQL 语句的执行交给指定的函数来做, 这个函数是由create_engineexecutor 参数指定:

1 #!/usr/bin/env python
 2 # time:
 3 # Auto:PANpan
 4 # func:
 5 from sqlalchemy.ext.declarative import declarative_base
 6 from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Index
 7 from sqlalchemy.orm import sessionmaker, relationship
 8 from sqlalchemy import create_engine
 9 def f(sql,*args,**kwargs):
10     print(sql,args,kwargs)
11 s="mysql+pymysql://mysql:123456@10.0.0.8:3306/test"
12 engin=create_engine(s,strategy='mock',executor=f)
13 print(engin.execute('select id from "user"'))

回到顶部

2.3. 各数据库实现

各数据库的实现在 SQLAlchemy 中分成了两个部分, 一是数据库的类型, 二是具体数据库中适配的客户端实现. 比如对于 Postgresql 的访问, 可以使用 psycopg2 , 也可以使用 pg8000 :

1 s = 'postgresql+psycopg2://test@localhost:5432/bbcustom'

2 s = 'postgresql+pg8000://test@localhost:5432/bbcustom'

3 engine = create_engine(s)

具体的适配工作, 是需要在代码中实现一个 Dialect 类来完成的. 官方的实现在 dialects 目录下.

获取具体的 Dialect 的行为, 则是前面提到的 URL 对象的 get_dialect 方法. create_engine 时你单传一个字符串, SQLAlchemy 自己也会使用 make_url 得到一个 URL 的实例).

回到顶部

2.4. 连接池

SQLAlchemy 支持连接池, 在 create_engine 时添加相关参数即可使用.

  • pool_size 连接数
  • max_overflow 最多多几个连接
  • pool_recycle 连接重置周期
  • pool_timeout 连接超时时间

连接池效果:

# -*- coding: utf-8 -*-
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column
from sqlalchemy.types import String, Integer
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine("mysql+pymysql://mysql:123456@10.0.0.8:3306/test", pollsize=2,max_overflow=0)
DBSession = sessionmaker(engine)
session = DBSession()
BaseModel = declarative_base()
from threading import Thread
def f():
    print (engine.execute('show databases').fetchall())
p = []
for i in range(3):
    p.append(Thread(target=f))
for t in p:
    t.start()

 

 

 

3.SQLAlchemy 模型使用

回到顶部

3.1. 模型定义

对于 Table 的定义, 本来是直接的实例化调用, 通过 declarative 的包装, 可以像"定义类"这样的更直观的方式来完成.

user = Table('user',
    Column('user_id', Integer, primary_key = True),
    Column('user_name', String(16), nullable = False),
    Column('email_address', String(60)),
    Column('password', String(20), nullable = False)
)
1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 from sqlalchemy.ext.declarative import declarative_base
 4 from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Index
 5 from sqlalchemy.orm import sessionmaker, relationship
 6 from sqlalchemy import create_engine
 7
 8 engine = create_engine("mysql+pymysql://mysql:123456@10.0.0.8:3306/test", max_overflow=5) #创建数据库连接
 9 DBSession=sessionmaker(engine)#创建了一个自定义了的 Session类
10 session=DBSession()
11 Base = declarative_base()#创建对象的基类
12
13
14 class Blog(Base):
15     __tablename__ = 'blog'
16
17     id = Column(Integer, primary_key=True)
18     title = Column(String(64), server_default='', nullable=False)
19     text = Column(String, server_default='', nullable=False)
20     user = Column(String(32), index=True, server_default='', nullable=False)
21     create = Column(String(32), index=True, server_default='0', nullable=False)
22
23
24 class User(Base):
25     __tablename__ = 'user'
26
27     id = Column(Integer, primary_key=True)
28     name = Column(String(32), server_default='', nullable=False)
29     username = Column(String(32), index=True, server_default='', nullable=False)
30     password = Column(String(64), server_default='', nullable=False)
31
32
33 def init_db():
34     Base.metadata.create_all(engine)
35
36 def drop_db():
37     Base.metadata.drop_all(engine)
38
39 if __name__ == '__main__':
40     init_db()
41     #drop_db()
42     #Base.metadata.tables['user'].create(engine, checkfirst=True)
43     #Base.metadata.tables['user'].drop(engine, checkfirst=False)
44     #pass

回到顶部

3.2. 创建

1 session = Session()

2 session.add(User(id=1)) #创建一个

3 session.add(Blog(id=1))

4 session.add_all([ User(id=2),Blog(id=2)]) #创建多个

5 session.commit()

执行的顺序并不一定会和代码顺序一致, SQLAlchemy 自己会整合逻辑再执行.

回到顶部

3.3. 查询

SQLAlchemy 实现的查询非常强大, 写起来有一种随心所欲的感觉.

查询的结果, 有几种不同的类型, 这个需要注意, 像是:

  • instance
  • instance of list
  • keyed tuple of list
  • value of list

基本查询:

1 session.query(User).filter_by(username='abc').all()#查询用户User创建表中username字段等于abc的内容
 2 session.query(User).filter(User.username=='abc').all()
  #平时使用的时候,两者区别主要就是当使用filter的时候条件之间是使用“==",fitler_by使用的是"="
 3 session.query(Blog).filter(Blog.create >= 0).all() #查询Blog对象中创建表中create字段大于等于0的所有内容
 4 session.query(Blog).filter(Blog.create >= 0).first()
 5 session.query(Blog).filter(Blog.create >= 0 | Blog.title == 'A').first()#条件或
 6 session.query(Blog).filter(Blog.create >= 0 & Blog.title == 'A').first()#条件与
 7 session.query(Blog).filter(Blog.create >= 0).offset(1).limit(1).scalar()#  offset(N)从第N条开始返回  limit(N)最多返回 N 条记录 scalar() 如果有记录
 8 session.query(User).filter(User.username ==  'abc').scalar()
 9 session.query(User.id).filter(User.username ==  'abc').scalar()
10 session.query(Blog.id).filter(Blog.create >= 0).all()
11 session.query(Blog.id).filter(Blog.create >= 0).all()[0].id
12 dict(session.query(Blog.id, Blog.title).filter(Blog.create >= 0).all())
13 session.query(Blog.id, Blog.title).filter(Blog.create >= 0).first().title #记录不存在时,first() 会返回 None
14 session.query(User.id).order_by('id desc').all()#对结果集进行排序
15 session.query(User.id).order_by('id').first()
16 session.query(User.id).order_by(User.id).first()
17 session.query(User.id).order_by(-User.id).first()
18 session.query('id', 'username').select_from(User).all()#与
19 session.query(User).get('16e19a64d5874c308421e1a835b01c69')# 以主键获取

多表查询:

session.query(Blog, User).filter(Blog.user == User.id).first().User.username

session.query(Blog, User.id, User.username).filter(Blog.user == User.id).first().id

session.query(Blog.id,User.id,User.username).filter(Blog.user == User.id).first().keys()

条件查询:

from sqlalchemy import or_, not_
session.query(User).filter(or_(User.id == '',
                               User.id == '1')).all()
session.query(User).filter(not_(User.id == '1')).all()
session.query(User).filter(User.id.in_(['1'])).all()
session.query(User).filter(User.id.like('1%')).all()
session.query(User).filter(User.id.startswith('1')).all()
dir(User.id)

函数:

from sqlalchemy import func
session.query(func.count('1')).select_from(User).scalar()
session.query(func.count('1'), func.max(User.username)).select_from(User).first()
session.query(func.count('1')).select_from(User).scalar()
session.query(func.md5(User.username)).select_from(User).all()
session.query(func.current_timestamp()).scalar()
session.query(User).count()

回到顶部

3.4. 修改

还是通常的两种方式:

1 session.query(User).filter(User.username == 'abc').update({'name': '123'})
2 session.commit()
3
4 user=session.query(User).filter_by(username='abc').scalar()
5 user.name = '223'
6 session.commit()

如果涉及对属性原值的引用, 则要考虑 synchronize_session 这个参数.

  • 'evaluate' 默认值, 会同时修改当前 session 中的对象属性.
  • 'fetch' 修改前, 会先通过 select 查询条目的值.
  • False 不修改当前 session 中的对象属性.

在默认情况下, 因为会有修改当前会话中的对象属性, 所以如果语句中有 SQL 函数, 或者"原值引用", 那是无法完成的操作, 自然也会报错, 比如:

from sqlalchemy import func

session.query(User).update({User.name: func.trim('123 ')})

session.query(User).update({User.name: User.name + 'x'})

这种情况下, 就不能要求 SQLAlchemy 修改当前 session 的对象属性了, 而是直接进行数据库的交互, 不管当前会话值:

session.query(User).update({User.name: User.name + 'x'}, synchronize_session=False)

是否修改当前会话的对象属性, 涉及到当前会话的状态. 如果当前会话过期, 那么在获取相关对象的属性值时, SQLAlchemy 会自动作一次数据库查询, 以便获取正确的值:

user = session.query(User).filter_by(username='abc').scalar()
print user.name
session.query(User).update({User.name: 'new'}, synchronize_session=False)
print (user.name)
session.commit()
print (user.name)

执行了 update 之后, 虽然相关对象的实际的属性值已变更, 但是当前会话中的对象属性值并没有改变. 直到 session.commit() 之后, 当前会话变成"过期"状态, 再次获取 user.name 时, SQLAlchemy 通过 userid 属性, 重新去数据库查询了新值. (如果 userid 变了呢? 那就会出事了啊.)

synchronize_session 设置成 'fetch' 不会有这样的问题, 因为在做 update 时已经修改了当前会话中的对象了.

不管 synchronize_session 的行为如何, commit 之后 session 都会过期, 再次获取相关对象值时, 都会重新作一次查询.

回到顶部

3.5. 删除

session.query(User).filter_by(username='abc').delete()


user = session.query(User).filter_by(username='abc').first()

session.delete(user)

删除同样有像修改一样的 synchronize_session 参数的问题, 影响当前会话的状态.

回到顶部

3.6. JOIN

SQLAlchemy 可以很直观地作 join 的支持:

1 r = session.query(Blog, User).join(User, Blog.user == User.id).all()
2 for blog, user in r:
3     print (blog.id, blog.user, user.id)
4 r = session.query(Blog, User.name, User.username).join(User, Blog.user == User.id).all()
5 print (r)

表操作总结:


表结构+连接数据库

 

1.增

1 obj = Users(name="alex0", extra='sb')
2 session.add(obj)
3 session.add_all([
4     Users(name="alex1", extra='sb'),
5     Users(name="alex2", extra='sb'),
6 ])
7 session.commit()

2.删

session.query(Users).filter(Users.id > 2).delete()
session.commit()

3.改

1 session.query(Users).filter(Users.id > 2).update({"name" : "099"})
2 session.query(Users).filter(Users.id > 2).update({Users.name: Users.name + "099"}, synchronize_session=False)
3 session.query(Users).filter(Users.id > 2).update({"num": Users.num + 1}, synchronize_session="evaluate")
4 session.commit()

4.查

1 ret = session.query(Users).all()
2 ret = session.query(Users.name, Users.extra).all()
3 ret = session.query(Users).filter_by(name='alex').all()
4 ret = session.query(Users).filter_by(name='alex').first()

5.其他

View Code

 

 

 

 

 

 

 

4. SQLAlchemy外键和关系

回到顶部

4.1. 外键约束

使用 ForeignKey 来定义一个外键约定:

1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 from sqlalchemy.ext.declarative import declarative_base
 4 from sqlalchemy import Column, Integer, String, CHAR,BIGINT,ForeignKey, UniqueConstraint, Index
 5 from sqlalchemy.orm import sessionmaker, relationship
 6 from sqlalchemy import create_engine
 7
 8 engine = create_engine("mysql+pymysql://mysql:123456@10.0.0.8:3306/test", max_overflow=5) #创建数据库连接
 9 DBSession=sessionmaker(engine)#创建了一个自定义了的 Session类
10 session=DBSession()
11 BaseModel = declarative_base()#创建对象的基类
12 class Blog(BaseModel):
13     __tablename__='blog'
14
15     id=Column(BIGINT,primary_key=True,autoincrement=True)
16     title=Column(String(64),server_default='',nullable=False)
17     text=Column(String(256),server_default='',nullable=False)
18     user=Column(BIGINT,ForeignKey('user.id'),index=True,nullable=False)#将user表中id字段作为外键,使用ForeignKey指定
19
20 class User(BaseModel):
21     __tablename__='user'
22
23     id=Column(BIGINT,primary_key=True,autoincrement=True)
24     name=Column(String(32),server_default='',nullable=False)
25     username=Column(String(32),index=True,server_default='',nullable=True)
26     passwd=Column(String(64),server_default='',nullable=False)

创建时:

1 BaseModel.metadata.create_all(engine)
2 user=User(name='first',username='pan',passwd='123456')
3 session.add(user)
4 session.flush()
5 blog = Blog(title='frist', user=user.id)
6 session.add(blog)
7 session.commit()

session.flush() 是进行数据库交互, 但是事务并没有提交. 进行数据库交互之后, user.id 才有值.

定义了外键, 对查询来说, 并没有影响. 外键只是单纯的一条约束而已. 当然, 可以在外键上定义一些关联的事件操作, 比如当外键条目被删除时, 字段置成 null , 或者关联条目也被删除等.

回到顶部

4.2. 关系定义

要定义关系, 必有使用 ForeignKey 约束. 当然, 这里说的只是在定义模型时必有要有, 至于数据库中是否真有外键约定, 这并不重要.

接下来我们来了解几个关于外键(Foreign Key)的小知识:

1. FOREIGN KEY 约束是大多数(但不是所有)的关系型数据库中可以链接到主键列,或者拥有UNIQUE约束的列。

2. FOREIGN KEY 能够引用多重列主键,并且其自身拥有多重列,被称为“复合外键”(composite foreign key)。其也能够引用这些列的子集(subset)。(注:这地方不太明白)

3. FOREIGN KEY 列作为对于其引用的列或者行的变化的响应能够自动更新其自身,比如CASCADE引用操作,这些都是内置于关系型数据库的功能之一。

4. FOREIGN KEY 能够引用其自身的表,这个就涉及到“自引用”(self-referential)的外键了。

5. 更多关于外键的资料可以参考Foreign Key – Wikipedia

1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 from sqlalchemy.ext.declarative import declarative_base
 4 from sqlalchemy import Column, Integer, String, CHAR,BIGINT,ForeignKey, UniqueConstraint, Index
 5 from sqlalchemy.orm import sessionmaker, relationship
 6 from sqlalchemy import create_engine
 7
 8 engine = create_engine("mysql+pymysql://mysql:123456@10.0.0.8:3306/test", max_overflow=5) #创建数据库连接
 9 DBSession=sessionmaker(engine)#创建了一个自定义了的 Session类
10 session=DBSession()
11 BaseModel = declarative_base()#创建对象的基类
12 class Blog(BaseModel):
13     __tablename__='blog'
14
15     id=Column(BIGINT,primary_key=True,autoincrement=True)
16     title=Column(String(64),server_default='',nullable=False)
17     text=Column(String(256),server_default='',nullable=False)
18     user=Column(BIGINT,ForeignKey('user.id'),index=True,nullable=False)
19     user_obj=relationship('User')
20
21 class User(BaseModel):
22     __tablename__='user'
23
24     id=Column(BIGINT,primary_key=True,autoincrement=True)
25     name=Column(String(32),server_default='',nullable=False)
26     username=Column(String(32),index=True,server_default='',nullable=True)
27     passwd=Column(String(64),server_default='',nullable=False)
28     blog_list=relationship('Blog',backref='Blog.user')
       #relationship函数是sqlalchemy对关系之间提供的一种便利的调用方式, backref参数则对关系提供反向引用的声明。
     #大致原理应该就是sqlalchemy在运行时对Blog对象动态的设置了一个指向所属User对象的属性,这样就能在实际开发中使逻辑关系更加清晰,代码更加简洁了。
31
32 BaseModel.metadata.create_all(engine)
33 user=User(name='first',username='pan',passwd='123456')
34 session.add(user)
35 session.flush()
36 blog = Blog(title='frist', user=user.id)
37 session.add(blog)
38 session.commit()

关系只是 SQLAlchemy 提供的工具, 与数据库无关, 所以任何时候添加都是可以的.

上面的 User-Blog 是一个"一对多"关系, 通过 Bloguser 这个 ForeignKey , SQLAlchemy 可以自动处理关系的定义. 在查询时, 返回的结果自然也是, 一个是列表, 一个是单个对象:

1 ret=session.query(Blog).get(1).user_obj
2 ret1=session.query(User).get(1).blog_list
3 print(ret.passwd,ret1[0].id)#ret.passwd=User.passwd  ret1[0].id=Blog.user列表中第一个值
#blog_list=relationship('Blog',backref='Blog.user')
#user_obj=relationship('User')
#relationship指定对象不同引用方法不同

这种关系的定义, 并不影响查询并获取对象的行为, 不会添加额外的 join 操作. 在对象上取一个user_obj 或者取 blog_list 都是发生了一个新的查询操作.

上面的关系定义, 对应的属性是实际查询出的实例列表, 当条目数多的时候, 这样可能会有问题. 比如用户名下有成千上万的文章, 一次全取出就太暴力了. 关系对应的属性可以定义成一个 Query :

1 class User(BaseModel):
2     __tablename__ = 'user'
3
4     id = Column(BIGINT, primary_key=True, autoincrement=True)
5     name = Column(String(32), server_default='', nullable=False)
6
7     blog_list = relationship('Blog', order_by='Blog.user', lazy="dynamic")

这样在获取实例时就可以自由控制了:

1 session.query(User).get(1).blog_list.all()

2 session.query(User).get(1).blog_list.filter(Blog.title == '1').first()

 

 

回到顶部

4.3. 关系的查询

关系定义之后, 除了在查询时会有自动关联的效果, 在作查询时, 也可以对定义的关系做操作:

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3
 4     id = Column(Integer, autoincrement=True, primary_key=True)
 5     title = Column(Unicode(32), server_default='')
 6     user = Column(Integer, ForeignKey('user.id'), index=True)
 7
 8     user_obj = relationship('User')
 9
10
11 class User(BaseModel):
12     __tablename__ = 'user'
13
14     id = Column(Integer, autoincrement=True, primary_key=True)
15     name = Column(Unicode(32), server_default='')
16
17     blogs = relationship('Blog')

对于 一对多 的关系, 使用 any() 函数查询:

user = session.query(User).filter(User.blogs.any(Blog.title == 'first')).first()

SQLAlchemy 会使用 exists 条件, 类似于:

1 SELECT *FROM user WHERE EXISTS

2     (SELECT 1 FROM blog  WHERE user.id = blog.user AND blog.title = ?)  LIMIT ? OFFSET ?

反之, 如果是 多对一 的关系, 则使用 has() 函数查询:

blog = session.query(Blog).filter(Blog.user_obj.has(User.name == 'pan')).first()

最后的 SQL 语句都是一样的.

回到顶部

4.4. 关系的获取形式

前面介绍的关系定义中, 提到了两种关系的获取形式, 一种是:

user_obj = relationship('User')

这种是在对象上获取关系对象时, 再去查询.

另一种是:

blog_list = relationship('Blog', lazy="dynamic")

这种的结果, 是在对象上获取关系对象时, 只返回 Query , 而查询的细节由人为来控制.

总的来说, 关系的获取分成两种, LazyEager . 在直接查询层面, 上面两种都属于 Lazy 的方式, 而 Eager 的一种, 就是在获取对象时的查询语句, 是直接带 join 的, 这样关系对象的数据在一个查询语句中就直接获取到了:

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 7
 8     user_obj = relationship('User', lazy='joined', cascade='all')
 9
10
11 class User(BaseModel):
12     __tablename__ = 'user'
13
14     id = Column(BIGINT, primary_key=True, autoincrement=True)
15     name = Column(String(32), server_default='', nullable=False)

这样在查询时:

blog = session.query(Blog).first()

print( blog.user_obj)

便会多出 LEFT OUTER JOIN 的语句, 结果中直接获取到对应的 User 实例对象.

也可以把 joined 换成子查询, subquery :

class User(BaseModel):
    __tablename__ = 'user'
    id = Column(BIGINT, primary_key=True, autoincrement=True)
    name = Column(String(32), server_default='', nullable=False)
    blog_list = relationship('Blog', cascade='all', lazy='subquery')
if __name__ == '__main__':
    session = Session()
    user = session.query(User).first()
    session.commit()

子查询会用到临时表.

上面定义的:

1 blog_list = relationship('Blog', lazy="dynamic")

2 user_obj = relationship('User', lazy='joined')

3 blog_list = relationship('Blog', lazy='subquery')

都算是一种默认方式. 在具体使用查询时, 还可以通过 options() 方法定义关联的获取方式:

from sqlalchemy.orm import lazyload, joinedload, subqueryload

user = session.query(User).options(lazyload('blog_list')).first()

print (user.blog_list)

更多的用法:

1 session.query(Parent).options(
 2     joinedload('foo').joinedload('bar').joinedload('bat')
 3     ).all()
 4
 5 session.query(A).options(
 6     defaultload("atob").joinedload("btoc")
 7     ).all()
 8
 9 session.query(MyClass).options(lazyload('*'))
10
11 session.query(MyClass).options(
12     lazyload('*'), joinedload(MyClass.widget)
13     )
14
15 session.query(User, Address).options(Load(Address).lazyload('*'))

如果关联的定义之前是 Lazy 的, 但是实际使用中, 希望在手工 join 之后, 把关联对象直接包含进结果实例, 可以使用 contains_eager() 来包装一下:

1 from sqlalchemy.orm import contains_eager

2

3 blog = session.query(Blog).join(Blog.user_obj)\

4              .options(contains_eager(Blog.user_obj)).first()

5 print blog.user_obj

回到顶部

4.5. 关系的表现形式

关系在对象属性中的表现, 默认是列表, 但是, 这不是唯一的形式. 根据需要, 可以作成 dictionary , set 或者其它你需要的对象.

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3
 4     id = Column(Integer, autoincrement=True, primary_key=True)
 5     title = Column(Unicode(32), server_default='')
 6     user = Column(Integer, ForeignKey('user.id'), index=True)
 7
 8     user_obj = relationship('User')
 9
10
11 class User(BaseModel):
12     __tablename__ = 'user'
13
14     id = Column(Integer, autoincrement=True, primary_key=True)
15     name = Column(Unicode(32), server_default='')
16
17     blogs = relationship('Blog')

对于上面的两个模型:

user = session.query(User).first()

print (user.blogs)

现在 user.blogs 是一个列表. 我们可以在 relationship() 调用时通过 collection_class 参数指定一个类, 来重新定义关系的表现形式:

1 user = User(name='XXX')
2 session.add_all([Blog(title='A', user_obj=user), Blog(title='B', user_obj=user)])
3 session.commit()
4
5 user = session.query(User).first()
6 print (user.blogs)

set , 集合:

blogs = relationship('Blog', collection_class=set)


#InstrumentedSet([<__main__.Blog object at 0x1a58710>, <__main__.Blog object at 0x1a587d0>])

attribute_mapped_collection , 字典, 键值从属性取:

1 from sqlalchemy.orm.collections import attribute_mapped_collection
2
3 blogs = relationship('Blog', collection_class=attribute_mapped_collection('title'))
4
5 #{'A': <__main__.Blog object at 0x20ed810>, 'B': <__main__.Blog object at 0x20ed8d0>}

如果 title 重复的话, 结果会覆盖.mapped_collection , 字典, 键值自定义:

1 from sqlalchemy.orm.collections import mapped_collection

2

3 blogs = relationship('Blog', collection_class=mapped_collection(lambda blog: blog.title.lower()))

回到顶部

4.6. 多对多关系

先考虑典型的多对多关系结构:

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6
 7     tag_list = relationship('Tag')
 8     tag_list = relationship('BlogAndTag')
 9
10
11 class Tag(BaseModel):
12     __tablename__ = 'tag'
13
14     id = Column(BIGINT, primary_key=True, autoincrement=True)
15     name = Column(String(16), server_default='', nullable=False)
16
17
18 class BlogAndTag(BaseModel):
19     __tablename__ = 'blog_and_tag'
20
21     id = Column(BIGINT, primary_key=True, autoincrement=True)
22     blog = Column(BIGINT, ForeignKey('blog.id'), index=True)
23     tag = Column(BIGINT, ForeignKey('tag.id'), index=True)
24     create = Column(BIGINT, index=True, server_default='0')

Blog 中的:

tag_list = relationship('Tag')

显示是错误的, 因为在 Tag 中并没有外键. 而:

tag_list = relationship('BlogAndTag')

这样虽然正确, 但是 tag_list 的关系只是到达 BlogAndTag 这一层, 并没有到达我们需要的 Tag.

这种情况下, 一个多对多关系是有三张表来表示的, 在定义 relationship 时, 就需要一个secondary 参数来指明关系表:

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6
 7     tag_list = relationship('Tag', secondary=lambda: BlogAndTag.__table__)
       #是用lambda可以使后面跟的类无需提前定义,如果直接secondary=classname,则class需天气定义
 8
 9
10 class Tag(BaseModel):
11     __tablename__ = 'tag'
12
13     id = Column(BIGINT, primary_key=True, autoincrement=True)
14     name = Column(String(16), server_default='', nullable=False)
15
16
17 class BlogAndTag(BaseModel):
18     __tablename__ = 'blog_and_tag'
19
20     id = Column(BIGINT, primary_key=True, autoincrement=True)
21     blog = Column(BIGINT, ForeignKey('blog.id'), index=True)
22     tag = Column(BIGINT, ForeignKey('tag.id'), index=True)
23     create = Column(BIGINT, index=True, server_default='0')

这样, 在操作时可以直接获取到对应的实例列表:

blog = session.query(Blog).filter(Blog.title == 'a').one()

print (blog.tag_list)

访问 tag_list 时, SQLAlchemy 做的是一个普通的多表查询.

tag_list 属性同时支持赋值操作:

session = Session()

blog = session.query(Blog).filter(Blog.title == 'a').one()

blog.tag_list = [Tag(name='t1')]

session.commit()

提交时, SQLAlchemy 总是会创建 Tag , 及对应的关系 BlogAndTag .

而如果是:

1 session = Session()
2 blog = session.query(Blog).filter(Blog.title == 'a').one()
3 blog.tag_list = []
4 session.commit()
5
6 tag = session.query(Tag).filter(Tag.name == 'x').one()
7 blog.tag_list.remove(tag)
8 session.commit()

那么 SQLAlchemy 只会删除对应的关系 BlogAndTag , 不会删除实体 Tag .

如果你直接删除实体, 那么对应的关系是不会自动删除的:

1 session = Session()
2 blog = session.query(Blog).filter(Blog.title == 'a').one()
3 tag = Tag(name='ok')
4 blog.tag_list = [tag]
5 session.commit()
6
7 tag = session.query(Tag).filter(Tag.name == 'ok').one()
8 session.delete(tag)
9 session.commit()

 

回到顶部

4.7. Cascades 自动关系处理

前面提到的, 当操作关系, 实体时, 与其相关联的关系, 实体是否会被自动处理的问题, 在 SQLAlchemy 中是通过 Cascades 机制来定义和解决的. ( Cascades 这个词是来源于Hibernate .)

cascade 是一个 relationship 的参数, 其值是逗号分割的多个字符串, 以表示不同的行为. 默认值是 " save-update, merge" , 稍后会介绍每个词项的作用.

这里的所有规则介绍, 只涉及从 ParentChild , Parent 即定义 relationship的类. 不涉及backref .

cascade 所有的可选字符串项是:

  • all , 所有操作都会自动处理到关联对象上.
  • save-update , 关联对象自动添加到会话.
  • delete , 关联对象自动从会话中删除.
  • delete-orphan , 属性中去掉关联对象, 则会话中会自动删除关联对象.
  • merge , session.merge() 时会处理关联对象.
  • refresh-expire , session.expire() 时会处理关联对象.
  • expunge , session.expunge() 时会处理关联对象.

save-update当一个对象被添加进 session 后, 此对象标记为 save-updaterelationship 关系对象也会同时添加进这个 session .

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3 
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 7 
 8 
 9 class User(BaseModel):
10     __tablename__ = 'user'
11 
12     id = Column(BIGINT, primary_key=True, autoincrement=True)
13     name = Column(String(32), server_default='', nullable=False)
14 
15     blog_list = relationship('Blog', cascade='')
16     blog_list_auto = relationship('Blog', cascade='save-update')
17 
18 
19 if __name__ == '__main__':
20 
21     session = Session()
22 
23     user = User(name=u'哈哈')
24     blog = Blog(title=u'第一个')
25     user.blog_list = [blog]
26     #user.blog_list_auto = [blog]
27     session.add(user)
28     print (blog in session)
29     session.commit()

 

delete当一个对象在 session 中被标记为删除时, 其属性中 relationship 关联的对象也会被标记成删除, 否则, 关联对象中的对应外键字段会被改成 NULL , 不能为 NULL 则报错.

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3 
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 7 
 8 
 9 class User(BaseModel):
10     __tablename__ = 'user'
11 
12     id = Column(BIGINT, primary_key=True, autoincrement=True)
13     name = Column(String(32), server_default='', nullable=False)
14 
15     blog_list = relationship('Blog', cascade='save-update, delete')
16 
17 
18 if __name__ == '__main__':
19     session = Session()
20 
21     #user = User(name=u'用户')
22     #user.blog_list = [Blog(title=u'哈哈')]
23     #session.add(user)
24     user = session.query(User).first()
25     session.delete(user)
26     session.commit()

 

delete-orphanrelationship 属性变化时, 被 "去掉" 的对象会被自动删除. 比如之前是:

user.blog_list = [blog, blog2]

现在变成:

user.blog_list = [blog2]

那么 blog 这个关联实体是会自动删除的. 这各机制只适用于 "一对多" 的关系中, "多对多" 和反过来的 "多对一" 都不适用. 在relationship 定义时, 可以添加 single_parent = True 参数来强制约束. 当然, 在实现上 SQLAlchemy 是会先查出所有关联实体, 然后计算差集确认哪些需要被删除.

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3 
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 7 
 8 
 9 class User(BaseModel):
10     __tablename__ = 'user'
11 
12     id = Column(BIGINT, primary_key=True, autoincrement=True)
13     name = Column(String(32), server_default='', nullable=False)
14 
15     blog_list = relationship('Blog', cascade='save-update, delete-orphan')
16 
17 
18 if __name__ == '__main__':
19 
20     session = Session()
21 
22     #user = User(name=u'用户')
23     #blog = Blog(title=u'一')
24     #blog2 = Blog(title=u'二')
25     #user.blog_list = [blog, blog2]
26     #session.add(user)
27     user = session.query(User).first()
28     blog2 =  session.query(Blog).filter(Blog.title == u'二').first()
29     user.blog_list = [blog2]
30     #session.delete(user)
31     session.commit()

 

merge这个选项是标识在 session.merge() 时处理关联对象. session.merge() 的作用, 是把一个会话外的实例, "整合"进会话, 比如 "有则修改, 无则创建" 就是典型的一种 "整合":

1 user = User(id=1, name="1")
 2 session.add(user)
 3 session.commit()
 4 
 5 user = User(id=1)
 6 user = session.merge(user)
 7 print user.name
 8 
 9 user = User(id=1, name="2")
10 user = session.merge(user)
11 session.commit()

 

cascade 中的 merge 作用:

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3 
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 7 
 8 
 9 class User(BaseModel):
10     __tablename__ = 'user'
11 
12     id = Column(BIGINT, primary_key=True, autoincrement=True)
13     name = Column(String(32), server_default='', nullable=False)
14 
15     blog_list = relationship('Blog',
16                              cascade='save-update, delete, delete-orphan, merge')
17 
18 
19 if __name__ == '__main__':
20 
21     session = Session()
22 
23     user = User(id=1, name='1')
24     session.add(user)
25     session.commit(user)
26 
27     user = User(id=1, blog_list=[Blog(title='哈哈')])
28     session.merge(user)
29 
30     session.commit()

 

refresh-expire

1 当使用 session.expire() 标识一个对象过期时, 此对象的关联对象是否也被标识为过期(访问属性会重新查询数据库).
 2 
 3 class Blog(BaseModel):
 4     __tablename__ = 'blog'
 5 
 6     id = Column(BIGINT, primary_key=True, autoincrement=True)
 7     title = Column(String(64), server_default='', nullable=False)
 8     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 9 
10 
11 class User(BaseModel):
12     __tablename__ = 'user'
13 
14     id = Column(BIGINT, primary_key=True, autoincrement=True)
15     name = Column(String(32), server_default='', nullable=False)
16 
17     blog_list = relationship('Blog',
18             cascade='save-update, delete, delete-orphan, merge, refresh-expire')
19 
20 
21 if __name__ == '__main__':
22 
23     session = Session()
24 
25     #user = User(id=1, name='1')
26     #blog = Blog(title="abc")
27     #user.blog_list = [blog]
28     #session.add(user)
29 
30     user = session.query(User).first()
31     blog = user.blog_list[0]
32     print (user.name)
33     print (blog.title)
34     session.expire(user)
35     print ('EXPIRE')
36     print (user.name)
37     print (blog.title)
38 
39     session.commit()

 

expungemerge 相反, 当 session.expunge() 把对象从会话中去除的时候, 此对象的关联对象也同时从会话中消失.

1 class Blog(BaseModel):
 2     __tablename__ = 'blog'
 3
 4     id = Column(BIGINT, primary_key=True, autoincrement=True)
 5     title = Column(String(64), server_default='', nullable=False)
 6     user = Column(BIGINT, ForeignKey('user.id'), index=True, nullable=False)
 7
 8
 9 class User(BaseModel):
10     __tablename__ = 'user'
11
12     id = Column(BIGINT, primary_key=True, autoincrement=True)
13     name = Column(String(32), server_default='', nullable=False)
14
15     blog_list = relationship('Blog', cascade='delete, delete-orphan, expunge')
16
17
18 if __name__ == '__main__':
19
20     session = Session()
21     user = User(name=u'用户')
22     blog = Blog(title=u'第一个')
23     user.blog_list = [blog]
24
25     session.add(user)
26     session.add(blog)
27
28     session.expunge(user)
29     print (blog in session)
30
31     #session.commit()

 

4.8. 属性代理考虑这样的情况, 关系是关联的整个模型对象的, 但是, 有时我们对于这个关系, 并不关心整个对象, 只关心其中的某个属性. 考虑下面的场景:

1 from sqlalchemy.ext.associationproxy import association_proxy
 2 
 3 class Blog(BaseModel):
 4     __tablename__ = 'blog'
 5 
 6     id = Column(Integer, autoincrement=True, primary_key=True)
 7     title = Column(Unicode(32), nullable=False, server_default='')
 8     user = Column(Integer, ForeignKey('user.id'), index=True)
 9 
10 
11 class User(BaseModel):
12     __tablename__ = 'user'
13 
14     id = Column(Integer, autoincrement=True, primary_key=True)
15     name = Column(Unicode(32), nullable=False, server_default='')
16 
17     blog_list = relationship('Blog')
18     blog_title_list = association_proxy('blog_list', 'title')

blog_list 是一个正确的一对多关系. 下面的 blog_title_list 就是这个关系上的一个属性代理.blog_title_list 只处理 blog_list 这个关系中对应的对象的 title 属性, 包括获取和设置两个方向.

1 session = Session()
2 
3 user = User(name='xxx')
4 user.blog_list = [Blog(title='ABC')]
5 session.add(user)
6 session.commit()
7 
8 user = session.query(User).first()
9 print (user.blog_title_list)

上面是获取属性的示例. 在"设置", 或者说"创建"时, 直接操作是有错的:

1 user = session.query(User).first()

2 user.blog_title_list = ['NEW']

3 session.add(user)

4 session.commit()

原因在于, 对于类 Blog 的初始化形式. association_proxy('blog_list', 'title') 中的 title只是获取时的属性定义, 而在上面的设置过程中, 实际上的调用形式为:

Blog('NEW')

Blog 类没有明确定义 __init__() 方法, 所有这种形式的调用会报错. 可以把 __init__() 方法补上:这样调用就没有问题了.

1 class Blog(BaseModel):
2     __tablename__ = 'blog'
3 
4     id = Column(Integer, autoincrement=True, primary_key=True)
5     title = Column(Unicode(32), nullable=False, server_default='')
6     user = Column(Integer, ForeignKey('user.id'), index=True)
7 
8     def __init__(self, title):
9         self.title = title

另一个方法, 是在调用 association_proxy() 时使用 creator 参数明确定义"值"和"实例"的关系:

1 class User(BaseModel):
2     __tablename__ = 'user'
3 
4     id = Column(Integer, autoincrement=True, primary_key=True)
5     name = Column(Unicode(32), nullable=False, server_default='')
6 
7     blog_list = relationship('Blog')
8     blog_title_list = association_proxy('blog_list', 'title',
9                                         creator=lambda t: User(title=t))

creator 定义的方法, 返回的对象可以被对应的 blog_list 关系接收即可.在查询方面, 多对一 的关系代理上, 可以直接使用属性:、

1 class Blog(BaseModel):
2     __tablename__ = 'blog'
3 
4     id = Column(Integer, autoincrement=True, primary_key=True)
5     title = Column(Unicode(32), server_default='')
6     user = Column(Integer, ForeignKey('user.id'), index=True)
7 
8     user_obj = relationship('User')
9     user_name = association_proxy('user_obj', 'name')

查询:

blog = session.query(Blog).filter(Blog.user_name == 'XX').first()

反过来的 一对多 关系代理上, 可以使用 contains() 函数:

user = session.query(User).filter(User.blogs_title.contains('A')).first()

 

5. SQLAlchemy会话与事务控制

回到顶部

5.1. 基本使用

SQLAlchemy 的 session 是用于管理数据库操作的一个像容器一样的东西. 模型实例对象本身独立存在, 而要让其修改(创建)生效, 则需要把它们加入某个 session . 同时你也可以把模型实例对象从 session 中去除. 被 session 管理的实例对象, 在 session.commit() 时被提交到数据库. 同时 session.rollback() 是回滚变更.

session.flush() 的作用是在事务管理内与数据库发生交互, 对应的实例状态被反映到数据库. 比如自增 ID 被填充上值.

1 user = User(name='名字')

2 session.add(user)

3 session.commit()

1 try:
2     user = session.Query(User).first()
3     user.name = u'改名字
4     session.commit()
5 except:
6     session.rollback()

回到顶部

5.2. for update

SQLAlchemy 的 Query 支持 select ... for update / share .

session.Query(User).with_for_update().first()

session.Query(User).with_for_update(read=True).first()

完整形式是:

with_for_update(read=False, nowait=False, of=None)

read是标识加互斥锁还是共享锁. 当为 True 时, 即 for share 的语句, 是共享锁. 多个事务可以获取共享锁, 互斥锁只能一个事务获取. 有"多个地方"都希望是"这段时间我获取的数据不能被修改, 我也不会改", 那么只能使用共享锁.nowait其它事务碰到锁, 是否不等待直接"报错".of指明上锁的表, 如果不指明, 则查询中涉及的所有表(行)都会加锁.

回到顶部

5.3. 事务嵌套

SQLAlchemy 中的事务嵌套有两种情况. 一是在 session 中管理的事务, 本身有层次性. 二是 session 和原始的 connection 之间, 是一种层次关系, 在这 session , connection 两个概念之中的事务同样具有这样的层次.

session 中的事务, 可能通过 begin_nested() 方法做 savepoint :

1 session.add(u1)
2 session.add(u2)
3
4 session.begin_nested()
5 session.add(u3)
6 session.rollback() # rolls back u3, keeps u1 and u2
7
8 session.commit()

或者使用上下文对象:

1 for record in records:
2     try:
3         with session.begin_nested():
4             session.merge(record)
5     except:
6         print "Skipped record %s" % record
7 session.commit()

嵌套的事务的一个效果, 是最外层事务提交整个变更才会生效.

1 user = User(name='2')
2
3 session.begin_nested()
4 session.add(user)
5 session.commit()
6
7 session.rollback()

于是, 前面说的第二种情况有一种应用方式, 就是在 connection 上做一个事务, 最终也在 connection 上回滚这个事务, 如果 session 是 bind 到这个连接上的, 那么 session 上所做的更改全部不会生效:

1 conn = Engine.connect()
 2 session = Session(bind=conn)
 3 trans = conn.begin()
 4
 5 user = User(name='2')
 6 session.begin_nested()
 7 session.add(user)
 8 session.commit()
 9
10 session.commit()
11
12 trans.rollback()

在测试中这种方式可能会有用.

回到顶部

5.4. 二段式提交

二段式提交, Two-Phase, 是为解决分布式环境下多点事务控制的一套协议.

与一般事务控制的不同是, 一般事务是 begin, 之后 commit 结束.

而二段式提交的流程上, begin 之后, 是 prepare transaction 'transaction_id' , 这时相关事务数据已经持久化了. 之后, 再在任何时候(哪怕重启服务), 作 commit prepared 'transaction_id' 或者 rollback prepared 'transaction_id' .

从多点事务的控制来看, 应用层要做的事是, 先把任务分发出去, 然后收集"事务准备"的状态(prepare transaction 的结果). 根据收集的结果决定最后是 commit 还是 rollback .

简单来说, 就是事务先保存, 再说提交的事.

SQLAlchemy 中对这个机制的支持, 是在构建会话类是加入 twophase 参数:

Session = sessionmaker(twophase=True)

然后会话类可以根据一些策略, 绑定多个 Engine , 可以是多个数据库连接, 比如:

Session = sessionmaker(twophase=True)

Session.configure(binds={User: Engine, Blog: Engine2})

这样, 在获取一个会话实例之后, 就处在二段式提交机制的支持之下, SQLAlchemy 自己会作多点的协调了. 完整的流程:

1 Engine = create_engine('postgresql://test@localhost:5432/test', echo=True)
 2 Engine2 = create_engine('postgresql://test@localhost:5432/test2', echo=True)
 3
 4 Session = sessionmaker(twophase=True)
 5
 6 Session.configure(binds={User: Engine, Blog: Engine2})
 7 session = Session()
 8
 9 user = User(name='名字')
10 session.add(user)
11 session.commit()

对应的 SQL 大概就是:

1 begin;

2 insert into "user" (name) values (?);

3 prepare transaction 'xx';

4 commit prepared 'xx';

使用时, Postgresql 数据库需要把 max_prepared_transactions 这个配置项的值改成大于 0

 

 

6. SQLAlchemy字段类型

回到顶部

6.1. 基本类型

字段类型是在定义模型时, 对每个 Column 的类型约定. 不同类型的字段类型在输入输出上, 及支持的操作方面, 有所区别.

这里只介绍 sqlalchemy.types.* 中的类型, SQL 标准类型方面, 是写什么最后生成的 DDL 语句就是什么, 比如 BIGINT, BLOG 这些, 但是这些类型并不一定在所有数据库中都有支持. 除此而外, SQLAlchemy 也支持一些特定数据库的特定类型, 这些需要从具体的 dialects 实现里导入.

Integer/BigInteger/SmallInteger整形.Boolean布尔类型. Python 中表现为 True/False , 数据库根据支持情况, 表现为 BOOLEANSMALLINT . 实例化时可以指定是否创建约束(默认创建).Date/DateTime/Time (timezone=False)日期类型, TimeDateTime 实例化时可以指定是否带时区信息.Interval时间偏差类型. 在 Python 中表现为 datetime.timedelta() , 数据库不支持此类型则存为日期.Enum (*enums, **kw)枚举类型, 根据数据库支持情况, SQLAlchemy 会使用原生支持或者使用 VARCHAR 类型附加约束的方式实现. 原生支持中涉及新类型创建, 细节在实例化时控制.Float浮点小数.Numeric (precision=None, scale=None, decimal_return_scale=None, ...)定点小数, Python 中表现为 Decimal .LargeBinary (length=None)字节数据. 根据数据库实现, 在实例化时可能需要指定大小.PickleTypePython 对象的序列化类型.String (length=None, collation=None, ...)字符串类型, Python 中表现为 Unicode , 数据库表现为 VARCHAR , 通常都需要指定长度.Unicode类似与字符串类型, 在某些数据库实现下, 会明确表示支持非 ASCII 字符. 同时输入输出也强制是 Unicode 类型.Text长文本类型, Python 表现为 Unicode , 数据库表现为 TEXT .UnicodeText参考 Unicode .

 

 

7. SQLAlchemy混合属性机制

回到顶部

7.1. 直接行为

混合属性, 官方文档中称之为 Hybrid Attributes . 这种机制表现为, 一个属性, 在类和层面, 和实例的层面, 其行为是不同的. 之所以需要关注这部分的差异, 原因源于 Python 上下文和 SQL 上下文的差异.

类 层面经常是作为 SQL 查询时的一部分, 它面向的是 SQL 上下文. 而实例是已经得到或者创建的结果, 它面向的是 Python 上下文.

定义模型的 Column() 就是一个典型的混合属性. 作为实例属性时, 是具体的对象值访问, 而作为类属性时, 则有构成 SQL 语句表达式的功能.

1 class Interval(BaseModel):
2     __tablename__ = 'interval'
3
4     id = Column(Integer, autoincrement=True, primary_key=True)
5     start = Column(Integer)
6     end = Column(Integer)
7
8 session.add(Interval(start=0, end=100))
9 session.commit()

实例行为:

ins = session.query(Interval).first()

print (ins.end - ins.start)

类行为:

ins = session.query(Interval).filter(Interval.end - Interval.start > 10).first()

这种机制其实一直在被使用, 但是可能大家都没有留意一个属性在类和实例上的区别.

如果属性需要被进一步封装, 那么就需要明确声明 Hybrid Attributes 了:

1 from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
 2
 3 class Interval(BaseModel):
 4     __tablename__ = 'interval'
 5
 6     id = Column(Integer, autoincrement=True, primary_key=True)
 7     start = Column(Integer)
 8     end = Column(Integer)
 9
10     @hybrid_property
11     def length(self):
12         return self.end - self.start
13
14     @hybrid_method
15     def bigger(self, i):
16         return self.length > i
17
18
19 session.add(Interval(start=0, end=100))
20 session.commit()
21
22 ins = session.query(Interval).filter(Interval.length > 10).first()
23 ins = session.query(Interval).filter(Interval.bigger(10)).first()
24 print( ins.bigger(1))

setter 的定义同样使用对应的装饰器即可:

1 class Interval(BaseModel):
 2     __tablename__ = 'interval'
 3
 4     id = Column(Integer, autoincrement=True, primary_key=True)
 5     start = Column(Integer)
 6     end = Column(Integer)
 7
 8     @hybrid_property
 9     def length(self):
10         return abs(self.end - self.start)
11
12     @length.setter
13     def length(self, l):
14         self.end = self.start + l

回到顶部

7.2. 表达式行为

前面说的属性, 在类和实例上有不同行为, 可以看到, 在类上的行为, 其实就是生成 SQL 表达式时的行为. 上面的例子只是简单的运算, SQLAlchemy 可以自动处理好 Python 函数和 SQL 函数的区别. 但是如果是一些特性更强的 SQL 函数, 就需要手动指定了. 于时, 这时的情况变成, 实例行为是 Python 范畴的调用行为, 而类行为则是生成 SQL 函数的相关表达式.

同时是前面的例子, 对于 length 的定义, 更严格上来说, 应该是取绝对值的.

1 class Interval(BaseModel):
 2     __tablename__ = 'interval'
 3
 4     id = Column(Integer, autoincrement=True, primary_key=True)
 5     start = Column(Integer)
 6     end = Column(Integer)
 7
 8     @hybrid_property
 9     def length(self):
10         return abs(self.end - self.start)

但是, 如果使用了 Python 的 abs() 函数, 在生成 SQL 表达式时显示有无法处理了. 所以, 需要手动定义:

1 from sqlalchemy import func
 2
 3 class Interval(BaseModel):
 4     __tablename__ = 'interval'
 5
 6     id = Column(Integer, autoincrement=True, primary_key=True)
 7     start = Column(Integer)
 8     end = Column(Integer)
 9
10     @hybrid_property
11     def length(self):
12         return abs(self.end - self.start)
13
14     @length.expression
15     def length(self):
16         return func.abs(self.end - self.start)

这样查询时就可以直接使用:

ins = session.query(Interval).filter(Interval.length > 1).first()

回到顶部

7.3. 应用于关系

总体上没有特别之处:

1 class Account(BaseModel):
 2     __tablename__ = 'account'
 3
 4     id = Column(Integer, autoincrement=True, primary_key=True)
 5     user = Column(Integer, ForeignKey('user.id'), index=True)
 6     balance = Column(Integer, server_default='0')
 7
 8
 9 class User(BaseModel):
10     __tablename__ = 'user'
11
12     id = Column(Integer, autoincrement=True, primary_key=True)
13     name = Column(Unicode(32), nullable=False, server_default='')
14
15     accounts = relationship('Account')
16     #balance = association_proxy('accounts', 'balance')
17
18     @hybrid_property
19     def balance(self):
20         return sum(x.balance for x in self.accounts)

查询时:

user = session.query(User).first()

print (user.balance)

这里涉及的东西都是 Python 自己的, 包括那个 sum() 函数, 和 SQL 没有关系.

如果想实现的是, 使用 SQL 的 sum() 函数, 取出指定用户的总账户金额数, 那么就要考虑把balance 作成表达式的形式:

1 from sqlalchemy import select
2
3 @hybrid_property
4 def balance(self):
5     return select([func.sum(Account.balance)]).where(Account.user == self.id).label('balance_v')
6     #return func.sum(Account.balance)

这样的话, User.balance 只是单纯的一个表达式了, 查询时指定字段:

user = session.query(User, User.balance).first()

print (user.balance_v)

注意, 如果写成:

session.query(User.balance).first()

意义就不再是"获取第一个用户的总金额", 而变成"获取总金额的第一个". 这里很坑吧.

像上面这样改, 实例层面就无法使用 balance 属性. 所以, 还是先前介绍的, 表达式可以单独处理:

1 @hybrid_property
2 def balance(self):
3     return sum(x.balance for x in self.accounts)
4
5 @balance.expression
6 def balance(self):
7     return select([func.sum(Account.balance)]).where(Account.user == self.id).label('balance_v')

定义了表达式的 balance , 这部分作为查询条件上当然也是可以的:

user = session.query(User).filter(User.balance > 1).first()

 

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
SQL 关系型数据库 数据库连接
框架分析(10)-SQLAlchemy
框架分析(10)-SQLAlchemy
|
4月前
|
测试技术 API 数据库
SqlAlchemy 2.0 中文文档(十)(4)
SqlAlchemy 2.0 中文文档(十)
64 1
|
4月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(十)(5)
SqlAlchemy 2.0 中文文档(十)
32 1
|
5月前
|
SQL 存储 Go
【译】SQLAlchemy文档:SQLAlchemy 统一教程
【译】SQLAlchemy文档:SQLAlchemy 统一教程
|
4月前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(三)(3)
SqlAlchemy 2.0 中文文档(三)
53 0
|
5月前
|
SQL 关系型数据库 MySQL
SQLAlchemy使用指南
**SQLAlchemy 指南**:Python SQL 工具包,提供数据库高级抽象。安装:`pip install sqlalchemy`,加上数据库驱动(如 MySQL: `pip install mysql-connector-python`)。基础使用包括:创建数据库连接、定义模型、创建表、添加/查询/更新/删除数据。高级功能涉及关系映射、原生 SQL 语句及 SQLAlchemy Core。推荐阅读官方文档以深入了解。
143 1
|
4月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(六)(2)
SqlAlchemy 2.0 中文文档(六)
35 0
|
存储 SQL 数据库连接
SQLAlchemy常用数据类型
SQLAlchemy常用数据类型
|
SQL 存储 关系型数据库
【flask-sqlalchemy】SQLAlchemy+PyMysql到mysql的映射
【flask-sqlalchemy】SQLAlchemy+PyMysql到mysql的映射
203 0
|
API Python
Python编程:orm之sqlalchemy模块
Python编程:orm之sqlalchemy模块
168 0