SQLAlchemy - 自定义声明基础中的自引用多对多关系 class
SQLAlchemy - Self-referential many-to-many relationship in custom declarative base class
我正在构建一个使用 SQLAlchemy 的 Python 应用程序,并且我正在尝试在自定义声明基础 class 与其自身(自引用)之间实现多对多关系。但我无法让它工作。我在下面附上代码以及错误回溯,以防万一有人可以提供帮助:)模型的所有实体都已经从这个基础扩展 class,并且应用程序到目前为止正在运行,以防万一有帮助。
谢谢!!
代码(非功能性):
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import MetaData, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy import String
permissions = Table(
'permissions', MetaData(),
Column('origin_id', String, ForeignKey('bases.id'), primary_key=True),
Column('target_id', String, ForeignKey('bases.id'), primary_key=True)
)
class Base:
__tablename__ = 'bases'
__table_args__ = {
'mysql_engine': 'InnoDB'
}
id = Column(String, primary_key=True)
targets = relationship(
'Base',
secondary='permissions',
primaryjoin='Base.id == permissions.c.origin_id',
secondaryjoin='Base.id == permissions.c.target_id',
backref='origins'
# Reference:
# https://docs.sqlalchemy.org/en/14/orm/join_conditions.html#self-referential-many-to-many
)
Base = declarative_base(cls=Base)
回溯:
class ContactMethod(Base):
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_api.py", line 72, in __init__
_as_declarative(reg, cls, dict_)
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 126, in _as_declarative
return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 177, in setup_mapping
return cfg_cls(registry, cls_, dict_, table, mapper_kw)
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 299, in __init__
self._scan_attributes()
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 511, in _scan_attributes
raise exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Mapper properties (i.e. deferred,column_property(), relationship(), etc.) must be declared as @declared_attr callables on declarative mixin classes. For dataclass field() objects, use a lambda:
以下是错误输出建议修复的示例:
from sqlalchemy.ext.declarative import declared_attr
class Base:
...
@declared_attr
def targets(cls):
return relationship(
'Base',
secondary='permissions',
primaryjoin='Base.id == permissions.c.origin_id',
secondaryjoin='Base.id == permissions.c.target_id',
backref='origins'
)
...
旁注:您可以在基础 class 上使用 as_declarative
mixin 作为快捷方式。
参考资料
扩充基础:https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/mixins.html#augmenting-the-base
declared_attr: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html#sqlalchemy.ext.declarative.declared_attr
as_declarative: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html#sqlalchemy.ext.declarative.as_declarative
查看一个有效的自包含示例:
## Imports
from sqlalchemy import Column, ForeignKey, Integer, String, Table, create_engine
from sqlalchemy.orm import Session, as_declarative, declared_attr, relationship
## Configuration
engine = create_engine("sqlite:///:memory:", echo=True)
@as_declarative()
class Base(object):
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
permissions = Table(
"permissions",
Base.metadata,
Column("origin_id", String, ForeignKey("model_base.id"), primary_key=True),
Column("target_id", String, ForeignKey("model_base.id"), primary_key=True),
)
## Model definitions
class ModelBase(Base):
__tablename__ = "model_base"
id = Column(Integer, primary_key=True)
type = Column(String, nullable=False)
__mapper_args__ = {
"polymorphic_on": type,
"polymorphic_identity": None,
}
targets = relationship(
"ModelBase",
secondary="permissions",
primaryjoin="model_base.c.id == permissions.c.origin_id",
secondaryjoin="model_base.c.id == permissions.c.target_id",
backref="origins",
# lazy="raise",
)
class ClassA(ModelBase):
__tablename__ = "class_a"
__mapper_args__ = {"polymorphic_identity": "class_a"}
id = Column(ForeignKey("model_base.id"), primary_key=True)
name = Column(String)
class ClassB(ModelBase):
__tablename__ = "class_b"
__mapper_args__ = {"polymorphic_identity": "class_b"}
id = Column(ForeignKey("model_base.id"), primary_key=True)
name = Column(String)
value = Column(Integer)
## Tests
def _main():
with Session(engine) as session:
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
print("=" * 80)
# data
a1, a2 = [ClassA(name="a1"), ClassA(name="a2")]
b1, b2 = [ClassB(name="b1"), ClassB(name="b2", value=3)]
session.add_all([a1, a2, b1, b2])
session.flush()
a1.targets.append(a2)
a1.targets.append(b1)
a1.targets.append(b2)
print(b2.targets)
print(b2.origins)
session.commit()
session.expunge_all()
print("=" * 80)
a1 = session.query(ClassA).first()
print(a1)
print(a1.origins)
print(a1.targets)
session.commit()
if __name__ == "__main__":
_main()
我正在构建一个使用 SQLAlchemy 的 Python 应用程序,并且我正在尝试在自定义声明基础 class 与其自身(自引用)之间实现多对多关系。但我无法让它工作。我在下面附上代码以及错误回溯,以防万一有人可以提供帮助:)模型的所有实体都已经从这个基础扩展 class,并且应用程序到目前为止正在运行,以防万一有帮助。
谢谢!!
代码(非功能性):
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import MetaData, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy import String
permissions = Table(
'permissions', MetaData(),
Column('origin_id', String, ForeignKey('bases.id'), primary_key=True),
Column('target_id', String, ForeignKey('bases.id'), primary_key=True)
)
class Base:
__tablename__ = 'bases'
__table_args__ = {
'mysql_engine': 'InnoDB'
}
id = Column(String, primary_key=True)
targets = relationship(
'Base',
secondary='permissions',
primaryjoin='Base.id == permissions.c.origin_id',
secondaryjoin='Base.id == permissions.c.target_id',
backref='origins'
# Reference:
# https://docs.sqlalchemy.org/en/14/orm/join_conditions.html#self-referential-many-to-many
)
Base = declarative_base(cls=Base)
回溯:
class ContactMethod(Base):
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_api.py", line 72, in __init__
_as_declarative(reg, cls, dict_)
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 126, in _as_declarative
return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 177, in setup_mapping
return cfg_cls(registry, cls_, dict_, table, mapper_kw)
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 299, in __init__
self._scan_attributes()
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 511, in _scan_attributes
raise exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Mapper properties (i.e. deferred,column_property(), relationship(), etc.) must be declared as @declared_attr callables on declarative mixin classes. For dataclass field() objects, use a lambda:
以下是错误输出建议修复的示例:
from sqlalchemy.ext.declarative import declared_attr
class Base:
...
@declared_attr
def targets(cls):
return relationship(
'Base',
secondary='permissions',
primaryjoin='Base.id == permissions.c.origin_id',
secondaryjoin='Base.id == permissions.c.target_id',
backref='origins'
)
...
旁注:您可以在基础 class 上使用 as_declarative
mixin 作为快捷方式。
参考资料
扩充基础:https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/mixins.html#augmenting-the-base
declared_attr: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html#sqlalchemy.ext.declarative.declared_attr
as_declarative: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html#sqlalchemy.ext.declarative.as_declarative
查看一个有效的自包含示例:
## Imports
from sqlalchemy import Column, ForeignKey, Integer, String, Table, create_engine
from sqlalchemy.orm import Session, as_declarative, declared_attr, relationship
## Configuration
engine = create_engine("sqlite:///:memory:", echo=True)
@as_declarative()
class Base(object):
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
permissions = Table(
"permissions",
Base.metadata,
Column("origin_id", String, ForeignKey("model_base.id"), primary_key=True),
Column("target_id", String, ForeignKey("model_base.id"), primary_key=True),
)
## Model definitions
class ModelBase(Base):
__tablename__ = "model_base"
id = Column(Integer, primary_key=True)
type = Column(String, nullable=False)
__mapper_args__ = {
"polymorphic_on": type,
"polymorphic_identity": None,
}
targets = relationship(
"ModelBase",
secondary="permissions",
primaryjoin="model_base.c.id == permissions.c.origin_id",
secondaryjoin="model_base.c.id == permissions.c.target_id",
backref="origins",
# lazy="raise",
)
class ClassA(ModelBase):
__tablename__ = "class_a"
__mapper_args__ = {"polymorphic_identity": "class_a"}
id = Column(ForeignKey("model_base.id"), primary_key=True)
name = Column(String)
class ClassB(ModelBase):
__tablename__ = "class_b"
__mapper_args__ = {"polymorphic_identity": "class_b"}
id = Column(ForeignKey("model_base.id"), primary_key=True)
name = Column(String)
value = Column(Integer)
## Tests
def _main():
with Session(engine) as session:
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
print("=" * 80)
# data
a1, a2 = [ClassA(name="a1"), ClassA(name="a2")]
b1, b2 = [ClassB(name="b1"), ClassB(name="b2", value=3)]
session.add_all([a1, a2, b1, b2])
session.flush()
a1.targets.append(a2)
a1.targets.append(b1)
a1.targets.append(b2)
print(b2.targets)
print(b2.origins)
session.commit()
session.expunge_all()
print("=" * 80)
a1 = session.query(ClassA).first()
print(a1)
print(a1.origins)
print(a1.targets)
session.commit()
if __name__ == "__main__":
_main()