Query many-to-many in SQLAlchemy - python

I have the following models. A user has many roles, and a role can have many permissions. I can't quite figure out how to query to get what I want.
user_role = db.Table(
'user_role',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
db.UniqueConstraint('user_id', 'role_id')
)
role_permission = db.Table(
'role_permission',
db.Column('permission_id', db.Integer, db.ForeignKey('permission.id')),
db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
db.UniqueConstraint('permission_id', 'role_id')
)
class Role(Base):
__tablename__ = 'role'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class Permission(Base):
__tablename__ = 'permission'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
roles = db.relation(Role, secondary=role_permission, backref=db.backref('permissions'))
class User(Base, UserMixin):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(60), unique=True, nullable=False)
password_hash = db.Column(db.String(80), nullable=False)
roles = db.relation(Role, secondary=user_role, backref=db.backref('users'))
I want to get a (preferably unique) list of all the permissions assigned to a user, but I can't seem to figure out how to do it.
I can get the list by creating a generator in the User model:
def get_all_permissions(self):
for role in self.roles:
for perm in role.permissions:
yield perm
But I'd love to be able to do it in one query.

Well, to just get a list of the permissions, try something like this:
permissions = session.query(Permission).\
join(Role).join(User).filter(User.username='MisterX').all()
Or filter for whatever you want. To make the Permissions unique, you could use group by:
permissions = session.query(Permission.id, Permission.name).join(Role).join(User).\
filter(User.username='MisterX').group_by(Permission.id).all()
Or, without a special query, use the declarative extension, if you can:
permissions = User.roles.permissions
Does that help?

Possibly your association tables are not recognized properly, as you did not specify the metadata parameter. This script works for me:
#!/bin/python
from sqlalchemy import Table
from sqlalchemy import Integer, String, ForeignKey, create_engine, Column, PrimaryKeyConstraint
from sqlalchemy.orm import relationship, backref, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///:memory:', echo=True)
Base = declarative_base()
user_role = Table(
'user_role',
Base.metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id')),
PrimaryKeyConstraint('user_id', 'role_id')
)
role_permission = Table(
'role_permission',
Base.metadata,
Column('permission_id', Integer, ForeignKey('permissions.id')),
Column('role_id', Integer, ForeignKey('roles.id')),
PrimaryKeyConstraint('permission_id', 'role_id')
)
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True)
name = Column(String(100), unique=True, nullable=False)
class Permission(Base):
__tablename__ = 'permissions'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
roles = relationship("Role", secondary=role_permission, backref=backref('permissions'))
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(60), unique=True, nullable=False)
password_hash = Column(String(80), nullable=False)
roles = relationship("Role", secondary=user_role, backref=backref('users'))
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine)()
u = User(username="user", password_hash="secret")
r1 = Role(name="Role 1")
session.add(r1)
r2 = Role(name="Role 2")
session.add(r2)
p1 = Permission(name="Permission 1")
p2 = Permission(name="Permission 2")
p3 = Permission(name="Permission 3")
r1.permissions.append(p1)
r1.permissions.append(p2)
r2.permissions.append(p2)
r2.permissions.append(p3)
u.roles.append(r1)
u.roles.append(r2)
session.add(u)
for perm in session.query(Permission).join(Role, Permission.roles).\
join(User, Role.users).filter(User.username=="user").distict()all():
print(perm.name)

If you have loaded User object already into memory together with Permissions and Roles, you code should do it quickly and without going to a database.
Otherwise, this query should work:
user_id = 789
permissions = (db.session.query(Permission)
.join(Role, Permission.roles)
.join(User, Role.users)
.filter(User.id == user_id)
).distinct()
#print(permissions)
for perm in permissions:
print(perm)

Related

How to query two association tables in Flask-SQLAlchemy?

