Custom operations when creating an SQLAlchemy object - python

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.

Related

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)

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.

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.

One-to-many relationships SQLAlchemy that depend on each other

I'm trying to get the following models working together. Firstly the scenario is as follows:
A user can have many email addresses, but each email address can only be associated with one user;
Each user can only have one primary email address (think of it like their current email address).
An email address is a user's id, so they must always have one, but when they change it, I want to keep track of other ones they've used in the past. So far the setup is to have a helper table user_emails that holds a tie between an email and a user, which I hear is not supposed to be setup as a class in using the declarative SQLAlchemy approach (though I don't know why). Also, am I right in thinking that I need to use use_alter=True because the users table won't know the foreign key email_id until it's inserted?
models.py looks like this:
"""models.py"""
user_emails = Table('user_emails', Base.metadata,
Column('user_id', Integer, ForeignKey('users.id'),
primary_key=True),
Column('email', String(50), ForeignKey('emails.address'),
primary_key=True))
class User(Base):
__tablename__ = 'users'
id = Column(Integer, Sequence('usr_id_seq', start=100, increment=1),
primary_key=True)
email_id = Column(String(50),
ForeignKey('emails.address', use_alter=True, name='fk_email_id'),
unique=True, nullable=False)
first = Column(String(25), unique=True, nullable=False)
last = Column(String(25), unique=True, nullable=False)
def __init__(self, first, last):
self.first = first
self.last = last
class Email(Base):
__tablename__ = 'emails'
address = Column(String(50), unique=True, primary_key=True)
user = relationship(User, secondary=user_emails, backref='emails')
added = Column(DateTime, nullable=False)
verified = Column(Boolean, nullable=False)
def __init__(self, address, added, verified=False):
self.address = address
self.added = added
self.verified = verified
Everything seems OK until I try and commit to the DB:
>>> user = models.User("first", "last")
>>> addy = models.Email("example#example.com", datetime.datetime.utcnow())
>>> addy
<Email 'example#example.com' (verified: False)>
>>> user
>>> <User None (active: True)>
>>>
>>> user.email_id = addy
>>> user
>>> <User <Email 'example#example.com' (verified: False)> (active: True)>
>>> Session.add_all([user, addy])
>>> Session.commit()
>>> ...
>>> sqlalchemy.exc.ProgrammingError: (ProgrammingError) can't adapt type 'Email' "INSERT INTO users (id, email_id, first, last, active) VALUES (nextval('usr_id_seq'), %(email_id)s, %(first)s, %(last)s, %(active)s) RETURNING users.id" {'last': 'last', 'email_id': <Email 'example#example.com' (verified: False)>, 'active': True, 'first': 'first'}
So, I figure I'm doing something wrong/stupid, but I'm new to SQLAlchemy so I'm not sure what I need to do to setup the models correctly.
Finally, assuming I get the right models setup, is it possible to add a relationship so that by loading an arbitrary email object I'll be able to access the user who owns it, from an attribute in the Email object?
Thanks!
You have already got a pretty good solution, and a small fix will make your code work. Find below the quick feedback on your code below:
Do you need the use_alter=True? No, you actually do not need that. If the primary_key for the Email table was computed on the database level (as with autoincrement-based primary keys), then you might need it when you have two tables with foreign_keys to each other. In your case, you even do not have that because you have a third table, so for any relationship combination the SA (sqlalchemy) will figure it out by inserting new Emails, then Users, then relationships.
What is wrong with your code?: Well, you are assigning an instance of Email to User.email_id which is supposed to get the email value only. There are two ways how you can fix it:
Assign the email directly. so change the line user.email_id = addy to user.email_id = addy.address
Create a relationship and then make the assignment (see code below).
Personally, I prefer the option-2.
Other things: your current model does not check that the User.email_id is actually one of the User.emails. This might be by design, but else just add a ForeignKey from [users.id, users.email_id] to [user_emails.user_id, user_emails.email]
Sample code for version-2:
""" models.py """
class User(Base):
__tablename__ = 'users'
# ...
email_id = Column(String(50),
ForeignKey('emails.address', use_alter=True,
name='fk_email_id'), unique=True,
nullable=False)
default_email = relationship("Email", backref="default_for_user")
""" script """
# ... (all that you have below until next line)
# user.email_id = addy.address
user.default_email = addy
I'm not familiar with Python/SQLAlchemy, but here is one way to represent what you want in the database:
You'd either use deferred constraints (if your DBMS supports them), of leave USER.PRIMARY_EMAIL NULL-able (as shown in the model above) to break the data modification cycle.
Alternatively, you could do something like this:
E-mails belonging to the same user are ordered (note the alternate key on: {USER_ID, ORDER}), and whichever e-mail is on top of that ordering can be considered "primary". The nice thing about this approach is that it completely avoids the circular reference.
The examples in http://docs.sqlalchemy.org/en/latest/orm/tutorial.html are close to what you want. I never use an intermediate join table like your user_emails unless I need a many-to-many relationship. user-to-email should be a one-to-many.
For your need to keep track of old email addresses? Add an "obsolete" Boolean attribute to your Email class and filter on that to show current or old email addresses.

Categories