SQLAlchemy Three Many-To-Many relations in one elegant query - python

How to do a query in sqlalchemy through three many-to-many relations, joining them and getting only unique records?
My setup is following:
Command - something a user can perform
User - a person Group - group
of users for easy distribution of commands
UserGroup - N:N
relationship between users and groups
UserCommand - N:N relationship
between users and commands
GroupCommand - N:N relationship between
groups and commands
See the code:
#!/usr/bin/python3
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
Column,
Integer,
ForeignKey
)
from sqlalchemy.orm import (
relationship,
backref
)
Base = declarative_base()
class Command(Base):
__tablename__ = 'commands'
id = Column(Integer, primary_key=True)
users = relationship('User', secondary='users_commands')
groups = relationship('Group', secondary='groups_commands')
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
groups = relationship('Group', secondary='users_groups')
commands = relationship('Command', secondary='users_commands')
class Group(Base):
__tablename__ = 'groups'
id = Column(Integer, primary_key=True)
users = relationship('User', secondary='users_groups')
commands = relationship('Command', secondary='groups_commands')
class UserGroup(Base):
"""Many-To-Many on users and groups"""
__tablename__ = 'users_groups'
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
group_id = Column(Integer, ForeignKey('groups.id'), primary_key=True)
user = relationship('User', backref=backref('user_groups', cascade='all, delete-orphan'))
user_group = relationship('UserGroup', backref=backref('user_groups', cascade='all, delete-orphan'))
class UserCommand(Base):
"""Many-to-many on users and commands"""
__tablename__ = 'users_commands'
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
command_id = Column(Integer, ForeignKey('commands.id'), primary_key=True)
user = relationship('User', backref=backref('user_commands', cascade='all, delete-orphan'))
command = relationship('Command', backref=backref('user_commands', cascade='all, delete-orphan'))
class GroupCommand(Base):
"""Many-to-many on groups and commands"""
__tablename__ = 'groups_commands'
group_id = Column(Integer, ForeignKey('groups.id'), primary_key=True)
command_id = Column(Integer, ForeignKey('commands.id'), primary_key=True)
group = relationship('Group', backref=backref('group_commands', cascade='all, delete-orphan'))
command = relationship('Command', backref=backref('group_commands', cascade='all, delete-orphan'))
I want to get all commands for one user, first based on groups the user is in and then based on user specific commands. E.g. pseudocode:
User[1] belongs to the Group[1, 2]
User[1] itself has Command[1, 2]
Group[1] has Command[2, 3], Group[2] has Command[3, 4]
I want the query to return Command[1, 2, 3, 4]
I am able to do it via two separate queries and list/set unique join:
commands_user_specific = session.query(Command).\
join(Command.users).\
join(User.groups).\
filter(User.id == self.user.id).all()
commands_group_specific = session.query(Command).\
join(Command.groups).\
join(Group.users).\
filter(User.id == self.user.id).all()
commands = commands_user_specific +\
list(set(commands_group_specific) - set(commands_user_specific))
However I believe that there is also some more elegant way, with ".join" or even through ".filter", which I prefer to use.
Thank you for your insights

Related

SQLAlchemy - how to set relationship to back_populates to parent id in the same table

I try to set fk which parent_id contains id of a person in People table in orm manner and backpopulate between them but it does not work.
class People(Base):
__tablename__ = "people"
id = Column(Integer, primary_key=True)
name = Column(String(20), nullable=False, unique=True)
parent_id = Column(Integer, ForeignKey('people.id'))
parent = relationship("People", back_populates="parent", uselist=False)
engine = create_engine(
f'mssql://{username}:{password}#{server_name}/{db_name}?driver=SQL+Server&trusted_connection=yes')
Session = sessionmaker(bind=engine)
session = Session()
session.add(People(name='me'))
raise sa_exc.ArgumentError(
sqlalchemy.exc.ArgumentError: People.parent and back-reference People.parent are both of the same direction symbol('ONETOMANY'). Did you mean to set remote_side on the many-to-one side ?
You can use the remote_side argument.
Here's code I'm using adapted to your example:
class People(Base):
__tablename__ = "people"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('people.id'))
parent = relationship('People', foreign_keys=parent_id, remote_side=id)
children = relationship('People', back_populates='parent')

How do i get multiple foreign keys targetting the same model? (AmbiguousForeignKeysError)

