I'm trying to establish a many-to-many relationship with only one table and an association table in between. The problem I'm facing is that appending to the relationship in one object doesn't back populate in the corresponding object unless I make the relationship directed. What I want in this case is to make the relationship undirected though.
undirected
if PERSON A is friends with PERSON B, PERSON B is friends with PERSON A as well
directed
if PERSON A follows PERSON B, PERSON B has a follower in PERSON A
from sqlalchemy import Table, ForeignKey, Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
engine = create_engine('postgresql+psycopg2://deniz#localhost:54321/sqla', echo=False)
Base = declarative_base(bind=engine)
Session = sessionmaker(bind=engine)
friends_association = Table(
'friends_association', Base.metadata,
Column('person1_id', ForeignKey('person.id'), primary_key=True),
Column('person2_id', ForeignKey('person.id'), primary_key=True)
)
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
name = Column(String)
friends = relationship('Person', back_populates='friends', secondary=friends_association,
primaryjoin=friends_association.c.person1_id == id,
secondaryjoin=friends_association.c.person2_id == id
)
def __repr__(self) -> str:
return f"<Person {self.name}>"
Base.metadata.create_all()
ben = Person(name='Ben')
deniz = Person(name='Deniz')
paul = Person(name='Paul')
deniz.friends.extend([ben, paul])
session = Session()
session.add(deniz)
session.commit()
persons = session.query(Person)
for person in persons:
print((f"Person {person.name} has friends {person.friends}"))
OUTPUT:
Person Deniz is friends with [<Person Ben>, <Person Paul>]
Person Ben is friends with []
Person Paul is friends with []
if I change the undirected 'friends'-relationship attribute of the Person class to a directed follower-following-relationship like this, the data is back populated correctly.
class Person(Base):
...
followers = relationship('Person', backref='following', secondary=friends_association,
primaryjoin=friends_association.c.person1_id == id,
secondaryjoin=friends_association.c.person2_id == id
)
persons = session.query(Person)
for person in persons:
print((f"Person {person.name} has followers {person.followers} and is following {person.following}"))
OUTPUT:
Person Deniz has followers [<Person Ben>, <Person Paul>] and is following []
Person Ben has followers [] and is following [<Person Deniz>]
Person Paul has followers [] and is following [<Person Deniz>]
Related
In my database there is a Many-To-Many relationship between users and projects. A user can be member of multiple projects and a project can have multiple members.
I want know if a given user is member of a given project.
I currently solve this by querying for both the project and the user and by then checking if the user is contained in the projects member list. Here is my (working) code:
def is_project_member(db: Session, project_id: int, user_id: int):
project = db.query(dms.Project).filter(dms.Project.id == project_id).first()
user = db.query(dms.User).filter(dms.User.id == user_id).first()
return user in project.members
Can this be done more efficiently? Might it possible to directly query the relationship table?
Here my database models:
project_user_table = Table(
"project_user_association",
Base.metadata,
Column("project_id", ForeignKey("projects.id")),
Column("user_id", ForeignKey("users.id"))
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
projects = relationship("Project",
secondary=project_user_table, back_populates="members")
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True)
members = relationship("User", secondary=project_user_table,
back_populates="projects")
I'm trying to join two tables.
Here are my tables simplified
class Spots(db.Model):
__tablename__ = "spots"
geobaseid = db.Column((db.Integer), primary_key=True,autoincrement=True)
spot = db.Column(db.String(50))
url_slug = db.Column(db.String(50))
region = db.Column(db.String(50))
country = db.Column(db.String(50))
chop = db.Column(db.Integer)
small_wave = db.Column(db.Integer)
flat = db.Column(db.Integer)
big_wave = db.Column(db.Integer)
west_direction = db.Column(db.Integer)
southwest_direction = db.Column(db.Integer)
amount = db.Column(db.Integer)
url_slug = db.Column(db.String(50))
forcast = db.relationship('Forcast_short')
class Forcast_short(db.Model):
__tablename__ = "forcast_short"
id = db.Column((db.Integer), primary_key=True,autoincrement=True)
date = db.Column(db.Date)
geobaseid = db.Column((db.Integer), ForeignKey('spots.geobaseid'))
spot = db.Column(db.String(50))
region = db.Column(db.String(50))
country = db.Column(db.String(50))
latitude = db.Column(db.Numeric(10,8))
longitude = db.Column(db.Numeric(10,8))
deg = db.Column(db.Numeric(65,1))
mps = db.Column(db.Numeric(65,1))
Here's my query that is not working
forcast_query = Forcast_short.query.join(Spots, Spots.geobaseid==Forcast_short.geobaseid).filter(Forcast_short.region == region).all()
What am I doing wrong?
When I run the query I only get results from Forcast_short with and without the filter.
<tbody>
<tr>{%for row in forcast_query%}
<td> {{row.spot}} </td>
<td>{{row.mps}} </td>
<td>{{row.url_slug}} </td>
</tr>
{%endfor%}
</tbody>
This query works in Mysql workbench.
select * from
(
SELECT * FROM sc_db2.forcast_short
) a
join
(
select * from sc_db2.spots
) b
on a.geobaseid = b.geobaseid
;
The way you made your models is quite confusing. In keeping with the documentation of sqlalchemy on Relationship Patterns, which I advise you to read, here is how you should set up a one to many relationship:
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
children = relationship("Child")
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
This relationship places a foreign key on the child table referencing the parent. relationship() is then specified on the parent, as referencing a collection of items represented by the child.
ABOUT YOUR QUERY:
After harmonizing your models, your request should look like this:
forcast_query = Forcast_short.query.join(Spots, Spots.geobaseid==Forcast_short.geobaseid).filter(Forcast_short.region == region).filter(Forcast_short.date == date_1).all()
Here's a shorter example which illustrates a one-to-many relationship between spots and forecasts (which I think is what you're trying to do):
from app import db
class Spot(db.Model):
id = db.Column(db.Integer, primary_key=True)
geobaseid = db.Column(db.Integer)
forecasts = db.relationship('Forecast', backref='spot', lazy='dynamic')
class Forecast(db.Model):
id = db.Column(db.Integer, primary_key=True)
region = db.Column(db.String(50))
spot_id = db.Column(db.Integer, db.ForeignKey('spot.id'))
The db object is set up in the app package initialiser, following Miguel Grinberg's pattern:
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from app import models
Note that having set up the foreign key relationship, you don't need an explicit join to access the parent's fields from the child object - SQLAlchemy allows you to just use dot notation:
>>> from app import db
>>> from app.models import Spot, Forecast
>>> for spot in Spot.query.all():
... print(spot.id, spot.geobaseid)
...
1 1
2 2
>>> for forecast in Forecast.query.all():
... print(forecast.id, forecast.region, forecast.spot_id, forecast.spot.geobaseid)
...
1 Scotland 2 2
2 England 2 2
There are 3 tables: Account, Role, User. Both Role and User have a foreign key account_id that points to Account.
A user can have multiple roles, hence the roles_users table which acts as the secondary relation table between Role and User.
The Account table is a tenant table for our app, it is used to separate different customers.
Note that all tables have (besides Account) have composite primary keys with account_id. This is done for a few reasons, but let's say it's done to keep everything consistent.
Now if I have a simple secondary relationship (User.roles - the one that is commented out) all works as expected. Well kind of.. it throws a legitimate warning (though I believe it should be an error):
SAWarning: relationship 'User.roles' will copy column role.account_id to column roles_users.account_id, which conflicts with relationship(s): 'User.roles' (copies user.account_id to roles_users.account_id). Consider applying viewonly=True to read-only relationships, or provide a primaryjoin condition marking writable columns with the foreign() annotation.
That's why I created the second relation User.roles - the one that is not commented out. Querying works as expected which has 2 conditions on join and everything. However I get this error when I try to save some roles on the user:
sqlalchemy.orm.exc.UnmappedColumnError: Can't execute sync rule for source column 'roles_users.role_id'; mapper 'Mapper|User|user' does not map this column. Try using an explicit `foreign_keys` collection which does not include destination column 'role.id' (or use a viewonly=True relation).
As far as I understand it, SA is not able to figure out how to save the secondary because it has a custom primaryjoin and secondaryjoin so it proposes to use viewonly=True which has the effect of just ignoring the roles relation when saving the model.
The question is how to save the roles for a user without having to do it by hand (the example is commented out in the code). In the real app we have many secondary relationships and we're saving them in many places. It would be super hard to rewrite them all.
Is there a solution to keep using User.roles = some_roles while keeping the custom primaryjoin and secondaryjoin below?
The full example using SA 1.1.9:
from sqlalchemy import create_engine, Column, Integer, Text, Table, ForeignKeyConstraint, ForeignKey, and_
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import foreign, relationship, Session
Base = declarative_base()
class Account(Base):
__tablename__ = 'account'
id = Column(Integer, primary_key=True)
roles_users = Table(
'roles_users', Base.metadata,
Column('account_id', Integer, primary_key=True),
Column('user_id', Integer, primary_key=True),
Column('role_id', Integer, primary_key=True),
ForeignKeyConstraint(['user_id', 'account_id'], ['user.id', 'user.account_id']),
ForeignKeyConstraint(['role_id', 'account_id'], ['role.id', 'role.account_id']),
)
class Role(Base):
__tablename__ = 'role'
id = Column(Integer, primary_key=True)
account_id = Column(Integer, ForeignKey('account.id'), primary_key=True)
name = Column(Text)
def __str__(self):
return '<Role {} {}>'.format(self.id, self.name)
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
account_id = Column(Integer, ForeignKey('account.id'), primary_key=True)
name = Column(Text)
# This works as expected: It saves data in roles_users
# roles = relationship(Role, secondary=roles_users)
# This custom relationship - does not work
roles = relationship(
Role,
secondary=roles_users,
primaryjoin=and_(foreign(Role.id) == roles_users.c.role_id,
Role.account_id == roles_users.c.account_id),
secondaryjoin=and_(foreign(id) == roles_users.c.user_id,
account_id == roles_users.c.account_id))
engine = create_engine('sqlite:///')
Base.metadata.create_all(engine)
session = Session(engine)
# Create our account
a = Account()
session.add(a)
session.commit()
# Create 2 roles
u_role = Role()
u_role.id = 1
u_role.account_id = a.id
u_role.name = 'user'
session.add(u_role)
m_role = Role()
m_role.id = 2
m_role.account_id = a.id
m_role.name = 'member'
session.add(m_role)
session.commit()
# Create 1 user
u = User()
u.id = 1
u.account_id = a.id
u.name = 'user'
# This does not work
u.roles = [u_role, m_role]
session.add(u)
session.commit()
# Works as expected
i = roles_users.insert()
i = i.values([
dict(account_id=a.id, role_id=u_role.id, user_id=u.id),
dict(account_id=a.id, role_id=m_role.id, user_id=u.id),
])
session.execute(i)
# re-fetch user from db
u = session.query(User).first()
for r in u.roles:
print(r)
NOTE: Switching primaryjoin with secondaryjoin does not help.
Solution for posterity sake - switch foreign wrappers and careful with primary vs secondary joins:
Instead of this:
roles = relationship(
Role,
secondary=roles_users,
primaryjoin=and_(foreign(Role.id) == roles_users.c.role_id,
Role.account_id == roles_users.c.account_id),
secondaryjoin=and_(foreign(id) == roles_users.c.user_id,
account_id == roles_users.c.account_id))
Do this:
roles = relationship(
Role,
secondary=roles_users,
primaryjoin=and_(id == foreign(roles_users.c.user_id), account_id == foreign(roles_users.c.account_id)),
secondaryjoin=and_(Role.id == foreign(roles_users.c.role_id), Role.account_id == roles_users.c.account_id),
)
How can I implement a self-referential many-to-many relationship that is effectively the union of two other relationships?
The relationship should return all FacebookFriendship models that exist between a user and other users in the network. A user may have a FacebookFriendship that points to another existing user, but due to FB API outages, privacy controls, etc, the mirror FBFriendship might not exist for the existing user to this user.
# This class is necessary for python-social-auth
# A UserSocialAuth model only exists for users who are in the network
class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin):
"""Social Auth association model"""
__tablename__ = 'social_auth_usersocialauth'
__table_args__ = (UniqueConstraint('provider', 'uid'),)
id = Column(Integer, primary_key=True)
provider = Column(String(32))
uid = Column(String(UID_LENGTH))
extra_data = Column(JSONType())
user_id = Column(
Integer, ForeignKey(User.id), nullable=False, index=True)
user = relationship(
User,
backref=backref('social_auth', lazy='dynamic')
)
This relationship finds FacebookFriendship models that point from this user to any existing user.
facebook_friendships = relationship(
FacebookFriendship,
primaryjoin=and_(
user_id == FacebookFriendship.user_id,
provider == 'facebook'
),
secondary=FacebookFriendship.__table__,
secondaryjoin=uid == FacebookFriendship.fb_uid_friend,
foreign_keys=[provider, user_id, uid],
viewonly=True,
uselist=True,
lazy='dynamic',
)
This relationship finds FacebookFriendship models that point to this user.
other_facebook_friendships = relationship(
FacebookFriendship,
primaryjoin=and_(
uid == FacebookFriendship.fb_uid_friend,
provider == 'facebook'
),
foreign_keys=[provider, uid],
viewonly=True,
uselist=True,
lazy='dynamic',
)
I was able to express the union query using the hybrid_property decorator, but this prevents usage of comparators like any() or from using association proxies, at least from what I can tell.
# Can I rewrite this using relationship()?
#hybrid_property
def all_facebook_friendships(self):
return self.facebook_friendships.union(
self.other_facebook_friendships).correlate(
FacebookFriendship)
# FBFriendship models are created for every friend that a user has,
# regardless of whether they're in the network or not.
class FacebookFriendship(Base):
__tablename__ = u'user_fb_friend'
user_id = Column(Integer, sa.ForeignKey(User.id), primary_key=True)
user = relationship(
User, backref=backref('facebook_friendships', lazy='dynamic'),
primaryjoin=User.id == user_id)
fb_uid_friend = Column(sa.String(length=255), primary_key=True)
In the end, I'd like to query this relationship like any other InstrumentedAttribute:
UserSocialAuth.query.filter(UserSocialAuth.all_facebook_friendships.any()).all()
and define an association_proxy on the User model:
User.all_facebook_friends = association_proxy('all_facebook_friendships', 'user')
Sorry for the length of this question, but I've trialed & errored to no avail for days now.
Related:
How can I achieve a self-referencing many-to-many relationship on the SQLAlchemy ORM back referencing to the same attribute?
How to create relationship many to many in SQLAlchemy (python, flask) for model User to itself
Using zzzeek's solution linked above, I created a self-referential M2M relationship by using a select statement as the "secondary" argument to relationship().
friendship_union = select([
FacebookFriendship.dater_id,
cast(FacebookFriendship.fb_uid_friend, Integer()).label(
'fb_uid_friend')
]).union(
select([
cast(FacebookFriendship.fb_uid_friend, Integer()),
FacebookFriendship.dater_id]
)
).alias()
cls.all_fb_friendships = relationship(
UserSocialAuth,
secondary=friendship_union,
primaryjoin=UserSocialAuth.user_id == friendship_union.c.dater_id,
secondaryjoin=and_(
UserSocialAuth.provider == 'facebook',
cast(UserSocialAuth.uid, Integer() ) == friendship_union.c.fb_uid_friend,
),
viewonly=True
)
In the code below I want to replace all_holdings in Account with a property called holdings that returns the desired_holdings (which are the holdings representing the latest known quantity which can change over time). I'm having trouble figuring out how to construct the call to relationship.
In addition I'd appreciate any comments on the appropriateness of the pattern (keeping historic data in a single table and using a max date subquery to get most recent), as well as on better alternatives, or improvements to the query.
from sqlalchemy import Column, Integer, String, Date, DateTime, REAL, ForeignKey, func
from sqlalchemy.orm import relationship, aliased
from sqlalchemy.sql.operators import and_, eq
from sqlalchemy.ext.declarative import declarative_base
from db import session
import datetime
import string
Base = declarative_base()
class MySQLSettings(object):
__table_args__ = {'mysql_engine':'InnoDB'}
class Account(MySQLSettings, Base):
__tablename__ = 'account'
id = Column(Integer, primary_key=True)
name = Column(String(64))
all_holdings = relationship('Holding', backref='account')
def desired_holdings(self):
max_date_subq = session.query(Holding.account_id.label('account_id'),
Holding.stock_id.label('stock_id'),
func.max(Holding.as_of).label('max_as_of')). \
group_by(Holding.account_id, Holding.stock_id).subquery()
desired_query = session.query(Holding).join(Account,
Account.id==account.id).join(max_date_subq).\
filter(max_date_subq.c.account_id==account.id).\
filter(Holding.as_of==max_date_subq.c.max_as_of).\
filter(Holding.account_id==max_date_subq.c.account_id).\
filter(Holding.stock_id==max_date_subq.c.stock_id)
return desired_query.all()
def __init__(self, name):
self.name = name
class Stock(MySQLSettings, Base):
__tablename__ = 'stock'
id = Column(Integer, primary_key=True)
name = Column(String(64))
def __init__(self, name):
self.name = name
class Holding(MySQLSettings, Base):
__tablename__ = 'holding'
id = Column(Integer, primary_key=True)
account_id = Column(Integer, ForeignKey('account.id'), nullable=False)
stock_id = Column(Integer, ForeignKey('stock.id'), nullable=False)
quantity = Column(REAL)
as_of = Column(Date)
stock = relationship('Stock')
def __str__(self):
return "Holding(%f, '%s' '%s')"%(self.quantity, self.stock.name, str(self.as_of))
def __init__(self, account, stock, quantity, as_of):
self.account_id = account.id
self.stock_id = stock.id
self.quantity = quantity
self.as_of = as_of
if __name__ == "__main__":
ibm = Stock('ibm')
session.add(ibm)
account = Account('a')
session.add(account)
session.flush()
session.add_all([ Holding(account, ibm, 100, datetime.date(2001, 1, 1)),
Holding(account, ibm, 200, datetime.date(2001, 1, 3)),
Holding(account, ibm, 300, datetime.date(2001, 1, 5)) ])
session.commit()
print "All holdings by relation:\n\t", \
string.join([ str(h) for h in account.all_holdings ], "\n\t")
print "Desired holdings query:\n\t", \
string.join([ str(h) for h in account.desired_holdings() ], "\n\t")
The results when run are:
All holdings by relation:
Holding(100.000000, 'ibm' '2001-01-01')
Holding(200.000000, 'ibm' '2001-01-03')
Holding(300.000000, 'ibm' '2001-01-05')
Desired holdings query:
Holding(300.000000, 'ibm' '2001-01-05')
Following answer provided by Michael Bayer after I posted to sqlalchemy google group:
The desired_holdings() query is pretty complicated and I'm not seeing a win by trying to get relationship() to do it. relationship() is oriented towards maintaining the persistence between two classes, not as much a reporting technique (and anything with max()/group_by in it is referring to reporting).
I would stick #property on top of desired_holdings, use object_session(self) to get at "session", and be done.
See more information on query-enabled properties.