SQLAlchemy - Self-referential many-to-many relationship in custom declarative base class - python

I am building a Python application that uses SQLAlchemy, and I am trying to implement a many-to-many relationship between a custom declarative base class and itself (self-referential). But I cannot get it to work. I am attaching the code down below, as well as the error traceback, in case anyone can help :) All the entities of the model already extended from this base class, and the application was working so far, in case that helps.
Thanks!!
Code (non-functional):
#!/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)
Traceback:
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:

See a working self-containing example:
## 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()

Here’s an example of what the error output recommends to remediate:
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'
)
...
Sidenote: you can use the as_declarative mixin on your base class as a shortcut.
References
Augmenting the base: 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

Related

How to establish a self-referencing many-to-many relationship [duplicate]

I've found examples for a self-referencing many-to-many relationship with an association table. How can I achieve the same using an association object?
The code below is based on: How can I achieve a self-referencing many-to-many relationship on the SQLAlchemy ORM back referencing to the same attribute?
from sqlalchemy import Table, Column, Integer, ForeignKey
from db.common import Base
from sqlalchemy.orm import relationship
M2M = Table('m2m',
Base.metadata,
Column('entity_parent_id',
Integer,
ForeignKey('entity.id'),
primary_key=True),
Column('entity_child_id',
Integer,
ForeignKey('entity.id'),
primary_key=True),
)
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, primary_key=True)
entity_childs = relationship("Entity",
secondary=M2M,
primaryjoin="Enity.id==m2m.c.entity_parent_id",
secondaryjoin="Enity.id==m2m.c.entity_child_id",
)
entity_parents = relationship("Entity",
secondary=M2M,
primaryjoin="Enity.id==m2m.c.entity_child_id",
secondaryjoin="Enity.id==m2m.c.entity_parent_id",
)
The following approach uses an association object instead of an association table to get a self-referencing many-to-many relationship:
from sqlalchemy import Column, Integer, ForeignKey, create_engine, String
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class EntityAssociation(Base):
__tablename__ = 'entity_association'
entity_parent_id = Column(Integer, ForeignKey('entity.id'), primary_key=True)
entity_child_id = Column(Integer, ForeignKey('entity.id'), primary_key=True)
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, primary_key=True)
name = Column(String)
entity_childs = relationship('Entity',
secondary='entity_association',
primaryjoin=id==EntityAssociation.entity_parent_id,
secondaryjoin=id==EntityAssociation.entity_child_id,
backref='childs')
entity_parents = relationship('Entity',
secondary='entity_association',
primaryjoin=id==EntityAssociation.entity_child_id,
secondaryjoin=id==EntityAssociation.entity_parent_id,
backref='parents')
def __repr__(self):
return f'<Entity(name={self.name})>'
if __name__ == '__main__':
engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
Session = sessionmaker(engine)
db = Session()
parent1 = Entity(name='parent1')
parent2 = Entity(name='parent2')
child1 = Entity(name='child1')
child2 = Entity(name='child2')
parent1.entity_childs = [child1, child2]
parent2.entity_childs = [child2]
db.add(parent1)
db.add(parent2)
db.add(child1)
db.add(child2)
db.commit()
entities = db.query(Entity).all()
for entity in entities:
print(entity)
print(' Parent: ', entity.entity_parents)
print(' Childs: ', entity.entity_childs)
print()
This will have the following result:
<Entity(name=parent1)>
Parent: []
Childs: [<Entity(name=child1)>, <Entity(name=child2)>]
<Entity(name=child1)>
Parent: [<Entity(name=parent1)>]
Childs: []
<Entity(name=child2)>
Parent: [<Entity(name=parent1)>, <Entity(name=parent2)>]
Childs: []
<Entity(name=parent2)>
Parent: []
Childs: [<Entity(name=child2)>]

Why am I getting "InvalidRequestError"?

