Many-to-many relationship on same table with association object - python

Related (for the no-association-object use case): SQLAlchemy Many-to-Many Relationship on a Single Table
Building a many-to-many relationship is easy. Building a many-to-many relationship on the same table is almost as easy, as documented in the above question.
Building a many-to-many relationship with an association object is also easy.
What I can't seem to find is the right way to combine association objects and many-to-many relationships with the left and right sides being the same table.
So, starting from the simple, naïve, and clearly wrong version that I've spent forever trying to massage into the right version:
t_groups = Table('groups', metadata,
Column('id', Integer, primary_key=True),
)
t_group_groups = Table('group_groups', metadata,
Column('parent_group_id', Integer, ForeignKey('groups.id'), primary_key=True, nullable=False),
Column('child_group_id', Integer, ForeignKey('groups.id'), primary_key=True, nullable=False),
Column('expires', DateTime),
)
mapper(Group_To_Group, t_group_groups, properties={
'parent_group':relationship(Group),
'child_group':relationship(Group),
})
What's the right way to map this relationship?

I guess you are getting an error like Could not determine join condition between parent/child tables...
In this case, Change the mapper for Group_To_Group to the following:
mapper(Group_To_Group, t_group_groups, properties={
'parent_group':relationship(Group,
primaryjoin=(t_group_groups.c.parent_group_id==t_groups.c.id),),
'child_group':relationship(Group,
primaryjoin=(t_group_groups.c.child_group_id==t_groups.c.id),),
})
also you might want to add the backref so that you can navigate the relations from the Group objects as well.

Related

Correct way to implement many-to-many with additional columns in Flask-SQLAlchemy?

I need to implement many-to-many relationship with additional columns in Flask-SQLAlchemy. I am currently using association table to link two models (following this guide https://flask-sqlalchemy.palletsprojects.com/en/master/models/#many-to-many-relationships). My problem is that this relationship need to have additional attached data. My two models and table are:
log = db.Table('log',
db.Column('workout_id', db.Integer, db.ForeignKey('workout.id')),
db.Column('exercise_variant_id', db.Integer, db.ForeignKey('exercise_variant.id')),
db.Column('quantity', db.Integer, nullable=False),
db.Column('series', db.Integer, nullable=False)
)
class ExerciseVariant(db.Model):
__tablename__ = 'exercise_variant'
id = db.Column(db.Integer, primary_key=True)
class Workout(db.Model):
__tablename__ = 'workout'
id = db.Column(db.Integer, primary_key=True)
exercises = db.relationship('ExerciseVariant', secondary=log, lazy='subquery',
backref=db.backref('workouts', lazy=True))
This approach is working ok, but the current method for adding records to log table seems a bit hacky to me, since I have to first query both objects to get id I am looking for and then create custom statement:
statement = log.insert().values(
workout_id=workout.id,
exercise_variant_id=exercise_variant.id,
quantity=exercise_dict['quantity'],
series=exercise_dict['series']
)
db.session.execute(statement)
My questions are:
1. Should this kind of relationship be implemented using Table or Model?
2. If an answer to 1. is Table, can I somehow use backrefs to pass object instances instead of querying and passing their id?

How to delete a one-to-one relationship with SQLAlchemy

I would like to create a nullable, self-referencing relationship which can be deleted using SQLAlchemy. An example model is as follows (note, using Flask-SQLAlchemy):
class Person(db.Model):
__tablename__ = 'person'
id = db.Column(db.Integer, primary_key=True)
partner_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=True)
partner = db.relationship('Person', uselist=False)
So think of this as a table of cops who have only a single partner, but that partner may turn out to have been in the mafia all along, so they lose their partner for a while. A cop without a partner is fine, at least in database terms - but I assume over the course of the show their partnerless status means a lot of property damage.
Needless to say, this question: sqlalchemy: one-to-one relationship with declarative discusses how to set up this relationship. The question is how do you remove the relationship? Normally with a different foreign key you'd do this as follows:
joe.partner.remove(larry)
Where joe and larry are both Person objects. However, via the uselist argument, joe.partner is now actually a Person with no remove method.
How to delete one-to-one relationships is buried away in the SQLAlchemy documentation under the explanation of Cascades: https://docs.sqlalchemy.org/en/14/orm/cascades.html#notes-on-delete-deleting-objects-referenced-from-collections-and-scalar-relationships
The delete-orphan cascade can also be applied to a many-to-one or
one-to-one relationship, so that when an object is de-associated from its parent, it is also automatically marked for deletion. Using
delete-orphan cascade on a many-to-one or one-to-one requires an
additional flag relationship.single_parent which invokes an assertion
that this related object is not to shared with any other parent
simultaneously
So you'll want to set up your one-to-one relationship like so:
partner = db.relationship(
'Person',
cascade='all, delete-orphan',
uselist=False,
single_parent=True,
)
Then, deleting a Person's partner is just a matter of setting it to None:
some_person.partner = None
session.flush() # will delete the partner object

