mixin 的可变列不跟踪突变
Mutable column from mixin does not track mutation
在尝试诊断我遇到的 social-app-flask-sqlalchemy
问题时,我发现 sqlalchemy
的行为有点不直观,我不确定这是预期行为还是错误。
考虑以下片段:
from sqlalchemy import create_engine, Column, Integer, PickleType
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class A(Base):
__abstract__ = True
class B(Base):
id = Column(Integer, primary_key=True)
__tablename__ = 'some_table'
my_data = Column(MutableDict.as_mutable(PickleType))
class C(A, B):
pass
engine = create_engine('sqlite://')
session_factory = sessionmaker(bind=engine)
db_session = session_factory()
Base.metadata.create_all(engine)
assert B.my_data.type.__class__ is PickleType
c_instance = C(my_data={'foo': 'bar'})
db_session.add(c_instance)
db_session.commit()
loaded_instance = db_session.query(C).first()
loaded_instance.my_data.update(baz=1)
assert loaded_instance.my_data['baz'] == 1
assert loaded_instance in db_session.dirty
这运行没有问题。现在,如果我们 将 B
的 superclass 更改为 object
,最后的断言就会出错。在此之前,一切正常。
事实证明,通过不直接 B
subclass declarative_base
,my_data
的类型不再强制为 MutableDict
,但是我们在实例化对象时给出的任何类型(在本例中为 dict
)。显然,这也意味着不再跟踪对 my_data
的更改。但是,my_data
仍然使用 PickleType
作为其数据类型,因此效果不会立即可见。
我最初是在 social-app-flask-sqlalchemy
中对 extra_data
的更改未写入数据库时偶然发现的。 social-app-flask-sqlalchemy
使用与上图类似的 ORM——包含 extra_data
列的 SQLAlchemyUserMixin
class 不是 subclassing declarative_base
,但它的子 class UserSocialAuth
通过 _AppSession
.
现在我不确定在哪里报告问题,sqlalchemy
或 social-app-flask-sqlalchemy
。有什么想法吗?
你应该declare complex mixin columns such as yours using the declared_attr
decorator。将从 mixin 中复制简单的列声明:
To accomplish this, the declarative extension creates a copy of each Column
object encountered on a class that is detected as a mixin.
显然这与 mutation tracking 配合不佳。文档对于哪些结构应该使用装饰器可能有点含糊。
This copy mechanism is limited to simple columns that have no foreign keys, as a ForeignKey
itself contains references to columns which can’t be properly recreated at this level. For columns that have foreign keys, as well as for the variety of mapper-level constructs that require destination-explicit context, the declared_attr
decorator is provided so that patterns common to many classes can be defined as callables
添加了强调,尽管我可能误解了 "destination-explicit context" 在这种情况下的含义。所以这样做:
class B(object):
...
@declared_attr
def my_data(cls):
return Column(MutableDict.as_mutable(PickleType))
相反,如果 B
是一个混入 class。失败的简单声明和 declared_attr
:
的示例
In [2]: from sqlalchemy.ext.mutable import MutableDict
In [3]: class MixinA:
...: extra = Column(MutableDict.as_mutable(PickleType))
...:
In [4]: from sqlalchemy.ext.declarative import declared_attr
In [5]: class MixinB:
...: @declared_attr
...: def extra(cls):
...: return Column(MutableDict.as_mutable(PickleType))
...:
In [6]: class A(MixinA, Base):
...: __tablename__ = 'a'
...: id = Column(Integer, primary_key=True, autoincrement=True)
...:
In [7]: class B(MixinB, Base):
...: __tablename__ = 'b'
...: id = Column(Integer, primary_key=True, autoincrement=True)
...:
In [8]: metadata.create_all()
并在行动:
In [9]: a = A(extra={})
In [10]: b = B(extra={})
In [11]: session.add(a)
In [12]: session.add(b)
In [13]: session.commit()
In [14]: session.query(A.extra).first()
Out[14]: ({})
In [15]: session.query(B.extra).first()
Out[15]: ({})
In [16]: b.extra['b'] = 1
In [17]: session.commit()
In [18]: session.query(B.extra).first()
Out[18]: ({'b': 1})
In [19]: a.extra['a'] = 1
In [20]: session.commit()
In [21]: session.query(A.extra).first()
Out[21]: ({})
In [22]: b.extra['bb'] = 2
In [23]: assert b in session.dirty
在尝试诊断我遇到的 social-app-flask-sqlalchemy
问题时,我发现 sqlalchemy
的行为有点不直观,我不确定这是预期行为还是错误。
考虑以下片段:
from sqlalchemy import create_engine, Column, Integer, PickleType
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class A(Base):
__abstract__ = True
class B(Base):
id = Column(Integer, primary_key=True)
__tablename__ = 'some_table'
my_data = Column(MutableDict.as_mutable(PickleType))
class C(A, B):
pass
engine = create_engine('sqlite://')
session_factory = sessionmaker(bind=engine)
db_session = session_factory()
Base.metadata.create_all(engine)
assert B.my_data.type.__class__ is PickleType
c_instance = C(my_data={'foo': 'bar'})
db_session.add(c_instance)
db_session.commit()
loaded_instance = db_session.query(C).first()
loaded_instance.my_data.update(baz=1)
assert loaded_instance.my_data['baz'] == 1
assert loaded_instance in db_session.dirty
这运行没有问题。现在,如果我们 将 B
的 superclass 更改为 object
,最后的断言就会出错。在此之前,一切正常。
事实证明,通过不直接 B
subclass declarative_base
,my_data
的类型不再强制为 MutableDict
,但是我们在实例化对象时给出的任何类型(在本例中为 dict
)。显然,这也意味着不再跟踪对 my_data
的更改。但是,my_data
仍然使用 PickleType
作为其数据类型,因此效果不会立即可见。
我最初是在 social-app-flask-sqlalchemy
中对 extra_data
的更改未写入数据库时偶然发现的。 social-app-flask-sqlalchemy
使用与上图类似的 ORM——包含 extra_data
列的 SQLAlchemyUserMixin
class 不是 subclassing declarative_base
,但它的子 class UserSocialAuth
通过 _AppSession
.
现在我不确定在哪里报告问题,sqlalchemy
或 social-app-flask-sqlalchemy
。有什么想法吗?
你应该declare complex mixin columns such as yours using the declared_attr
decorator。将从 mixin 中复制简单的列声明:
To accomplish this, the declarative extension creates a copy of each
Column
object encountered on a class that is detected as a mixin.
显然这与 mutation tracking 配合不佳。文档对于哪些结构应该使用装饰器可能有点含糊。
This copy mechanism is limited to simple columns that have no foreign keys, as a
ForeignKey
itself contains references to columns which can’t be properly recreated at this level. For columns that have foreign keys, as well as for the variety of mapper-level constructs that require destination-explicit context, thedeclared_attr
decorator is provided so that patterns common to many classes can be defined as callables
添加了强调,尽管我可能误解了 "destination-explicit context" 在这种情况下的含义。所以这样做:
class B(object):
...
@declared_attr
def my_data(cls):
return Column(MutableDict.as_mutable(PickleType))
相反,如果 B
是一个混入 class。失败的简单声明和 declared_attr
:
In [2]: from sqlalchemy.ext.mutable import MutableDict
In [3]: class MixinA:
...: extra = Column(MutableDict.as_mutable(PickleType))
...:
In [4]: from sqlalchemy.ext.declarative import declared_attr
In [5]: class MixinB:
...: @declared_attr
...: def extra(cls):
...: return Column(MutableDict.as_mutable(PickleType))
...:
In [6]: class A(MixinA, Base):
...: __tablename__ = 'a'
...: id = Column(Integer, primary_key=True, autoincrement=True)
...:
In [7]: class B(MixinB, Base):
...: __tablename__ = 'b'
...: id = Column(Integer, primary_key=True, autoincrement=True)
...:
In [8]: metadata.create_all()
并在行动:
In [9]: a = A(extra={})
In [10]: b = B(extra={})
In [11]: session.add(a)
In [12]: session.add(b)
In [13]: session.commit()
In [14]: session.query(A.extra).first()
Out[14]: ({})
In [15]: session.query(B.extra).first()
Out[15]: ({})
In [16]: b.extra['b'] = 1
In [17]: session.commit()
In [18]: session.query(B.extra).first()
Out[18]: ({'b': 1})
In [19]: a.extra['a'] = 1
In [20]: session.commit()
In [21]: session.query(A.extra).first()
Out[21]: ({})
In [22]: b.extra['bb'] = 2
In [23]: assert b in session.dirty