I have two tables: courses and students, and two association tables: registered_students and attended_students. I am trying to query the two association tables to find the students who registered and attended a course. I found answers to how to query one association table, but I have two. I'm quite new to SQLAlchemy, so the complexity of it is beyond me.
Here are my models:
class Course(db.Model):
__tablename__ = 'courses'
__table_args__ = (db.UniqueConstraint('topic', 'date', name='topic_date_unique'),)
id = db.Column(db.Integer, primary_key=True)
topic = db.Column(db.String(64), nullable=False)
date = db.Column(db.DateTime, nullable=False)
registered_students = db.relationship('Student', secondary=registrations, lazy='dynamic',
backref=db.backref('registered_course', lazy='dynamic'))
attended_students = db.relationship('Student', secondary=attendances, lazy='dynamic',
backref=db.backref('attended_course', lazy='dynamic'))
class Student(db.Model):
__tablename__ = 'students'
__table_args__ = (db.UniqueConstraint('email', 'phone', name='email_phone_unique'), )
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), nullable=False)
email = db.Column(db.String(64), nullable=False)
phone = db.Column(db.Integer, nullable=False)
registrations = db.Table('registrations',
db.Column('course_id', db.Integer, db.ForeignKey('courses.id'), primary_key=True),
db.Column('student_id', db.Integer, db.ForeignKey('students.id'), primary_key=True)
)
attendances = db.Table('attendances',
db.Column('course_id', db.Integer, db.ForeignKey('courses.id'), primary_key=True),
db.Column('student_id', db.Integer, db.ForeignKey('students.id'), primary_key=True)
)
In the end, I decided to go with a no ORM solution. Maybe it's not the best, but it works for me. If you know how to do it in Flask-SQLAlchemy, please share.
def get_registered_and_attended_students_for(course_id):
connection = sqlite3.connect(DATABASE)
cursor = connection.cursor()
return cursor.execute(f"""SELECT registrations.student_id FROM registrations INNER JOIN attendances ON registrations.course_id = attendances.course_id
WHERE registrations.student_id = attendances.student_id AND attendances.course_id = {course_id}""").fetchall()

sqlalchemy throws an error when using foreign_key