User defined function creation in SQLAlchemy

SQLAlchemy provides a very clean interface for defining database tables:
engine = create_engine('sqlite:///:memory:')
metadata = MetaData()
user = Table('user', metadata,
Column('user_id', Integer, primary_key = True),
Column('user_name', String(16), nullable = False),
Column('email_address', String(60), key='email'),
Column('password', String(20), nullable = False)
)
user_prefs = Table('user_prefs', metadata,
Column('pref_id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey("user.user_id"), nullable=False),
Column('pref_name', String(40), nullable=False),
Column('pref_value', String(100))
)
And once these tables have been defined, it is very easy to create these tables with the metadata.create_all(engine) function. This is especially nice for testing, when you want to create tables that will not interfere with the existing tables being used in production.
A project I am currently working on relies heavily on user defined functions in postgres. Is there a clean way to define these functions using SQLAlchemy in order for the metadata.create_all(engine) to properly create the functions along with the appropriate tables?
I've been working on something similar today. So far the best way I've found to do it is using sqlalchemy before_create event listeners. For general functions, you can bind the events to the metadata, but for table specific functions you could bind to the tables. For example:
import sqlalchemy
from sqlalchemy.schema import DDL
sqlalchemy.event.listen(
metadata,
'before_create',
DDL('CREATE OR REPLACE FUNCTION myfunc() ...')
)
You can just replace metadata with your table if you wanted to create the function before a table was created.
This DDL seems to run every time you call metadata.create_all so it's important to use CREATE OR REPLACE. If you wanted a bit more control over when functions are created, you might be better looking into migrations with alembic or similar.
Some uses of DDL are described in the sqlalchemy docs here: http://docs.sqlalchemy.org/en/latest/core/ddl.html#custom-ddl

Querying association table object directly

I have to query the relationships directly, since I have to display them seperatly.
task_user = db.Table(
'pimpy_task_user',
db.Column('task_id', db.Integer, db.ForeignKey('pimpy_task.id')),
db.Column('user_id', db.Integer, db.ForeignKey('user.id'))
)
How do I do this in SQLAlchemy?
When I try this:
tasks_rel = task_user.join(Task).join(User).filter(Task.group_id == group_id)
It causes this error:
AttributeError: 'Join' object has no attribute 'filter'
the usage you have above is creating a join() construct, which is a Core (non-ORM) construct representing a join() of two tables (but not a full blown select()).
when using the ORM you usually start off a SELECT using the Query object. Querying from the class itself is a pattern offered by extensions like flask-sqlalchemy, but these extensions are usually confusing in this regard. Given any class or table you can query against it using the Query object:
session.query(task_user).join(Task).join(User).filter(Task.group_id == group_id)

Can I use an ORM class as association table for a many-to-many realtionship in SQLAlchemy?

I want to collect statistical information for songs (rating, play-count, last time played) from a couple of players over different devices and from different users. I use Python and SQL Alchemy.
I came up with the following table layout:
I can access all related Stats objects from my Commit ORM class as a list. I also want to have access to the related Song objects from the Commit class. Following the examples in the SQLALchemy Documentation I came up with an association table for the mtm relationship.
In code it looks like this (full source):
songcommits_table = Table(
'songcommits', Base.metadata,
Column('commit_id', Integer, ForeignKey('commits.commit_id')),
Column('song_id', Integer, ForeignKey('songs.song_id'))
)
class Commit(Base):
__tablename__ = 'commits'
commit_id = Column(Integer, primary_key=True)
# ...
songs = relationship(
"Song", secondary=songcommits_table, backref="commits"
)
stats = relationship('Stat', backref='commit')
def __repr__(self):
return "<Commit {0.commit_id}>".format(self)
It works. But I have the feeling it might work without the table, using the info already stored in the stats table, but I have no idea how to formulate this in the ORM.
So how can I use the information (commit_id and song_id) already present on the Stats ORM class instead of the helper table?

Categories

Resources