I have two straightforward models like so
class GameModel(db.Model):
__tablename__ = 'games'
id = db.Column(db.Integer, primary_key=True)
home_team = db.Column(db.Integer, db.ForeignKey("teams.team_id"))
away_team = db.Column(db.Integer, db.ForeignKey("teams.team_id"))
class TeamModel(db.Model):
__tablename__ = "teams"
id = db.Column(db.Integer, primary_key=True)
team_id = db.Column(db.Integer, nullable=False, unique=True)
games = db.relationship("GameModel", lazy="joined", backref="game")
when i migrate, i'm getting an error
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship T
eamModel.games - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing
a list of those columns which should be counted as containing a foreign key reference to the parent table.
how do i correctly join these two tables together
You'll need to maintain separate lists of home_games and away_games and then combine them to return the full list of games:
import datetime
import sqlalchemy as sa
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import declarative_base, relationship
connection_uri = (
"mssql+pyodbc://#localhost:49242/myDb?driver=ODBC+Driver+17+for+SQL+Server"
)
engine = sa.create_engine(
connection_uri,
future=True,
echo=False,
)
Base = declarative_base()
class Game(Base):
__tablename__ = "game"
id = sa.Column(sa.Integer, primary_key=True)
when = sa.Column(sa.Date, nullable=False)
home_team_id = sa.Column(sa.Integer, sa.ForeignKey("team.id"))
away_team_id = sa.Column(sa.Integer, sa.ForeignKey("team.id"))
home_team = relationship(
"Team", foreign_keys=[home_team_id], back_populates="home_games"
)
away_team = relationship(
"Team", foreign_keys=[away_team_id], back_populates="away_games"
)
def __repr__(self):
return f"<Game(when='{self.when}')>"
class Team(Base):
__tablename__ = "team"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(50))
home_games = relationship(
Game, foreign_keys=[Game.home_team_id], back_populates="home_team"
)
away_games = relationship(
Game, foreign_keys=[Game.away_team_id], back_populates="away_team"
)
#hybrid_property
def games(self):
return self.home_games + self.away_games
def __repr__(self):
return f"<Team(name='{self.name}')>"
# <just for testing>
Base.metadata.drop_all(engine, checkfirst=True)
Base.metadata.create_all(engine)
# </just for testing>
with sa.orm.Session(engine, future=True) as session:
t_a = Team(name="Team_A")
t_b = Team(name="Team_B")
g1 = Game(when=datetime.date(2021, 1, 1), home_team=t_a, away_team=t_b)
g2 = Game(when=datetime.date(2022, 2, 2), home_team=t_b, away_team=t_a)
session.add_all([t_a, t_b, g1, g2])
print(t_a.home_games) # [<Game(when='2021-01-01')>]
print(t_a.away_games) # [<Game(when='2022-02-02')>]
print(t_a.games) # [<Game(when='2021-01-01')>, <Game(when='2022-02-02')>]

Many to many query in sqlalchemy

There are tables for my question.
class TemplateExtra(ExtraBase, InsertMixin, TimestampMixin):
__tablename__ = 'template_extra'
id = Column(Integer, primary_key=True, autoincrement=False)
name = Column(Text, nullable=False)
roles = relationship(
'RecipientRoleExtra',
secondary='template_to_role',
)
class RecipientRoleExtra(
ExtraBase, InsertMixin, TimestampMixin,
SelectMixin, UpdateMixin,
):
__tablename__ = 'recipient_role'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False)
description = Column(Text, nullable=False)
class TemplateToRecipientRoleExtra(ExtraBase, InsertMixin, TimestampMixin):
__tablename__ = 'template_to_role'
id = Column(Integer, primary_key=True, autoincrement=True)
template_id = Column(Integer, ForeignKey('template_extra.id'))
role_id = Column(Integer, ForeignKey('recipient_role.id'))
I want to select all templates with prefetched roles in two sql-queries like Django ORM does with prefetch_related. Can I do it?
This is my current attempt.
def test_custom():
# creating engine with echo=True
s = DBSession()
for t in s.query(TemplateExtra).join(RecipientRoleExtra, TemplateExtra.roles).all():
print(f'id = {t.id}')
for r in t.roles:
print(f'-- {r.name}')
But..
it generates select query for every template to select its roles. Can I make sqlalchemy to do only one query?
generated queries for roles are without join, just FROM recipient_role, template_to_role with WHERE %(param_1)s = template_to_role.template_id AND recipient_role.id = template_to_role.role_id. Is it correct?
Can u help me?
Based on this answer:
flask many to many join as done by prefetch_related from django
Maybe somthing like this:
roles = TemplateExtra.query.options(db.joinedload(TemplateExtra.roles)).all
Let me know if it worked.

Error on join condition with SqlAlchemy

I'm trying to use SQLAlchemy on my python app but I have a problem with the many to many relationship.
I have 4 tables:
users, flags, commandes, channels, and commandes_channels_flags
commandes_channels_flags contain a foreign key for each concerned table (commandes, channels and flags)
An user has a flag_id as foreign key too.
So I try to link commandes, channels and flag. the objective is to know that a command can run on a channel for a flag.
I did this:
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
pseudo = Column(String(50), unique=True, nullable=False)
flag_id = Column(ForeignKey('flags.id'))
class Flag(Base):
__tablename__ = 'flags'
id = Column(Integer, primary_key=True)
irc_flag = Column(Integer)
nom = Column(String(50))
users = relationship("User", backref="flag", order_by="Flag.irc_flag")
commande = relationship("Commande", secondary="commandes_channels_flags", back_populates="flags")
channel = relationship("Channel", secondary="commandes_channels_flags", back_populates="flags")
class Channel(Base):
__tablename__ = 'channels'
id = Column(Integer, primary_key=True)
uri = Column(String(50))
topic = Column(String(255))
commande = relationship("Commande", secondary="commandes_channels_flags", back_populates="channels")
flag = relationship("Flag", secondary="commandes_channels_flags", back_populates="channels")
class Commande(Base):
__tablename__ = 'commandes'
id = Column(Integer, primary_key=True)
pattern = Column(String(50))
channel = relationship("Channel", secondary="commandes_channels_flags", back_populates="commandes")
flag = relationship("Flag", secondary="commandes_channels_flags", back_populates="commandes")
class CommandeChannelFlag(Base):
__tablename__ = 'commandes_channels_flags'
id = Column(Integer, primary_key=True)
commande_id = Column(ForeignKey('commandes.id'))
channel_id = Column(ForeignKey('channels.id'))
flag_id = Column(ForeignKey('flags.id'))
But I have this error:
sqlalchemy.exc.InvalidRequestError: Mapper 'Mapper|Commande|commandes' has no property 'channels'
I understand that I have an error in my tables linking but I can't find it.
back_populates needs to match the exact name of the related property on the other model. In Channel, you have back_populates="channels", but in Commande, you have:
channel = relationship("Channel", secondary="commandes_channels_flags", back_populates="commandes")
Instead, change channel = relationship to channels = relationship.
You'll also need to change the other relationship properties to Flag.commandes, Flag.channels, Channel.commandes, Channel.flags, and Commande.flags to match your back_populates arguments.

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