为何SQLAlchemy关联属性的两种访问方式返回结果不同?
问题重现
我在SQLAlchemy的User模型里定义了两种关联关系:一种直接作为类属性,另一种放在类的字典属性中。本以为两种方式都能正常返回关联数据,但实际测试结果却完全不同:
模型代码
class User(db.Model): id = db.Column(db.Integer, primary_key) # 其他属性省略 # 直接定义的多对多关联 sz_shirt_dress_sleeve = db.relationship( 'SizeKeyShirtDressSleeve', secondary=LinkUserSizeShirtDressSleeve, backref=db.backref('users', lazy='dynamic') ) # 放在字典里的关联 strange = { 'whyisthisdifferent': db.relationship( 'SizeKeyShirtDressNeck', secondary=LinkUserSizeShirtDressNeck, backref=db.backref('users', lazy='dynamic') ) }
测试会话
>>> from app import db >>> from app.models import User >>> u1 = User.query.first() >>> u1.sz_shirt_dress_sleeve # 正常返回关联数据 [Dress shirt sleeve size: 3000] >>> u1.strange['whyisthisdifferent'] # 返回了RelationshipProperty对象 <RelationshipProperty at 0x106c6d248; no key>
我知道可以用Association Proxy实现字典式的关联访问,但还是想搞清楚这种行为背后的原因。
核心原因:SQLAlchemy的模型元类只处理直接类属性
这一切要从SQLAlchemy的声明式模型机制说起:
当你定义继承自db.Model的类时,背后的DeclarativeMeta元类会在类定义阶段做一系列工作:
- 扫描所有直接挂载在类上的属性
- 对其中的
Column、relationship等特殊对象进行包装,把它们转换成实例属性代理
比如sz_shirt_dress_sleeve = db.relationship(...),这个db.relationship()返回的原本是RelationshipProperty对象,但元类会把它替换成一个代理属性。当你访问实例的sz_shirt_dress_sleeve时,这个代理会自动触发数据库查询,返回实际的关联数据集合(列表或动态查询对象)。
而放在strange字典里的db.relationship()结果,完全不在元类的扫描范围内——元类只会处理类的直接属性,不会递归解析字典里的内容。所以当你访问u1.strange['whyisthisdifferent']时,拿到的只是原始的RelationshipProperty对象,没有经过任何代理包装,自然不会返回查询结果。
你联想到JavaScript的函数定义和字面量区别是有点道理的,但这里本质是Python元类的属性处理逻辑,SQLAlchemy只对“顶级”的类属性做特殊处理。
实现字典式关联访问的可行方案
如果确实需要字典形式的关联访问,可以用以下两种方式:
方案1:使用SQLAlchemy的Association Proxy
这是官方推荐的方式,专门用来处理这类间接关联的场景:
from sqlalchemy.ext.associationproxy import association_proxy class User(db.Model): id = db.Column(db.Integer, primary_key) # 先定义基础的关联关系 _neck_sizes = db.relationship( 'SizeKeyShirtDressNeck', secondary=LinkUserSizeShirtDressNeck, backref=db.backref('users', lazy='dynamic') ) # 用association proxy模拟字典访问 strange = association_proxy('_neck_sizes', lambda x: x) # 如果需要多个键,可以定义多个proxy,或者自定义一个字典类属性
方案2:自定义@property方法
用Python的属性装饰器手动封装字典:
class User(db.Model): id = db.Column(db.Integer, primary_key) sz_shirt_dress_sleeve = db.relationship(...) _neck_sizes = db.relationship(...) @property def strange(self): return { 'whyisthisdifferent': self._neck_sizes }
这样访问u1.strange['whyisthisdifferent']就能得到和直接定义属性一样的查询结果了。
内容的提问来源于stack exchange,提问作者Lirum

