(翻译玩)SQLALchemy backref章节文档
Linking Relationships with Backref
自从在Object Relational Tutorial中第一次提到backref参数后,许多案例中也用到了backref,那么他到底做了什么?让我们从典型的"用户-地址"模型来探究吧.
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
addresses = relationship("Address", backref="user")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
上面的代码在User建立addresses属性,可以通过User.addresses查询对应的Address对象集合的.同样也在Address中建立了用于查询User对象的user属性.
实际上,backref关键字只是一个建立关系的快捷方式,他在Address中建立了关系映射,including the establishment of an event listener on both sides which will mirror attribute operations in both directions.上面的代码等价于:
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
addresses = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship("User", back_populates="addresses")
我们明确的在Address表中添加了.user关系,在两个关系中,back_populates明确的向另一边表明另一边需要在两表之间建立一个双向的联系.
这些代码主要做的是当append事件或者set事件发生时,把自己设置到对应对象的某个属性中.下面将用User-Address来简单阐明这一行为,address属性和user属性现在为空
>>> u1 = User()
>>> a1 = Address()
>>> u1.addresses
[]
>>> print a1.user
None
然而,当Address插入到u1.addresses中时,u1.addresses属性和a1.user属性同时被添加了内容
>>> u1.addresses.append(a1)
>>> u1.addresses
[<__main__.Address object at 0x12a6ed0>]
>>> a1.user
<__main__.User object at 0x12a6590>
反过来,设置a1.user时也一样,对两边的属性设置效果是等价.例如,当设置a1.user为None时,Address对象也同样从User的addresses中移除了.
>>> a1.user = None
>>> u1.addresses
[]
对.addresses属性和.user属性的操作完全是通过python完成的,而没有通过SQL交互环境操作,不然的话the proper state would be apparent on both sides once the data has been flushed to the database, and later reloaded after a commit or expiration operation occurs. backref/back_populates的优点是操作后不需要往返数据库就可以正确的反应出正确的状态
记住,backref关键字单独的用在一边和在关系两边使用back_populates关键字,效果是完全等价的
Backref Arguments
我们已经确定了backref关键字仅仅只是建立两个单独relationship()的快捷方式,设置在一个relationship()中的barkref配置将会自动设置在另一边.也就是说,两边的关系设置不太可能有差异. 一般情况,有secondary属性的多对多关系,或者是有primaryjoin属性的一对多,多对一关系(primaryjoin属性在Specifying Alternate Join Conditions中讨论过).Such as if we limited the list of Address objects to those which start with “tony”:
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
addresses = relationship("Address",
primaryjoin="and_(User.id==Address.user_id, "
"Address.email.startswith('tony'))",
backref="user")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
We can observe, by inspecting the resulting property, that both sides of the relationship have this join condition applied:
>>> print User.addresses.property.primaryjoin
"user".id = address.user_id AND address.email LIKE :email_1 || '%%'
>>>
>>> print Address.user.property.primaryjoin
"user".id = address.user_id AND address.email LIKE :email_1 || '%%'
>>>
This reuse of arguments should pretty much do the “right thing” - it uses only arguments that are applicable, and in the case of a many-to- many relationship, will reverse the usage of primaryjoin and secondaryjoin to correspond to the other direction (see the example in Self-Referential Many-to-Many Relationship for this).
It’s very often the case however that we’d like to specify arguments that are specific to just the side where we happened to place the “backref”. This includes relationship() arguments like lazy, remote_side, cascade and cascade_backrefs. For this case we use the backref() function in place of a string:
from sqlalchemy.orm import backref
class User(Base):
tablename = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
addresses = relationship("Address",
backref=backref("user", lazy="joined"))
Where above, we placed a lazy="joined" directive only on the Address.user side, indicating that when a query against Address is made, a join to the User entity should be made automatically which will populate the .user attribute of each returned Address. The backref() function formatted the arguments we gave it into a form that is interpreted by the receiving relationship() as additional arguments to be applied to the new relationship it creates.
One Way Backrefs
An unusual case is that of the “one way backref”. This is where the “back-populating” behavior of the backref is only desirable in one direction. An example of this is a collection which contains a filtering primaryjoin condition. We’d like to append items to this collection as needed, and have them populate the “parent” object on the incoming object. However, we’d also like to have items that are not part of the collection, but still have the same “parent” association - these items should never be in the collection.
Taking our previous example, where we established a primaryjoin that limited the collection only to Address objects whose email address started with the word tony, the usual backref behavior is that all items populate in both directions. We wouldn’t want this behavior for a case like the following:
>>> u1 = User()
>>> a1 = Address(email='mary')
>>> a1.user = u1
>>> u1.addresses
[<__main__.Address object at 0x1411910>]
Above, the Address object that doesn’t match the criterion of “starts with ‘tony’” is present in the addresses collection of u1. After these objects are flushed, the transaction committed and their attributes expired for a re-load, the addresses collection will hit the database on next access and no longer have this Address object present, due to the filtering condition. But we can do away with this unwanted side of the “backref” behavior on the Python side by using two separate relationship() constructs, placing back_populates only on one side:
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
addresses = relationship("Address",
primaryjoin="and_(User.id==Address.user_id, "
"Address.email.startswith('tony'))",
back_populates="user")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship("User")
With the above scenario, appending an Address object to
the .addresses collection of a User will always establish the .user attribute on that Address:
>>> u1 = User()
>>> a1 = Address(email='tony')
>>> u1.addresses.append(a1)
>>> a1.user
<__main__.User object at 0x1411850>
However, applying a User to the .user attribute of an Address, will not append the Address object to the collection:
>>> a2 = Address(email='mary')
>>> a2.user = u1
>>> a2 in u1.addresses
False
Of course, we’ve disabled some of the usefulness of backref here, in that when we do append an Address that corresponds to the criteria of email.startswith('tony'), it won’t show up in the User.addresses collection until the session is flushed, and the attributes reloaded after a commit or expire operation. While we could consider an attribute event that checks this criterion in Python, this starts to cross the line of duplicating too much SQL behavior in Python. The backref behavior itself is only a slight transgression of this philosophy - SQLAlchemy tries to keep these to a minimum overall.