Hi can someone help with this sqlalchemy code. When i invoke this code, it throws this error
~/.local/lib/python3.8/site-packages/sqlalchemy/sql/base.py:425: SAWarning: Can't validate argument 'foreign_key'; can't locate any SQLAlchemy dialect named 'foreign'
util.warn(
Code follows:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Base = declarative_base()
class Users(Base):
__tablename__ = 'users'
email = Column(String, primary_key=True, nullable=False)
firstname = Column(String)
lastname = Column(String)
userid = Column(String, unique=True, nullable=False)
class Account(Base):
__tablename__ = 'account'
userid = Column(Integer, foreign_key='Users.userid', nullable=False)
internal_account_id = Column(Integer, primary_key=True, nullable=False)
account_id = Column(String, nullable=False)

sqlachemy Many to Many: remove is not working and delete is too much

I am working on a project including to link two class: User and Group.
A user can be in some groups, and each group can have some users.
So, I created a third table : group_guest, to hold user_id and group_id.
When I create user1, user2, group1 and group2, I can add to user1 the group1 and the group2.
And add to user2 the group1
Issue: then I cannot remove only the group1 from user1
And delete group1 is not fine : user2 has no more group :/
I have tried almost every combinaisons: cascade, backref, delete orphan...
If someone as an idea... Should I modify the model ?
I would be gratefull !
code:
from app import db
from flask_login import UserMixin
group_guest = db.Table('group_guest',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('group_id', db.Integer, db.ForeignKey('group.id'))
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
group_guest_group_id = db.relationship(
"Group",
secondary=group_guest,
back_populates="group_guest_user_id",
cascade="all, delete",
lazy='dynamic'
)
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
entitled = db.Column(db.String(64))
group_guest_user_id = db.relationship(
"User",
secondary=group_guest,
back_populates="group_guest_group_id",
lazy='dynamic'
)
Then :
user1.group_guest_group_id.remove(group1)
db.session.commit()
user1.guested_group().all()
It should (in my mind) return only group2 but it return the both
It seems that you want an asymmetric relationship across your table:
If you delete a User then obviously all user-group secondary references to that User should also be deleted, i.e. cascaded in the secondary table.
But you do not want to be able to delete a Group if at least one User is present in the group, deleting an active Group raises an integrity error.
In that case you can try the following:
group_guest = db.Table('group_guest',
db.Column('user_id', db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), primary_key=True),
db.Column('group_id', db.Integer, db.ForeignKey('group.id', ondelete='RESTRICT'), primary_key=True)
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
group_guest_group_id = db.relationship("Group",
secondary=group_guest,
back_populates="group_guest_user_id",
passive_deletes='all', lazy='dynamic'
)
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
entitled = db.Column(db.String(64))
group_guest_user_id = db.relationship("User",
secondary=group_guest,
back_populates="group_guest_group_id",
passive_deletes='all', lazy='dynamic'
)
Passive deletes prevents SQLAlchemy from auto-executing cascading deletions and the Database low-level instructions specified by 'ondelete' of the foreignKey dictate the action directly.
Since the primary key on the secondary table cannot be NULL then you cannot delete the parent Group anyway. I just like the ondelete='Restrict' line since it explictly states the rules and can be easier to maintain/debug later on if you ever revisit it.
edited
the following works for me without issue:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
import uuid
Base= declarative_base()
group_guest= Table('group_guest', Base.metadata,
Column('user_id', Integer, ForeignKey('user.id', ondelete='CASCADE'), primary_key=True),
Column('group_id', Integer, ForeignKey('group.id', ondelete='RESTRICT'), primary_key=True))
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
username = Column(String(64), index=True, unique=True)
group_guest_group_id = relationship("Group",
secondary=group_guest,
back_populates="group_guest_user_id",
passive_deletes='all', lazy='dynamic'
)
class Group(Base):
__tablename__ = 'group'
id = Column(Integer, primary_key=True)
entitled = Column(String(64))
group_guest_user_id = relationship("User",
secondary=group_guest,
back_populates="group_guest_group_id",
passive_deletes='all', lazy='dynamic'
)
e = create_engine("sqlite://")
Base.metadata.create_all(e)
s = Session(e)
u1 = User(username='A')
u2 = User(username='B')
g1 = Group()
g2 = Group()
s.add_all([u1, u2, g1, g2])
s.commit()
u1.group_guest_group_id.append(g1)
u1.group_guest_group_id.append(g2)
g1.group_guest_user_id.append(u2)
g2.group_guest_user_id.append(u2)
s.add_all([u1, u2, g1, g2])
s.commit()
u1.group_guest_group_id.remove(g1)
s.add(u1)
s.commit()
print([group for group in u1.group_guest_group_id])
print([group for group in u2.group_guest_group_id])

many to many relationship with three tables (sql-alchemy)

I am trying to get a many to many relationship working. I have three tables
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
class Groups(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(1000))
class Members(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
group_id = db.Column(db.Integer, db.ForeignKey('groups.id'))
I would like to have the option group.members, which should give me all User objects which are member of that group. I implemented it the following way
members = db.relationship('User', secondary="join(Members, User, Members.user_id == User.id)", primaryjoin="and_(Groups.id == Members.group_id)")
this seems to work, but when I delete a group it gives me (sometimes) the error
AttributeError: 'Join' object has no attribute 'delete'
so I guess this is not the right way to implement such a relation.
Any ideas how to do this correctly?
thanks
carl
Perhaps a simpler way to implement this is as follows (adapted from the documentation on Flask-SQLAlchemy
members = db.Table('members',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('group_id', db.Integer, db.ForeignKey('groups.id'))
)
class Groups(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(1000))
members = db.relationship('User', secondary=members, backref=db.backref('group', lazy='dynamic'))
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
Instead of using a Model for the joining table (members), let's just use a simple table.
With this configuation, you can easily add/remove members and groups:
u = User(username='matt')
g = Groups(name='test')
db.session.add(u)
db.session.add(g)
db.session.commit()
g.members.append(u)
db.session.commit()
db.session.delete(g)
db.session.commit()

Flask-SQLAlchemy Many-to-Many with composite key

I am trying to build a Many-to-Many relationship using Flask-SQLAlchemy using two primary keys from one model and one from another. My models are the following:
Service:
class Service(db.Model):
"""
Service model object
"""
name = db.Column(db.String(120), primary_key=True, nullable=False)
description = db.Column(db.Text)
ContactClosure:
class ContactClosure(db.Model):
module = db.Column(db.Integer, primary_key=True, nullable=False)
relay = db.Column(db.Integer, primary_key=True, nullable=False)
status = db.Column(db.Boolean)
description = db.Column(db.Text)
#Relationships
hostname = db.Column(db.String(120), db.ForeignKey('ip2cc.hostname'), primary_key=True, nullable=False)
device_name = db.Column(db.String(120), db.ForeignKey('device.name'), primary_key=True, nullable=False)
services = db.relationship('Service', secondary=cc_services, backref=db.backref('contact_closures', lazy='dynamic'))
This is the related table:
cc_services = db.Table('cc_services',
db.Column('service_name', db.String(120), db.ForeignKey('service.name')),
db.Column('hostname', db.String(120), db.ForeignKey('contact_closure.hostname')),
db.Column('device_name', db.String(120), db.ForeignKey('contact_closure.device_name')),
)
And this is the error I am getting:
"AmbiguousForeignKeysError: Could not determine join condition between
parent/child tables on relationship ContactClosure.services - there
are multiple foreign key paths linking the tables via secondary table
'cc_services'. Specify the 'foreign_keys' argument, providing a list
of those columns which should be counted as containing a foreign key
reference from the secondary table to each of the parent and child
tables."
If anybody can tell what is the problem here I will be highly thankful, I've been stuck on this for a while...
Ok, finally I found the solution to this problem:
cc_services = db.Table('cc_services',
db.Column('service_name', db.String(120),
db.ForeignKey('service.name')),
db.Column('cc_hostname', db.String(120)),
db.Column('cc_module', db.Integer),
db.Column('cc_relay', db.Integer),
ForeignKeyConstraint(
('cc_hostname', 'cc_module', 'cc_relay'),
('contact_closure.hostname', 'contact_closure.module', 'contact_closure.relay')
)
)
If a model has multiple keys, they must be declared on the helper table on a ForeignKeyConstraint statement.
You need to define 'foreign_keys' argument as the error text says, like this:
services = db.relationship('Service', secondary=cc_services, backref=db.backref('contact_closures', lazy='dynamic'), foreign_keys=[column_name])
If you're using an association table or fully declared table metadata, you can use the primary_key=True in both columns, as suggested here.
Association table example:
employee_role = db.Table(
"employee_role",
db.Column("role_id", db.Integer, db.ForeignKey("role.id"), primary_key=True),
db.Column("employee_id", db.Integer, db.ForeignKey("agent.id"), primary_key=True),
)
Metadata example:
# this is using SQLAlchemy
class EmployeeRole(Base):
__tablename__ = "employee_role"
role_id = Column(Integer, primary_key=True)
employee_id = Column(Integer, primary_key=True)
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class EmployeeRole(db.Model):
__tablename__ = "employee_role"
role_id = db.Column(db.Integer, primary_key=True)
employee_id = db.Column(db.Integer, primary_key=True)
Alembic migration for it:
op.create_table(
'employee_role',
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('employee_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('role_id', 'employee_id')
)
SQL:
CREATE TABLE agent_role (
role_id INTEGER NOT NULL,
employee_id INTEGER NOT NULL,
PRIMARY KEY (role_id, employee_id)
);
In terms of relationship, declare it on one side (this should give you role.employees or employee.roles which should return a list):
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Employee(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
roles = db.relationship("Role", secondary=employee_role, backref="employee")
Your Role class can be:
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Role(db.Model):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(25), nullable=False, unique=True)

Categories