I'm trying to implement some foreign keys between tables and query using joins as per this ORM tutorial:
from sqlalchemy import (Column, ForeignKey, Integer, create_engine)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
Base = declarative_base()
class DataAccessLayer():
def __init__(self):
conn_string = "mysql+mysqlconnector://root:root#localhost/"
self.engine = create_engine(conn_string)
def create_session(self):
Base.metadata.create_all(self.engine)
Session = sessionmaker()
Session.configure(bind=self.engine)
self.session = Session()
class Bet(Base):
__tablename__ = "bet"
__table_args__ = ({"schema": "belgarath", "extend_existing": True})
id_ = Column(Integer, primary_key=True)
match_id = Column(Integer, ForeignKey("belgarath.match_.id_"))
match_ = relationship("Match", back_populates="belgarath.bet")
class Match(Base):
__tablename__ = "match_"
__table_args__ = ({"schema": "belgarath", "extend_existing": True})
id_ = Column(Integer, primary_key=True)
tournament_id = Column(Integer)= Column(Integer)
dal = DataAccessLayer()
dal.create_session()
bets = dal.session.query(Bet)
bets.join(Match)
for bet in bets:
print(bet.id_, bet.tournament_id = Column(Integer))
However I'm getting the following error on the bets = dal.session.query(Bet) line:
Exception has occurred: InvalidRequestError
Mapper 'mapped class Match->match_' has no property 'belgarath.bet'
Where am I going wrong? Do I need some kind of reciprocal relationship in Match?

Inherit and add indexes from sqlalchemy mixins

