Flask-SQLAlchemy: Load children's association object from another association object - python

I'm trying to, querying an association table that has two foreign keys to another table, load an information that is in another association table. Difficult to explain, so look at the diagram:
Essentialy, what I'm trying to do is: starting from a Convalidacoes object, access the Disciplinas object that comes from the disciplina_origem_id relationship, and from that access the categoria information in the DisciplinasProjetos association object.
This is already working with the Disciplina object that comes from disciplina_id relationship, but not when coming from disciplina_origem_id. The latter gives me a Disciplina object that has an empty DisciplinasProjetos property.
My two association table are set up like this:
disciplinas_projetos:
class DisciplinaProjeto(db.Model):
__tablename__ = 'disciplinas_projetos'
id = db.Column(db.Integer, primary_key=True)
categoria = db.Column(db.Integer, nullable=False)
disciplina_id = db.Column(db.Integer, db.ForeignKey('disciplinas.id'))
projeto_pedagogico_id = db.Column(db.Integer, db.ForeignKey('projetos_pedagogicos.id'))
disciplina = db.relationship(
'Disciplina', back_populates='projetos_pedagogicos', lazy='joined'
)
projeto_pedagogico = db.relationship(
'ProjetoPedagogico', back_populates='disciplinas', lazy='joined'
)
convalidacoes:
class Convalidacao(db.Model):
__tablename__ = 'convalidacoes'
id = db.Column(db.Integer, primary_key=True)
projeto_pedagogico_id = db.Column(db.Integer, db.ForeignKey('projetos_pedagogicos.id'))
disciplina_origem_id = db.Column(db.Integer, db.ForeignKey('disciplinas.id'))
disciplina_id = db.Column(db.Integer, db.ForeignKey('disciplinas.id'))
projeto_pedagogico = db.relationship('ProjetoPedagogico', foreign_keys=[projeto_pedagogico_id])
disciplina_origem = db.relationship('Disciplina', foreign_keys=[disciplina_origem_id])
disciplina = db.relationship('Disciplina', foreign_keys=[disciplina_id])
And I'm querying the Convalidacoes object like this:
def buscar_convalidacoes_origem(projeto: ProjetoPedagogico, disciplinas: set[DisciplinaArquivo]) -> list[Convalidacao]:
return Convalidacao.query \
.join(
Disciplina, Disciplina.id == Convalidacao.disciplina_origem_id
) \
.filter(
Convalidacao.projeto_pedagogico == projeto,
Disciplina.codigo.in_({disciplina.codigo for disciplina in disciplinas})
) \
.all()
But after trying to query:
Do you have any idea why a property gets populated correctly and the other one does not?

Related

Sqlalchemy many-to-many association proxy: silently reject duplicates

I have a many to many association using association proxies as follows:
import sqlalchemy.orm as orm
import sqlalchemy as sa
import sqlalchemy.ext.associationproxy as ap
class Asset(BaseModel):
__tablename__ = 'assets'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.VARCHAR(255))
asset_tags = orm.relationship(
"AssetTag", back_populates='asset', cascade='all, delete-orphan')
tags = ap.association_proxy(
"asset_tags", "tag",
creator=lambda tag: AssetTag(tag=tag))
class AssetTag(BaseModel):
__tablename__ = 'asset_tags'
asset_id = sa.Column(sa.Integaer, sa.ForeignKey("assets.id"), primary_key=True)
tag_id = sa.Column(sa.Integer, alsach.ForeignKey("tags.id"), primary_key=True)
asset = orm.relationship("Asset", back_populates='asset_tags')
tag = orm.relationship("Tag", back_populates='asset_tags')
class Tag(BaseModel):
__tablename__ = 'tags'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.VARCHAR(255))
asset_tags = orm.relationship("AssetTag",
back_populates='tag',
cascade='all, delete-orphan')
assets = ap.association_proxy("asset_tags", "asset",
creator=lambda asset: AssetTag(asset=asset))
Note that the asset_tas table has a uniqueness constraint on (asset_id, tag_id).
If I do
with Session() as s:
a = s.get(Asset, 1)
tag = a.tags[0]
a.tags.append(tag)
s.commit()
SQLAlchemy creates a new AssetTag between Asset<1> and Tag<31> (example number), and tries to commit that, violating the uniqueness constraint.
pymysql.err.IntegrityError: (1062, "Duplicate entry '1-31' for key 'PRIMARY'")
Is there any way to make asset.tags have the behavior of a set, where adding an existing item is skipped?
asset.tags.append(tag)
asset.tags.append(tag) # fails silently
You can pass set as the relationship's collection_class argument. This will have the collection behave as a set rather than as a list.
asset_tags = orm.relationship(
"AssetTag",
back_populates='asset',
cascade='all, delete-orphan',
collection_class=set,
)
Note that you must use set methods when operating on the collection, for example
asset.tags.add(some_tag)
not
asset.tags.append(some_tag)

