SQLAlchemy many-to-many with single link for multiple tables - python

I'm writing a SQLAlchemy based ORM for a database where multiple pairs of tables are connected by a single link table. This link table has four columns: from_table_name, from_table_key, to_table_key, to_table_key.
I want to set up a many to many relationship between two tables in the database. Normally I would do something like:
class Table1(Base):
__tablename__ = 'table_1'
id = Column(Integer, primary_key=True)
# some other columns here
data_from_table_2 = relationship('Table2', secondary='link')
class Table2(Base):
__tablename__ = 'table_2'
id = Column(Integer, primary_key=True)
# some other columns here
data_from_table_1 = relationship('Table1', secondary='link')
class Link(Base):
__tablename__ = 'link'
table_1_id = Column(Integer, ForeignKey('table_1.id'), primary_key=True)
table_2_id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
However, this doesn't work in the given case because I need to specify not just the key in my link table, but also the name of the table being related.
For inspiration, I've been looking through the SQLAlchemy docs on relationships here but haven't found anything useful. Does anyone know how to set up a many to many relationship between the two tables above?

As pointed out in the comments to this question, the presented database schema is a confusing design. However, the premise of the question indicates that the schema was created externally and it is up to the developer to come up with an ORM.
The requested relationship can bee implemented manually via class properties and custom queries. For example:
class Table1(Base):
__tablename__ = 'table_1'
id = Column(Integer, primary_key=True)
#property
def data_from_table_2(self):
with Session() as session:
return session.query(Reference).filter(
Link.table_1_id == self.id and
Link.table_1_name == self.__tablename__
).all()
Note that depending on your use case, you may want to use the session attached to the database object
#property
def data_from_table_2(self):
session = object_session(self)
return session.query(Reference).filter(
Link.table_1_id == self.id and
Link.table_1_name == self.__tablename__
).all()

Related

SQLAlchemy Join to retrieve data from multiple tables

I'm trying to retrieve data from multiple tables with SQLAlchemy using the .join() method.
When I run the query I was expecting to get a single object back which had all the data from the different tables joined so that I could use a.area_name and so on where area_name is on one of the joined tables. Below is the query I am running and the table layout, if anyone could offer insight into how to achieve the behavior I'm aiming for I would greatly appreciate it! I've been able to use the .join() method with this same syntax to match results and return them, I figured it would return the extra data from the rows as well since it joins the tables (perhaps I'm misunderstanding how the method works or how to retrieve the information via the query object?).
If it helps with the troubleshooting I'm using MySQL as the database
query:
a = User.query.filter(User.user_id==1).join(UserGroup,
User.usergroup==UserGroup.group_id).join(Areas, User.area==Areas.area_id).first()
and the tables:
class User(db.Model):
user_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True)
usergroup = db.Column(db.Integer, db.ForeignKey('user_group.group_id'), nullable=False)
area = db.Column(db.Integer, db.ForeignKey('areas.area_id'), nullable=False)
class UserGroups(db.Model):
id = db.Column(db.Integer, primary_key=True)
group_id = db.Column(db.Integer, nullable=False, unique=True)
group_name = db.Column(db.String(64), nullable=False, unique=True)
class Areas(db.Model):
id = db.Column(db.Integer, primary_key=True)
area_id = db.Column(db.Integer, nullable=False, unique=True)
area_name = db.Column(db.String(64), nullable=False, unique=True)
So it seems that I need to use a different approach to the query, and that it returns a tuple of objects which I then need to parse.
What worked is:
a = db.session.query(User, UserGroups, Areas
).filter(User.user_id==1
).join(UserGroup,User.usergroup==UserGroup.group_id
).join(Areas, User.area==Areas.area_id
).first()
The rest remaining the same. This then returned a tuple that I could parse where the data from User is a[0], from UserGroups is a[1], and Areas is a[2]. I can then access the group_name column with a[1].group_name etc.
Hopefully this helps someone else who's trying to work with this!
Take a look at SQLAlchemy's relationship function:
http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#one-to-many
You may want to add a new attribute to your User class like so:
group = sqlalchemy.relationship('UserGroups', back_populates='users')
This will automagically resolve the one-to-many relationship between User and UserGroups (assuming that a User can only be member of one UserGroup at a time). You can then simply access the attributes of the UserGroup once you have queried a User (or set of Users) from your database:
a = User.query.filter(...).first()
print(a.group.group_name)
SQLAlchemy resolves the joins for you, you do not need to explicitly join the foreign tables when querying.
The reverse access is also possible; if you just query for a UserGroup, you can access the corresponding members directly (via the back_populates-keyword argument):
g = UserGroup.query.filter(...).first()
for u in g.users:
print(u.name)

How to create a field with a list of foreign keys in SQLAlchemy?

