外键约束
使用 ForeignKey 来定义一个外键约定:
1 | from sqlalchemy import Column, ForeignKey |
创建时:
1 | session = Session() |
session.flush()
是进行数据库交互, 但是事务并没有提交. 进行数据库交互之后,user.id
才有值.
定义了外键, 对查询来说, 并没有影响. 外键只是单纯的一条约束而已. 当然, 可以在外键上定义一些关联的事件操作, 比如当外键条目被删除时, 字段置成null
, 或者关联条目也被删除等.
关系定义
要定义关系, 必有使用 ForeignKey 约束. 当然, 这里说的只是在定义模型时必有要有, 至于数据库中是否真有外键约定, 这并不重要.
1 | from sqlalchemy import Column, ForeignKey |
关系只是 SQLAlchemy 提供的工具, 与数据库无关, 所以任何时候添加都是可以的.
上面的 User-Blog 是一个”一对多”关系, 通过Blog
的user
这个ForeignKey
, SQLAlchemy 可以自动处理关系的定义. 在查询时, 返回的结果自然也是, 一个是列表, 一个是单个对象:
1 | session = Session() |
这种关系的定义, 并不影响查询并获取对象的行为, 不会添加额外的join
操作. 在对象上取一个user_obj 或者取blog_list
都是发生了一个新的查询操作.
上面的关系定义, 对应的属性是实际查询出的实例列表, 当条目数多的时候, 这样可能会有问题. 比如用户名下有成千上万的文章,一次全取出就太暴力了. 关系对应的属性可以定义成一个QuerySet
:
1 | class User(BaseModel): |
这样在获取实例时就可以自由控制了:
1 | session.query(User).get(1).blog_list.all() |
关系的查询
关系定义之后, 除了在查询时会有自动关联的效果, 在作查询时, 也可以对定义的关系做操作:
1 | class Blog(BaseModel): |
对于 一对多 的关系, 使用any()
函数查询:
1 | user = session.query(User).filter(User.blogs.any(Blog.title == u'A')).first() |
SQLAlchemy 会使用exists
条件, 类似于:
1 | SELECT * |
反之, 如果是 多对一 的关系, 则使用has()
函数查询:
1 |
|
最后的 SQL 语句都是一样的.
关系的获取形式
前面介绍的关系定义中, 提到了两种关系的获取形式, 一种是:
1 | user_obj = relationship('User') |
这种是在对象上获取关系对象时, 再去查询.
另一种是:
1 | blog_list = relationship('Blog', lazy="dynamic") |
这种的结果, 是在对象上获取关系对象时, 只返回 Query , 而查询的细节由人为来控制.
总的来说, 关系的获取分成两种,Lazy
或Eager
. 在直接查询层面, 上面两种都属于Lazy
的方式, 而Eager
的一种, 就是在获取对象时的查询语句, 是直接带join
的, 这样关系对象的数据在一个查询语句中就直接获取到了:
1 | class Blog(BaseModel): |
这样在查询时:
1 | blog = session.query(Blog).first() |
便会多出LEFT OUTER JOIN
的语句, 结果中直接获取到对应的User
实例对象.
也可以把joined
换成子查询,subquery
:
1 | class User(BaseModel): |
子查询会用到临时表.
上面定义的:
1 | blog_list = relationship('Blog', lazy="dynamic") |
都算是一种默认方式. 在具体使用查询时, 还可以通过 options() 方法定义关联的获取方式:
1 | from sqlalchemy.orm import lazyload, joinedload, subqueryload |
更多的用法:
1 | session.query(Parent).options( |
如果关联的定义之前是 Lazy 的, 但是实际使用中, 希望在手工join
之后, 把关联对象直接包含进结果实例, 可以使用contains_eager()
来包装一下:
1 | from sqlalchemy.orm import contains_eager |
关系的表现形式
关系在对象属性中的表现, 默认是列表, 但是, 这不是唯一的形式. 根据需要, 可以作成 dictionary , set 或者其它你需要的对象.
1 | class Blog(BaseModel): |
对于上面的两个模型:
1 | user = session.query(User).first() |
现在user.blogs
是一个列表. 我们可以在relationship()
调用时通过collection_class
参数指定一个类, 来重新定义关系的表现形式:
1 | user = User(name=u'XX') |
set集合
1 | blogs = relationship('Blog', collection_class=set) |
attribute_mapped_collection , 字典, 键值从属性取
1 | from sqlalchemy.orm.collections import attribute_mapped_collection |
如果title
重复的话, 结果会覆盖.
mapped_collection , 字典, 键值自定义
1 | from sqlalchemy.orm.collections import mapped_collection |
多对多关系
先考虑典型的多对多关系结构:
1 | class Blog(BaseModel): |
在 Blog 中的:
1 | tag_list = relationship('Tag') |
显示是错误的, 因为在 Tag 中并没有外键. 而:
1 | tag_list = relationship('BlogAndTag') |
这样虽然正确, 但是 tag_list 的关系只是到达 BlogAndTag 这一层, 并没有到达我们需要的 Tag.
这种情况下, 一个多对多关系是有三张表来表示的, 在定义 relationship 时, 就需要一个secondary 参数来指明关系表:
1 | class Blog(BaseModel): |
这样, 在操作时可以直接获取到对应的实例列表:
1 | blog = session.query(Blog).filter(Blog.title == 'a').one() |
访问 tag_list 时, SQLAlchemy 做的是一个普通的多表查询.
tag_list 属性同时支持赋值操作:
1 | session = Session() |
提交时, SQLAlchemy 总是会创建 Tag , 及对应的关系 BlogAndTag .
而如果是:
1 | session = Session() |
那么 SQLAlchemy 只会删除对应的关系 BlogAndTag , 不会删除实体 Tag .
如果你直接删除实体, 那么对应的关系是不会自动删除的:
1 | session = Session() |
Cascades 自动关系处理
前面提到的, 当操作关系, 实体时, 与其相关联的关系, 实体是否会被自动处理的问题, 在 SQLAlchemy 中是通过Cascades
机制来定义和解决的. ( Cascades 这个词是来源于Hibernate .)
cascade 是一个relationship
的参数, 其值是逗号分割的多个字符串, 以表示不同的行为. 默认值是 save-update, merge
,稍后会介绍每个词项的作用.
这里的所有规则介绍, 只涉及从Parent
到Child
, Parent
即定义relationship
的类. 不涉及backref
.
cascade 所有的可选字符串项是:
- all , 所有操作都会自动处理到关联对象上.
- save-update , 关联对象自动添加到会话.
- delete , 关联对象自动从会话中删除.
- delete-orphan , 属性中去掉关联对象, 在会话中会自动删除关联对象.
- merge , session.merge() 时会处理关联对象.
- refresh-expire , session.expire() 时会处理关联对象.
- expunge , session.expunge() 时会处理关联对象.
save-update
当一个对象被添加进 session 后, 此对象标记为 save-update 的 relationship 关系对象也会同时添加进这个 session .
1 | class Blog(BaseModel): |
delete
当一个对象在 session 中被标记为删除时, 其属性中 relationship 关联的对象也会被标记成删除, 否则, 关联对象中的对应外键字段会被改成 NULL , 不能为 NULL 则报错.
1 | class Blog(BaseModel): |
delete-orphan
当 relationship 属性变化时, 被 “去掉” 的对象会被自动删除. 比如之前是:
1 | user.blog_list = [blog, blog2] |
现在变成:
1 | user.blog_list = [blog2] |
那么 blog 这个关联实体是会自动删除的.
这各机制只适用于 “一对多” 的关系中, “多对多” 和反过来的 “多对一” 都不适用. 在relationship 定义时, 可以添加 single_parent = True 参数来强制约束. 当然, 在实现上 SQLAlchemy 是会先查出所有关联实体, 然后计算差集确认哪些需要被删除.
1 | class Blog(BaseModel): |
merge
这个选项是标识在 session.merge() 时处理关联对象. session.merge() 的作用, 是把一个会话外的实例, “整合”进会话, 比如 “有则修改, 无则创建” 就是典型的一种 “整合”:
1 | user = User(id=1, name="1") |
cascade 中的 merge 作用:
1 | class Blog(BaseModel): |
refresh-expire
当使用 session.expire() 标识一个对象过期时, 此对象的关联对象是否也被标识为过期(访问属性会重新查询数据库).
1 | class Blog(BaseModel): |
expunge
与 merge 相反, 当 session.expunge() 把对象从会话中去除的时候, 此对象的关联对象也同时从会话中消失.
1 | class Blog(BaseModel): |
属性代理
考虑这样的情况, 关系是关联的整个模型对象的, 但是, 有时我们对于这个关系, 并不关心整个对象, 只关心其中的某个属性. 考虑下面的场景:
1 | from sqlalchemy.ext.associationproxy import association_proxy |
blog_list 是一个正确的一对多关系. 下面的 blog_title_list 就是这个关系上的一个属性代理.blog_title_list 只处理 blog_list 这个关系中对应的对象的 title 属性, 包括获取和设置两个方向.
1 | session = Session() |
上面是获取属性的示例. 在”设置”, 或者说”创建”时, 直接操作是有错的:
1 | user = session.query(User).first() |
原因在于, 对于类 Blog 的初始化形式. association_proxy(‘blog_list’, ‘title’) 中的 title只是获取时的属性定义, 而在上面的设置过程中, 实际上的调用形式为:
Blog(‘NEW’)
Blog 类没有明确定义 init() 方法, 所有这种形式的调用会报错. 可以把 init() 方法补上:
1 | class Blog(BaseModel): |
这样调用就没有问题了.
另一个方法, 是在调用association_proxy()
时使用creator
参数明确定义”值”和”实例”的关系:
1 | class User(BaseModel): |
creator 定义的方法, 返回的对象可以被对应的 blog_list 关系接收即可.
在查询方面, 多对一 的关系代理上, 可以直接使用属性:
1 | class Blog(BaseModel): |
查询:
1 | blog = session.query(Blog).filter(Blog.user_name == u'XX').first() |
反过来的 一对多 关系代理上, 可以使用contains()
函数:
1 | user = session.query(User).filter(User.blogs_title.contains('A')).first() |