How to properly define SQLAlchemy backrefs so that they can be reflected?

Let's assume we have the following code in some Models.py file:
class Person(db.Model):
__tablename__ = 'Persons'
ID = db.Column(db.Integer, primary_key=True, nullable=False)
Name = db.Column(db.String(255), nullable=False)
class House(db.Model):
__tablename__ = 'Houses'
ID = db.Column(db.Integer,primary_key=True,nullable=False)
OwnerID = db.Column(db.Integer, nullable=False)
TenantID = db.Column(db.Integer, nullable=False)
__table_args__ = (
db.ForeignKeyConstraint(
['OwnerID'],
['Persons.ID'],
),
db.ForeignKeyConstraint(
['TenantID'],
['Persons.ID'],
),
)
OwnerBackref = db.relationship('Person', backref='OwnerBackref', lazy=True, foreign_keys=[OwnerID])
TenantBackref = db.relationship('Person', backref='TenantBackref', lazy=True, foreign_keys=[TenantID])
And we want to reflect these models using the automap base, so we have this code in another module called Database.py:
Base = automap_base()
engine = create_engine(DB_CONNECTION, pool_size=10, max_overflow=20)
db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
Base.prepare(engine, reflect=True)
Person = Base.classes.Persons
House = Base.classes.Houses
Now, when I import House in some other module I want to be able to do this:
h = db_session.query(House).first()
print(h.OwnerBackref.Name)
print(h.TenantBackref.Name)
But instead I get an error saying that those 2 backrefs do not exist and instead a field called 'persons' gets added to my House object but the problem here is that it links only 1 (either the Tenant either the Owner). By this I mean that if I do this:
print(h.persons.Name)
It will only print the Name either for the respective Tenant either for the Owner leaving me with no way of accessing the informations for the other one. (Note here that the names that I set to the backrefs are nowhere to be found)
So, my question is how can I use the backrefs I created to access my desired informations ? Am I doing something wrong here ?
The error in your code is that you are using foreign_keys= to define the relationship between the tables but you are passing the local key name not the foreign key name to the function. For your code you cannot use foreign_keys= to define the relationship within the House model as there is only one possible foreign key Person.ID but two possible local keys House.OwnerID and House.TenantID. The primaryjoin= argument should be used instead to specify this.
class Person(db.Model):
__tablename__ = 'Persons'
ID = db.Column(db.Integer, primary_key=True)
Name = db.Column(db.String(255), nullable=False)
class House(db.Model):
__tablename__ = 'Houses'
ID = db.Column(db.Integer,primary_key=True)
OwnerID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)
TenantID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)
Owner = db.relationship('Person', backref='HousesOwned', primaryjoin='House.OwnerID == Person.ID')
Tenant = db.relationship('Person', backref='HousesOccupied', primaryjoin='House.TenantID == Person.ID')
If you placed the relationship statements in in the Person model rather than the House model then you could use either foreign_keys= or primaryjoin= to define the relationship. The following code will result in exactly the same relationships as in the previous code.
class Person(db.Model):
__tablename__ = 'Persons'
ID = db.Column(db.Integer, primary_key=True)
Name = db.Column(db.String(255), nullable=False)
HousesOwned = db.relationship('House', backref='Owner', foreign_keys='[House.OwnerID]')
HousesOccupied = db.relationship('House', backref='Tenant', foreign_keys='[House.TenantID]')
class House(db.Model):
__tablename__ = 'Houses'
ID = db.Column(db.Integer,primary_key=True)
OwnerID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)
TenantID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)

