Automatically create related model - python

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.

Related

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

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.

Is it possible to set a Boolean column as a unique constraint in Flask-SQLalchemy?

Is it possible to setup a table in such a way that only one record can be set to true at any given time?
I have a 'User' table and a 'Credentials' table. A User can have one or more login credentials associated to it. There should only be one "Active" credential at a time which is indicated by the "active" column(Boolean) in the Credentials table.
Is it possible to setup some kind of constraint to prevent two records from being "active" at the same time?
You can use a unique constraint to prevent 2 columns being active at the same time but you will have to use True and None as the possible values rather than True and False. This is because the unique constraint will only allow a single True in the column but would also only allow a single False as well. None (or SQL NULL) values do not participate in the unique constraint and therefore you can have as many of theses as you you have remaining rows. To ensure database integrity this is probably best achieved with a enum datatype with only a single possible value.
import enum
class Active(enum.Enum):
true = True
class Credentials(db.Model):
active = db.Column(db.Enum(Active), unique=True)
You can now to use Active.true as the value to indicate the active credential and None for all other credentials with integrity enforced at the database level. If you intended to have one active credential per user rather than one active credential in total this could be achieved with a separate UniqueConstraint statement.
class Credential(db.Model):
__tablename__ = "credentials"
__table_args__ = (db.UniqueConstraint('user_id', 'active'),)
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
user = db.relationship("User", back_populates="credentials")
active = db.Column(db.Enum(Active))
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
credentials = db.relationship("Credential", back_populates="user")
While this does prevent more than one credential being marked as active it will not prevent a user from having no active credentials and you will need to rely on your application logic for this.
So if i'm reading correctly you have two model classes User and Credential which is a one-to-many relationship:
class User(db.Model):
...
credentials = db.relationship('Credential')
class Credential(db.Model):
...
user_id = db.Column(db.Integer, db.ForeignKey('user.id', nullable=False)
Is it possible to add an additional foreignkey to signify a one-to-one relationship:
class User(db.Model):
...
active_credential_id = db.Column(db.Integer, db.ForeignKey('credential.id'))
class Credential(db.Model):
...
active_user = db.relationship('User')
You could update this with something like:
inactive_credential = # some credential from the user's list of credentials
user = User.query.filter(User.id == inactive_credential.user_id)
user.active_credential_id = inactive_credential.id
db.session.add(user)
db.session.commit()
# inactive_credential ==>> active_credential
The use of a foreign key here maintains database integrity.
You will need some additional constraint that says an active_credential_id can only be selected from the list of credentials whose user is defined by user_id. I just thought of this at the end and will update answer later if I have solution.

SAWarning: Object of type <Child> not in session, add operation along 'Parent.children' will not proceed

I'm stuck on this issue and I don't know how to fix it. This is my models.py file:
models.py
class TripDetail(db.Model):
"""
Base class for every table that contains info about a trip.
"""
__abstract__ = True
__bind_key__ = 'instructions'
id = db.Column(db.Integer, primary_key=True)
# list of fields
class Overview(TripDetail):
"""
Class that contains general information about a trip.
"""
__tablename__ = 'overviews'
__table_args__ = (
db.ForeignKeyConstraint(['user_id', 'calendar_id'], ['calendars.user_id', 'calendars.id'], ondelete='CASCADE'),
) # constraints on other tables, omitted here
user_id = db.Column(db.Integer, primary_key=True)
calendar_id = db.Column(db.Integer, primary_key=True)
calendar = db.relationship('Calendar', backref=db.backref('overviews', cascade='delete'), passive_deletes=True)
# other fields
class Step(TripDetail):
__tablename__ = 'steps'
overview_id = db.Column(db.Integer, db.ForeignKey('overviews.id', ondelete='CASCADE'))
overview = db.relationship('Overview', backref=db.backref('steps', cascade='delete'), passive_deletes=True)
# also other fields
And this is how I add items to the DB (the response parameter contains a dict that matches the classes, in such a way that it can be unpacked directly):
def add_instruction(response):
"""
Adds a travel instruction to the database.
"""
steps = response.pop('steps')
overview = Overview(**response)
for step in steps:
Step(overview=overview, **step)
db.session.add(overview)
db.session.commit()
logger.info(f"Stored instruction with PK {(overview.id, overview.user_id, overview.calendar_id, overview.event_id)}")
Now, the overviews table is filled up correctly, but steps stays empty. Inspecting the logs, I receive this warning:
SAWarning: Object of type not in session, add operation along 'Overview.steps' will not proceed
(orm_util.state_class_str(state), operation, prop))
What am I doing wrong?
Normally, when add()ing objects to a session, their related objects will get auto-added like you wanted. That behavior is controlled by the relationship's cascade.
Setting cascade to 'delete' in Steps.overview removes the default 'save-update', which is what turns on the auto-adding. You could just add it back with cascade='save-update, delete', but take a look at the possible traits and see what else you might need. A common set is 'all, delete-orphan'.
And remember these are strictly ORM behaviors; setting a 'delete' in your cascade won't set the column's ON [event] CASCADE.
Well, I've solved this by expliciting adding the created step to the session. Still have no idea what the warning means though, so I'll just leave this here. My fix:
for step in steps:
step = Step(overview=overview, **step) # explicitly add
db.session.add(step)

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.

Categories