Given a mixin with an index, how is it possible to add additional indexes to a model inheriting that mixin? With an index name like idx__TABLENAME__COLUMN the mixin is unable to obtain the TABLENAME for itself. If a __tablename__ is specified for the mixin, then duplicate index names arise.
Example code follows.
import sqlalchemy as sql
from sqlalchemy import Column, Index, String, Integer
from sqlalchemy.ext.declarative import declared_attr, declarative_base
Base = declarative_base()
class MixinOwner(object):
id = Column('id', Integer, primary_key=True)
owner = Column('owner', String)
#declared_attr
def __table_args__(cls):
return (Index('idx__%s__owner' % cls.__tablename__, 'owner'), )
class Letter(MixinOwner, Base):
__tablename__ = 'letter'
a = Column('a', String)
b = Column('b', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = list(MixinOwner.__table_args__) # <--- Error (MixinOwner does not have attribute __tablename__)
mixin_indexes.extend([
Index('idx__letter__a', 'a'),
Index('idx__letter__b', 'b'),
])
return tuple(mixin_indexes)
class Word(MixinOwner, Base):
__tablename__ = 'word'
apple = Column('apple', String)
banana = Column('banana', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = list(MixinOwner.__table_args__)
mixin_indexes.extend([
Index('idx__word__apple', 'apple'),
Index('idx__word__banana', 'banana'),
])
return tuple(mixin_indexes)
engine = sqlalchemy.create_engine('sqlite:///:memory:')
engine.connect()
Base.metadata.bind = engine
Base.metadata.create_all()
Session = sqlalchemy.orm.sessionmaker(bind=engine)
session = Session()
To incorporate indexes from mixins you need to use super to access the methods of base classes within the context of the subclass. In addition to this fix, which solves the original question I posed, there springs another question -- how to incorporate indexes from multiple mixins not just one (as was setup in the original question). The following code explains this larger issue as well. The solution is to iterate over the MRO and acquire the __table_args__ for each base class with the context of the model.
import sqlalchemy as sql
from sqlalchemy import Column, Index, String, Integer, Date
from sqlalchemy.ext.declarative import declared_attr, declarative_base
Base = declarative_base()
class MixinOwner(object):
id = Column('id', Integer, primary_key=True)
owner = Column('owner', String)
#declared_attr
def __table_args__(cls):
return (Index('idx__%s__owner' % cls.__tablename__, 'owner'), )
class MixinDate(object):
date = Column('date', Date)
#declared_attr
def __table_args__(cls):
return (Index('idx__%s__date' % cls.__tablename__, 'date'), )
# single mixin inheritance (original question) -- use super(cls, cls)
class Word(MixinOwner, Base):
__tablename__ = 'word'
apple = Column('apple', String)
banana = Column('banana', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = list((super(cls, cls).__table_args__))
mixin_indexes.extend([
Index('idx__word__apple', 'apple'),
Index('idx__word__banana', 'banana'),
])
return tuple(mixin_indexes)
# multiple mixin iheritance (not in original question)
# iterate through __mro__ and aggregate __table_args__ from each base
class Letter(MixinOwner, MixinDate, Base):
__tablename__ = 'letter'
a = Column('a', String)
b = Column('b', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = []
for base_class in cls.__mro__:
try:
mixin_indexes.extend(super(base_class, cls).__table_args__)
except AttributeError:
pass
mixin_indexes.extend([
Index('idx__letter__a', 'a'),
Index('idx__letter__b', 'b'),
])
return tuple(mixin_indexes)
engine = sql.create_engine('sqlite:///:memory:')
engine.connect()
Base.metadata.bind = engine
Base.metadata.create_all()
Session = sql.orm.sessionmaker(bind=engine)
session = Session()

Automating sqlalchemy declarative model creation

Let’s say we have several sqlalchemy models for catalogues:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer
from sqlalchemy.orm import relationship
Base = declarative_base()
class Plane(Base):
__tablename__ = 'Plane'
plane_id = Column(Integer, primary_key=True)
class Car(Base):
__tablename__ = 'Car'
car_id = Column(Integer, primary_key=True)
Now for import/export purposes we want to relate these to external ids. So for Plane we would write:
class PlaneID(Base):
issuer = Column(String(32), primary_key=True)
external_id = Column(String(16), primary_key=True)
plane_id = Column(Integer, ForeignKey(Plane.plane_id))
plane = relationship(Plane, backref='external_ids')
A CarID model would be defined in exactly the same way.
What are possibilities to automate this process?
Maybe we could use a mixin, factory, decorator or meta class. How would we generate the dynamically named Columns then? It would be good to be able to add more Columns to the generated models as needed. For example:
class CarID(ExternalID):
valid_from = Column(Date)
You can subclass DeclarativeMeta - the metaclass used in declarative_base function:
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class ExternalObject(DeclarativeMeta):
def __new__(mcs, name, bases, attributes):
if 'issuer' not in attributes:
attributes['issuer'] = Column(String(32), primary_key=True)
if 'external_id' not in attributes:
attributes['external_id'] = Column(String(16), primary_key=True)
if name[-2:] == 'ID':
ext_cls_name = name[:-2]
attr_rel = ext_cls_name.lower()
attr_id = '%s_id' % attr_rel
if attr_rel in attributes or attr_id in attributes:
# Some code here in case 'car' or 'car_id' attribute is defined in new class
pass
attributes[attr_id] = Column(Integer, ForeignKey('%s.%s' % (ext_cls_name, attr_id)))
attributes[attr_rel] = relationship(ext_cls_name, backref='external_ids')
new_cls = super().__new__(mcs, name, bases, attributes)
return new_cls
ExternalID = declarative_base(metaclass=ExternalObject)
After that you can create subclass from ExternalID and add another attributes like you did for CarID.

Flask SQLAlchemy: AttributeError: Neither 'Column' object nor 'Comparator' object has an attribute 'schema' [duplicate]

This question already has answers here:
Python SQLAlchemy: AttributeError: Neither 'Column' object nor 'Comparator' object has an attribute 'schema'
(3 answers)
Closed 4 years ago.
all
I have a question on a Flask with SQL-Alchemy
I am now implementing the Poll app using Flask.
During the modeling, I'm facing the problem with many to many relationship. Based on the tutorial on Flask official website, I followed it, but I got a problem when I tried to use it.
Here is the models.py code
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Table, PrimaryKeyConstraint
from sqlalchemy.orm import relationship
from database import Base
from datetime import datetime
respondents_identifier = Table('respondents_identifier',
Column('user_id', Integer, ForeignKey('users.id')),
Column('poll_id', Integer, ForeignKey('polls.id')),
)
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(20))
name_string = Column(String(100), unique=True)
def __init__(self, name=None, name_string=None):
self.name = name
self.name_string = name_string
def __repr__(self):
return '<User %r %r>' % self.name, self.nameString
class Poll(Base):
__tablename__ = 'poll'
id = Column(Integer, primary_key=True)
subject = Column(String(50))
question_statement = Column(String(100))
num_questions = Column(Integer) # 응답지 개수
total_participant = Column(Integer) # 총 참여자 수
questions = relationship('Question', backref='Poll')
comments = relationship('Comment', backref='Poll')
respondents = relationship("User",
secondary=respondents_identifier)
def __init__(self, subject=None, question_statement=None, num_questions=2):
self.subject = subject
self.question_statement = question_statement
self.num_questions = num_questions
self.total_participant = 0
class Question(Base):
__tablename__ = 'question'
id = Column(Integer, primary_key=True)
choice_num = Column(Integer)
answer_description = Column(String(50))
poll_id = Column(Integer, ForeignKey('poll.id'))
selected_num = Column(Integer) # 선택된 수
def __init__(self, choice_num, answer_description=None):
self.choice_num = choice_num
self.answer_description = answer_description
self.selected_num = 0
def __repr__(self):
return '<Poll %d %r>' % self.answer_num, self.answer_description
def set_poll_id(self, poll):
self.poll_id = poll.id
class Comment(Base):
__tablename__ = 'comment'
id = Column(Integer, primary_key=True)
comment_content = (String(200))
user_name = (String(20))
poll_id = Column(Integer, ForeignKey('poll.id'))
comment_time = Column(DateTime)
def __init__(self, user_name, comment_content):
self.user_name = user_name
self.comment_content = comment_content
self.comment_time = datetime.now()
and, this is my database.py outside of app directory, this is in the root directory of my project.
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
def init_db():
# import all modules here that might define models so that
# they will be registered properly on the metadata. Otherwise
# you will have to import them first before calling init_db()
import HotOpinion.models
Base.metadata.create_all(bind=engine)
You can notify that, database.py is very similar to given example code on official website.
Here is the Stacktrace of the error.
/Users/junojunho/.pyenv/versions/hotopinion/bin/python /Users/junojunho/Documents/github/HotOpinion/runserver.py
Traceback (most recent call last):
File "/Users/junojunho/Documents/github/HotOpinion/runserver.py", line 15, in <module>
init_db()
File "/Users/junojunho/Documents/github/HotOpinion/database.py", line 17, in init_db
import HotOpinion.models
File "/Users/junojunho/Documents/github/HotOpinion/HotOpinion/models.py", line 11, in <module>
Column('poll_id', Integer, ForeignKey('polls.id')),
File "/Users/junojunho/.pyenv/versions/hotopinion/lib/python2.7/site-packages/sqlalchemy/sql/schema.py", line 374, in __new__
schema = metadata.schema
File "/Users/junojunho/.pyenv/versions/hotopinion/lib/python2.7/site-packages/sqlalchemy/sql/elements.py", line 735, in __getattr__
key)
AttributeError: Neither 'Column' object nor 'Comparator' object has an attribute 'schema'
Process finished with exit code 1
How do I solve it? I don' know where I can start to fix it.
If I remove the identifier part, and the relationship between User, and Poll, everything works fine. the problem is that part.
Oh, god. I find a solution.
I just add Base.metadata even if I can see the attribute.
And I just some fix to database syntax. Here is I did.
respondents_identifier = Table('respondents_identifier',
Base.metadata,
Column('user_id', Integer, ForeignKey('user.id')),
Column('poll_id', Integer, ForeignKey('poll.id')),
)

Categories