I am trying to store a list of models within the field of another model. Here is a trivial example below, where I have an existing model, Actor, and I want to create a new model, Movie, with the field Movie.list_of_actors:
import uuid
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.schema import ForeignKey
rom sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Actor(Base):
__tablename__ = 'actors'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String)
nickname = Column(String)
academy_awards = Column(Integer)
# This is my new model:
class Movie(Base):
__tablename__ = 'movies'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String)
# How do I make this a list of foreign keys???
list_of_actors = Column(UUID(as_uuid=True), ForeignKey('actors.id'))
I understand that this can be done with a many-to-many relationship, but is there a more simple solution? Note that I don't need to look up which Movie's an Actor is in - I just want to create a new Movie model and access the list of my Actor's. And ideally, I would prefer not to add any new fields to my Actor model.
I've gone through the tutorials using the relationships API, which outlines the various one-to-many/many-to-many combinations using back_propagates and backref here: http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html But I can't seem to implement my list of foreign keys without creating a full-blown many-to-many implementation.
But if a many-to-many implementation is the only way to proceed, is there a way to implement it without having to create an "association table"? The "association table" is described here: http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#many-to-many ? Either way, an example would be very helpful!
Also, if it matters, I am using Postgres 9.5. I see from this post there might be support for arrays in Postgres, so any thoughts on that could be helpful.
Update
It looks like the only reasonable approach here is to create an association table, as shown in the selected answer below. I tried using ARRAY from SQLAlchemy's Postgres Dialect but it doesn't seem to support Foreign Keys. In my example above, I used the following column:
list_of_actors = Column('actors', postgresql.ARRAY(ForeignKey('actors.id')))
but it gives me an error. It seems like support for Postgres ARRAY with Foreign Keys is in progress, but still isn't quite there. Here is the most up to date source of information that I found: http://blog.2ndquadrant.com/postgresql-9-3-development-array-element-foreign-keys/
If you want many actors to be associated to a movie, and many movies be associated to an actor, you want a many-to-many. This means you need an association table. Otherwise, you could chuck away normalisation and use a NoSQL database.
An association table solution might resemble:
class Actor(Base):
__tablename__ = 'actors'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String)
nickname = Column(String)
academy_awards = Column(Integer)
class Movie(Base):
__tablename__ = 'movies'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String)
actors = relationship('ActorMovie', uselist=True, backref='movies')
class ActorMovie(Base):
__tablename__ = 'actor_movies'
actor_id = Column(UUID(as_uuid=True), ForeignKey('actors.id'))
movie_id = Column(UUID(as_uuid=True), ForeignKey('movies.id'))
If you don't want ActorMovie to be an object inheriting from Base, you could use sqlachlemy.schema.Table.

How to create an SQLAlchemy nested filter

Say I have a schema of photo tagging, in postgreSQL.
It is pretty simple:
a user has many photos
a photo has many tags
I'm using SQL alchemy and trying to understand how to compose a filter to find, and delete, all the tags of all the photos of a specific user.
I know how to go about finding all the photos of a user:
specific_user_id = "1234"
DBSession = sessionmaker(bind=engine)
s = DBSession()
q = s.query(Photo).filter(Photo.user_id == specific_user_id)
q.delete()
How do I extend this to get all the tags. I can use a loop:
for photo in q.all():
q2 = s.query(Tag).filter(Tag.photo_id == photo.photo_id)
q2.delete()
But I'm looking to do this without the loop.
Assuming the models look something like this:
class Photo(Base):
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
user = relationship(User, backref='photos')
class Tag(Base):
id = Column(Integer, primary_key=True)
photo_id = Column(Integer, ForeignKey(Photo.id), nullable=False)
photo = relationship(Photo, backref='tags')
Each model has a foreign key to their "owner" (user, photo), and a relationship to that model. You can write a query with joins along the relationship to get all the tags for all a user's photos.
tags = session.query(Tag).join(
Tag.photo, Photo.user
).filter(User.id == specific_user_id)
for tag in tags:
session.delete(tag)
session.commit()
Use session.delete instead of query.delete because it allows SQLAlchemy to clean up behind the scenes to make sure everything is consistent according to any other relationship rules you defined.

sqlalchemy foreign key relationship attributes

I have a User table and a Friend table. The Friend table holds two foreign keys both to my User table as well as a status field. I am trying to be able to call attributes from my User table on a Friend object. For example, I would love to be able to do something like, friend.name, or friend.email.
class User(Base):
""" Holds user info """
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(25), unique=True)
email = Column(String(50), unique=True)
password = Column(String(25))
admin = Column(Boolean)
# relationships
friends = relationship('Friend', backref='Friend.friend_id',primaryjoin='User.id==Friend.user_id', lazy='dynamic')
class Friend(Base):
__tablename__ = 'friend'
user_id = Column(Integer, ForeignKey(User.id), primary_key=True)
friend_id = Column(Integer, ForeignKey(User.id), primary_key=True)
request_status = Column(Boolean)
When I get friend objects all I have is the 2 user_ids and i want to display all properties of each user so I can use that information in forms, etc. I am new to sqlalchemy - still trying to learn more advanced features. This is just a snippet from a larger Flask project and this feature is going to be for friend requests, etc. I've tried to look up association objects, etc, but I am having a hard with it.
Any help would be greatly appreciated.
First, if you're using flask-sqlalchemy, why are you using directly sqlalchemy instead of the Flask's db.Model?
I strongly reccomend to use flask-sqlalchemy extension since it leverages the sessions and some other neat things.
Creating a proxy convenience object is straightforward. Just add the relationship with it in the Friend class.
class Friend(Base):
__tablename__ = 'friend'
user_id = Column(Integer, ForeignKey(User.id), primary_key=True)
friend_id = Column(Integer, ForeignKey(User.id), primary_key=True)
request_status = Column(Boolean)
user = relationship('User', foreign_keys='Friend.user_id')
friend = relationship('User', foreign_keys='Friend.friend_id')
SQLAlchemy will take care of the rest and you can access the user object simply by:
name = friend.user.name
If you plan to use the user object every time you use the friend object specify lazy='joined' in the relationship. This way it loads both object in a single query.

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