Self referential relationship including a relationship attribute

Situation
I have the Self-Referential Many-to-Many Relationship (almost identical to the sqlalchemy manual entry of the same heading). This relationship is governed by the table entity_weights. This code works!
Question
How do I include an attribute in class Entity representing the table column entity_weights.weight. Lets say the attribute would be called Entity.child_weights. It is important that the rank of Entity.child_entities and Entity.child_weights are identical.
entity_weights = Table('entity_weights', Base.metadata,
Column('id',Integer, primary_key=True),
Column('parent_entity_id',Integer, ForeignKey('entity.id')),
Column('entity_id',Integer, ForeignKey('entity.id')),
Column('weight',Float))
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, primary_key=True)
name = Column(String)
domicile_id = Column(Integer, ForeignKey('domicile.id'))
entity_type = Column('type',Enum('asset','institution','model'))
source_table_id = Column(Integer)
child_entities = relationship('Entity',
secondary=entity_weights,
primaryjoin=id==entity_weights.c.parent_entity_id,
secondaryjoin=id==entity_weights.c.entity_id,
backref='parent_entity_id'
)
The cleanest solution I've found for this scenario is to break up the child_entities relationship by adding entity_weights as a one-to-many relationship on Entity and use an association proxy to proxy the weight value as well as the remote side of the many-to-many relationship:
class EntityWeight(Base):
__tablename__ = "entity_weights"
id = Column(Integer, primary_key=True)
parent_entity_id = Column(Integer, ForeignKey('entity.id'))
entity_id = Column(Integer, ForeignKey('entity.id'))
weight = Column(Float)
entity = relationship("Entity", primaryjoin=lambda: EntityWeight.entity_id == Entity.id)
class Entity(Base):
...
_child_weights = relationship(EntityWeight, primaryjoin=id == EntityWeight.parent_entity_id)
child_weights = association_proxy("_child_weights", "weight")
child_entities = association_proxy("_child_weights", "entity")

Trouble mapping multiple foreign keys to parent table

I have two tables, users and contacts. I query the contacts table and get a list of a user's contacts. I would then like to be able to write Contact.first_name (where first_name is a row from the users table) and print out that contact's first name.
Currently, my Contact object does not recognize any attributes of the user table.
Here is some code:
class User(Base):
""" Basic User definition """
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
first_name = Column(Unicode(255))
last_name = Column(Unicode(255))
contacts = relationship('Contact', backref='users')
class Contact(Base):
__tablename__ = 'contacts'
id = Column(Integer, primary_key=True)
user_id = Column(Integer)
contact_id = Column(Integer)
__table_args__ = (ForeignKeyConstraint([id], [User.id]), {})
Here is my query:
Contact.query.filter(Contact.user_id == self.user_id).filter(Contact.state == True).all()
To be honest, I'm unsure of how to properly map my two foreign keys Contact.user_id and Contact.contact_id to the User.id row. Maybe this is the source of my problem?
I'm very new to using SQLAlchemy, so this is a learning experience here. Thanks for your help.
What you have here is class User which essentially refers to itself. In other words, it's a self-referential many-to-many relationship. Your model definitions should look like this:
# This is so called association table, which links two tables in many-to-many
# relationship. In this case it links same table's ('users') different rows.
user_contacts = Table(
'user_contacts', Base.metadata,
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
Column('contact_id', Integer, ForeignKey('users.id'), primary_key=True),
)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
first_name = Column(String)
last_name = Column(String)
contacts = relationship(
'User',
secondary=user_contacts,
primaryjoin=id==user_contacts.c.user_id,
secondaryjoin=id==user_contacts.c.contact_id
)
Then you can do things like the following:
u1 = User(first_name='Foo', last_name='Foo')
u2 = User(first_name='Bar', last_name='Bar')
u3 = User(first_name='Baz', last_name='Baz')
u1.contacts = [u2, u3]
session.add(u1)
session.commit()
# ... and in some other place in your code ...
u = User.query.get(1)
print u.contacts[0].first_name

