Relationship to "jump" through M2M relationships in SqlAlchemy (User -> Role -> Permission) - python

I am trying to model a very typical permission structure where a User model (the human user of my website) can be assigned to some Roles, and each of those Roles has some Permissions.
It would be really helpful if I could get a relationship from User straight to Permission. That way, I could fetch a user (instance) from the database and just do user.permissions to get his permissions, put some filtering to check if a user has a specific Permission, have them pre-loaded... in sum: all the goodies that come with a relationship.
A viewonly relationship would be perfectly fine. As mentioned by Mike Bayer in a very similar question, I can't be writing to User.permissions because we don't not know which 'Role' to use or where to insert it.
I have created two intermediate tables:
User -- M2M --> Role(s) -- M2M --> Permission(s)
| ^
+-------- user.permissions -----------+
users_roles to connect the user to its roles through their primary key (the ID)
and roles_permissions to connect each role to its permissions,
This is my table structure (simplified for the question, but even the full version is really typical and... simple)
class User(DeclarativeBase, Mixin):
__tablename__ = 'users'
email = Column(String(25), nullable=False)
_users_roles = Table('users_roles', DeclarativeBase.metadata,
Column('user_id', ForeignKey('users.id', ...
Column('role_id', ForeignKey('roles.id', ...
PrimaryKeyConstraint('user_id', 'role_id',),
)
class Role(DeclarativeBase, Mixin):
__tablename__ = 'roles'
name = Column(Text(), nullable=False, unique=True)
users = relationship("User", secondary=_users_roles, backref="roles")
_roles_permissions = Table('roles_permissions', DeclarativeBase.metadata,
Column('role_id', ForeignKey('roles.id', ...
Column('permission_id', ForeignKey('permissions.id', ...
PrimaryKeyConstraint('role_id', 'permission_id',),
)
class Permission(DeclarativeBase, Mixin):
__tablename__ = 'permissions'
key = Column(Text, nullable=False, unique=True,)
I saw this other answer that looks very promising, yet I don't seem to be able to make it work. Honestly, I've been trying a lot of combinations and I'd say that the furthest I got was with this:
permissions = relationship('Permission',
secondary="""join(User, users_roles,
User.id == users_roles.c.user_id
).join(roles_permissions,
users_roles.c.role_id == roles_permissions.c.role_id
).join(
Permission, roles_permissions.c.permission_id == Permission.id
)""",
viewonly=True,
)
Which gives me this error:
sqlalchemy.exc.ArgumentError: Relationship User.permissions
could not determine any unambiguous local/remote column pairs
based on join condition and remote_side arguments. Consider
using the remote() annotation to accurately mark those elements
of the join condition that are on the remote side of the relationship.
Any hints would be greatly appreciated. Thank you in advance.

The secondary should not include the related tables themselves, but just the association between them:
permissions = relationship(
'Permission',
secondary="""join(users_roles, roles_permissions,
users_roles.c.role_id == roles_permissions.c.role_id)""",
viewonly=True,
)
Having the to-be-joined tables in the secondary confuses the automation that is trying to find the foreign key relationships between User and Permission, through the secondary.

Related

Custom operations when creating an SQLAlchemy object

Say I have a set of users, a set of games, and I track whether a user has finished a game in a separate table (name: 'game_progress'). I want it to be that whenever a user is created, the 'game_progress' table is auto-populated with her ID and a 'No' against all the available games. (I know that I can wait until she starts a game to create the record, but, I need this for an altogether different purpose.) How would I go about doing this?
I tried using the after_insert() event. But, then I can't retrieve the ID of the User to insert into 'game_progress'. I don't want to use after_flush (even if I can figure out how to do it) because it may be a bit of an overkill, as the user creation operation doesn't happen that often.
class Game(db.Model):
__tablename__ = 'games'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(30))
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(30))
class GameProgress(db.Model):
__tablename__ = 'game_progress'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
game_id = db.Column(db.Integer, db.ForeignKey('games.id'), primary_key=True)
game_finished = db.Column(db.Boolean)
#event.listens_for(User, "after_insert")
def after_insert(mapper, connection, target):
progress_table = GameProgress.__table__
user_id = target.id
connection.execute(
progress_table.insert().\
values(user_id=user_id, game_id=1, game_finished=0)
)
db.create_all()
game = Game(name='Solitaire')
db.session.add(game)
db.session.commit()
user = User(name='Alice')
db.session.add(user)
db.session.commit()
You don't need to do anything fancy with triggers or event listeners at all, you can just set up the relations and then make related objects in the constructor for User. As long as you have defined relationships (which you're not doing at present, you'd only added the foreign keys), then you don't need User to have an id to set up the associated objects. Your constructor can just do something like this:
class User(db.Model):
def __init__(self, all_games, **kwargs):
for k,v in kwargs.items():
setattr(self, k, v)
for game in all_games:
self.game_progresses.append( GameProgress(game=game, \
user=self, game_finished=False) )
When you commit the user, you'll also commit a list of GameProgress objects, one for each game. But the above depends on you setting up relationships on all your objects. You need to add the below to GameProgress class
game = relationship("Game", backref="game_progresses")
user = relationship("User", backref="game_progresses")
And pass in a list of games to user when you make your user:
all_games = dbs.query(Game).all()
new_user = User(all_games=all_games, name="Iain")
Once that's done you can just add GameProgress objects to the instrumented list user.game_progresses and you don't need to have committed anything before the first commit. SQLA will chase through all the relationships. Basically any time you need to muck with an id directly, ask yourself if you're using the ORM right, you rarely need to. The ORM tutorial on the SQLA docs goes through this very well. There are lots of options you can pass to relationships and backrefs to get the cascading doing what you want.

Properly cascade delete with sqlalchemy association proxy

I have a self-referential relationship in sqlalchemy that is based heavily on the example found in this answer.
I have a table of users, and an association table that links a primary user to a secondary user. User A can be primary for user B, and B may or may not also be a primary for user A. It works exactly like the twitter analogy in the answer I linked above.
This works fine, except that I don't know how to establish cascade rules for an association proxy. Currently, if I delete a user, the association record remains, but it nulls out any FKs to the deleted user. I would like the delete to cascade to the association table and remove the record.
I also need to be able to disassociate users, which would only remove the association record, but would propagate to the "is_primary_of" and "is_secondary_of" association proxies of the users.
Can anyone help me figure out how to integrate these behaviors into the models that I have? Code is below. Thanks!
import sqlalchemy
import sqlalchemy.orm
import sqlalchemy.ext.declarative
import sqlalchemy.ext.associationproxy
# This is the base class from which all sqlalchemy table objects must inherit
SAModelBase = sqlalchemy.ext.declarative.declarative_base()
class UserAssociation(SAModelBase):
__tablename__ = 'user_associations'
# Columns
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
# Foreign key columns
primary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id', name='user_association_primary_user_fk'))
secondary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id', name='user_association_secondary_user_fk'))
# Foreign key relationships
primary_user = sqlalchemy.orm.relationship('User',
foreign_keys=primary_user_id,
backref='secondary_users')
secondary_user = sqlalchemy.orm.relationship('User',
foreign_keys=secondary_user_id,
backref='primary_users')
def __init__(self, primary, secondary, **kwargs):
self.primary_user = primary
self.secondary_user = secondary
for kw,arg in kwargs.items():
setattr(self, kw, arg)
class User(SAModelBase):
__tablename__ = 'users'
# Columns
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
first_name = sqlalchemy.Column(sqlalchemy.String)
last_name = sqlalchemy.Column(sqlalchemy.String)
is_primary_of = sqlalchemy.ext.associationproxy.association_proxy('secondary_users', 'secondary_user')
is_secondary_of = sqlalchemy.ext.associationproxy.association_proxy('primary_users', 'primary_user')
def associate(self, user, **kwargs):
UserAssociation(primary=self, secondary=user, **kwargs)
Turns out to be pretty straightforward. The backrefs in the original code were just strings, but they can instead be backref objects. This allows you to set cascade behavior. See the sqlalchemy documentation on backref arguments.
The only changes required here are in the UserAssociation object. It now reads:
# Foreign key columns
primary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id',
name='user_association_primary_user_fk'),
nullable=False)
secondary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id',
name='user_association_associated_user_fk'),
nullable=False)
# Foreign key relationships
primary_user = sqlalchemy.orm.relationship('User',
foreign_keys=primary_user_id,
backref=sqlalchemy.orm.backref('secondary_users',
cascade='all, delete-orphan'))
secondary_user = sqlalchemy.orm.relationship('User',
foreign_keys=secondary_user_id,
backref=sqlalchemy.orm.backref('primary_users',
cascade='all, delete-orphan'))
The backref keyword argument is now a backref object instead of a string. I was also able to make the foreign key columns non-nullable, since it now cascades deleted users such that the associations are deleted as well.

ArgumentError in joinedload

I have these models:
class User(UserMixin, db.Model):
__tablename__ = 'users_user'
...
country = db.Column(db.Integer, db.ForeignKey('countries.id'))
class Country(db.Model):
__tablename__ = 'countries'
id = db.Column(db.Integer, primary_key=True)
...
user_country = db.relationship('User', backref='user_country', lazy='joined')
I am trying this query:
User.query.options(joinedload(Country.user_country)).filter_by(id=current_user.get_id()).first()
That will throw this error:
ArgumentError: Can't find property 'user_country' on any entity specified in this Query.
Note the full path from root (Mapper|User|users_user) to target entity must be specified.
What is wrong here?
The joinedload here is unnecessary.
By default relationships are lazily-loaded. This causes additional SELECT queries to be issued to retrieve the data. joinedload is one of the ways to force the relationship to be eagerly loaded by using a JOIN instead.
In this case, however, you've defaulted the relationship between User and Country to use eager loading by specifying lazy='joined'. This would reduce your query to
User.query.filter(id=current_user.get_id()).first()
While this will help you with the ArgumentError, we can go a little further. The query itself is unnecessary as well. current_user already has the data for its related Country because of the eager join. Accessing current_user.user_country will not send any additional queries to the database.

Automatically create related model

With SQLAlchemy I have a 1-1 relationship:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
class UserProfile(Base):
__tablename__ = 'user_profiles'
user_id = Column(Integer, ForeignKey(User.id), primary_key=True)
user = relationship(User,
backref=backref('profile', uselist=False),
foreign_keys=did)
It's required that every User always has an associated UserProfile, even if created like this:
user = User()
session.add(user)
session.commit(user)
Is there a way to automatically create and associate a related entity?
Currently, I'm doing it this way:
#event.listens_for(User, 'init')
def on_user_init(target, args, kwargs):
if not target.profile:
target.profile = UserProfile()
However this sometimes results in
IntegrityError: (IntegrityError) duplicate key value violates unique
constraint "user_profile_pkey" DETAIL: Key (user_id)=(1) already
exists. 'INSERT INTO user_profiles ...
since UserProfile is assigned to a User which already exists.
ORM Event "before_insert" is not applicable since the docs clearly state it is not allowed to add new or modify current instances.
Any better way to achieve that?
The quick-and-easy way is to use "init" event, but filter out instances which have a primary key. Note that at the moment of instance initialization, it's empty, and kwargs argument contains the fields SqlAlchemy (or your code) is going to set on it:
#event.listens_for(User, 'init')
def on_user_init(target, args, kwargs):
if not target.profile and 'id' not in kwargs:
target.profile = UserProfile()
This still won't work when you save instances with id set manually (e.g. User(id=1)), but will cover most of other cases.
As for now, I don't see a better way.

How to declared one-to-many if there are 2 fields for a same foreign key

I'm new to python(sqlalchemy), and I'm learning to build web site with pylons and sqlalchemy.
I have a problem when I declare the relationship between models. I've tried it several hours, but failed. But I think it should be a basic question.
I have two classes: User and Article, user can create articles, and modified the other people's article(like wiki).
So a user has created-articles and edited-articles.
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = ...
user_id = Column(Integer, ForeignKey('users.id'))
editor_id = Column(Integer, ForeignKey('users.id'))
# relations
user = relationship('User', backref='articles') # -> has error
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(20))
def __init__(self):
pass
But there is an error displayed:
InvalidRequestError: One or more mappers failed to compile. Exception was probably suppressed within a hasattr() call. Message was: Could not determine join condition between parent/child tables on relationship Article.user. Specify a 'primaryjoin' expression. If this is a many-to-many relationship, 'secondaryjoin' is needed as well.
I tried to add primaryjoin to the line('has error'), but don't know what it should be. I tried some codes, but none works.
Thank you in advance!
Ah, thats obvious one.
Article class has two references to User, user_id and editor_id, so SQLA does not know which one of them to use for your relation. Just use explicit primaryjoin:
user = relation('User', backref='articles', primaryjoin="Article.user_id==User.id")

Categories