How to build many-to-many relations using SQLAlchemy: a good example

I have read the SQLAlchemy documentation and tutorial about building many-to-many relation but I could not figure out how to do it properly when the association table contains more than the 2 foreign keys.
I have a table of items and every item has many details. Details can be the same on many items, so there is a many-to-many relation between items and details
I have the following:
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
My association table is (It's defined before the other 2 in the code):
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
In the documentation, it's said that I need to use the "association object". I could not figure out how to use it properly, since it's mixed declarative with mapper forms and the examples seem not to be complete. I added the line:
details = relation(ItemDetail)
as a member of Item class and the line:
itemDetail = relation('Detail')
as a member of the association table, as described in the documentation.
when I do item = session.query(Item).first(), the item.details is not a list of Detail objects, but a list of ItemDetail objects.
How can I get details properly in Item objects, i.e., item.details should be a list of Detail objects?
From the comments I see you've found the answer. But the SQLAlchemy documentation is quite overwhelming for a 'new user' and I was struggling with the same question. So for future reference:
ItemDetail = Table('ItemDetail',
Column('id', Integer, primary_key=True),
Column('itemId', Integer, ForeignKey('Item.id')),
Column('detailId', Integer, ForeignKey('Detail.id')),
Column('endDate', Date))
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail, backref='Detail')
Like Miguel, I'm also using a Declarative approach for my junction table. However, I kept running into errors like
sqlalchemy.exc.ArgumentError: secondary argument <class 'main.ProjectUser'> passed to to relationship() User.projects must be a Table object or other FROM clause; can't send a mapped class directly as rows in 'secondary' are persisted independently of a class that is mapped to that same table.
With some fiddling, I was able to come up with the following. (Note my classes are different than OP's but the concept is the same.)
Example
Here's a full working example
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, Session
# Make the engine
engine = create_engine("sqlite+pysqlite:///:memory:", future=True, echo=False)
# Make the DeclarativeMeta
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
projects = relationship('Project', secondary='project_users', back_populates='users')
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True)
name = Column(String)
users = relationship('User', secondary='project_users', back_populates='projects')
class ProjectUser(Base):
__tablename__ = "project_users"
id = Column(Integer, primary_key=True)
notes = Column(String, nullable=True)
user_id = Column(Integer, ForeignKey('users.id'))
project_id = Column(Integer, ForeignKey('projects.id'))
# Create the tables in the database
Base.metadata.create_all(engine)
# Test it
with Session(bind=engine) as session:
# add users
usr1 = User(name="bob")
session.add(usr1)
usr2 = User(name="alice")
session.add(usr2)
session.commit()
# add projects
prj1 = Project(name="Project 1")
session.add(prj1)
prj2 = Project(name="Project 2")
session.add(prj2)
session.commit()
# map users to projects
prj1.users = [usr1, usr2]
prj2.users = [usr2]
session.commit()
with Session(bind=engine) as session:
print(session.query(User).where(User.id == 1).one().projects)
print(session.query(Project).where(Project.id == 1).one().users)
Notes
reference the table name in the secondary argument like secondary='project_users' as opposed to secondary=ProjectUser
use back_populates instead of backref
I made a detailed writeup about this here.
Previous Answer worked for me, but I used a Class base approach for the table ItemDetail. This is the Sample code:
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True, index=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail.__table__, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail.__table__, backref='Detail')

Categories