I have user who can have his favorite series and there are episodes which have series as foreign key and I am trying to retrieve all episodes from favorite series of user.
I am using Flask-SQLAlchemy.
Database:
db = SQLAlchemy(app)
# cross table for user-series
favorite_series = db.Table('favorite_series',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('series_id', db.Integer, db.ForeignKey('series.id'))
)
# user
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
favorite_series = db.relationship('Series', secondary=favorite_series,
backref=db.backref('users', lazy='dynamic'))
# series
class Series(db.Model):
__tablename__ = 'series'
id = db.Column(db.Integer, primary_key=True)
# episode
class Episode(db.Model):
__tablename__ = 'episode'
id = db.Column(db.Integer, primary_key=True)
series_id = db.Column(db.Integer, db.ForeignKey('series.id'))
series = db.relationship('Series',
backref=db.backref('episodes', lazy='dynamic'))
Friend helped me with SQL
select user_id,series.name,episode.name from (favorite_series left join series on favorite_series.series_id = series.id) left join episode on episode.series_id = series.id where user_id=1;
Altough, I want it in SQLAlchemy API, but can't manage to get it working.
EDIT:
My final working result:
episodes = Episode.query.filter(Episode.series_id.in_(x.id for x in g.user.favorite_series)).filter(Episode.air_time!=None).order_by(Episode.air_time)
First of all you don't seem to be declaring your table names?
Also, the whole point of bothering with orm is so you never have to write sql queries:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import orm
import sqlalchemy as db
Base = declarative_base()
favorite_series = db.Table('favorite_series', Base.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('User.id')),
db.Column('series_id', db.Integer, db.ForeignKey('Series.id'))
)
class Episode(Base):
__tablename__ = 'Episode'
id = db.Column(db.Integer, primary_key=True)
season = db.Column(db.Integer)
episode_num = db.Column(db.Integer)
series_id = db.Column(db.Integer, db.ForeignKey('Series.id'))
def __init__(self, season, episode_num, series_id):
self.season = season
self.episode_num = episode_num
self.series_id = series_id
def __repr__(self):
return self.series.title + \
' S' + str(self.season) + \
'E' + str(self.episode_num)
class Series(Base):
__tablename__ = 'Series'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
episodes = orm.relationship('Episode', backref='series')
def __init__(self, title):
self.title = title
def __repr__(self):
return self.title
class User(Base):
__tablename__ = 'User'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
favorite_series = orm.relationship('Series',
secondary=favorite_series, backref='users')
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
Now you can just access the attributes of your objects and let sql alchemy deal with keeping you DB in sync and issuing queries.
engine = db.create_engine('sqlite:///:memory:')
session = orm.sessionmaker(bind=engine)()
Base.metadata.create_all(engine)
lt = User('Ludovic Tiako')
the_wire = Series('The Wire')
friends = Series('Friends')
session.add_all([lt, the_wire, friends])
session.commit() # need to commit here to generate the id fields
tw_s01e01 = Episode(1,1,the_wire.id)
tw_s01e02 = Episode(1,2,the_wire.id)
f_s01e01 = Episode(1,1,friends.id)
f_s01e02 = Episode(1,2,friends.id)
f_s01e03 = Episode(1,3,friends.id)
session.add_all([tw_s01e01, tw_s01e02,
f_s01e01, f_s01e02, f_s01e03])
session.commit()
the_wire.episodes # > [The Wire S1E1, The Wire S1E2]
friends.episodes # > [Friends S1E1, Friends S1E2, Friends S1E3]
Finally, to answer your question:
lt.favorite_series.append(the_wire)
session.commit()
lt.favorite_series # > [The Wire]
[s.episodes for s in lt.favorite_series] # >> [[The Wire S1E1, The Wire S1E2]]
I don't know about Flask, but from the docs of Flask-SQLAlchemy, it seems it uses declarative, so the ORM. And so, you should have a session. I think it is accessible to you from db.session.
Anyway, if those assumptions are true, this is how you should do it:
query = db.session.query(User.id, Series.name, Episode.name).filter((Episode.series_id == Series.id) & \
(User.id == favorite_series.c.user_id) & (Series.id == favorite_series.c.id) & \
(User.id == 1))
results = query.all();
It might not be the exact query you provided, but should do the same.
UPDATE: I just checked Flask-SQLALchemy code on github, it seems that db is an instance of SQLAlchemy, which has a session attribute, created by self.session = self.create_scoped_session(session_options) which returns a session object. So this should work.
Also, not that by doing that, you won't be using their BaseQuery, although I don't know what that would mean...
Check the documentation to know what to do exactly.
Related
I have a one to many relationship between a Project and a Note (i.e. a project will have many notes but a note belongs to a single project) in my Flask app:
class BaseDocument(db.Model):
__abstract__ = True
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, onupdate=datetime.now)
archived = db.Column(db.Boolean, default=False)
def __repr__(self):
return str(self.__dict__)
class Project(BaseDocument):
__tablename__ = "project"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
parent_id = db.Column(db.Integer, db.ForeignKey("project.id"))
notes = db.relationship(
"Note",
backref="project",
lazy=True,
order_by="Note.updated_at",
cascade="all, delete, delete-orphan",
)
class Note(BaseDocument):
__tablename__ = "note"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
content = db.relationship(
"Bullet", backref="note", lazy=True, order_by="Bullet.order"
)
project_id = db.Column(db.Integer, db.ForeignKey("project.id"))
I would like to do something that seems to be very simple but I can't figure out how: I want to update the archived property of all the child notes in a project to True if the parent project archived property is also set to True.
I can only find answers here in StackOverfow about how to update the parent object if a child is updated (the oposite of what I am trying to do), so I am assuming that what I want to do is trivial and I am just bad at sqlalchemy. How can I set this up? Do I need to use a after_update event on Project? If yes, how can I access all the child Notes and set archived=True for all of them?
I have tried to setup the following event listener with no success, I get the following error AttributeError: 'InstrumentedList' object has no attribute 'update':
#db.event.listens_for(Project, "after_update")
def archive_notes(mapper, connection, target):
obj = target.object
connection.execute(target.notes.update(archived=True))
Any help will be very appreciated!
You're on the right track by using after_update. Here's a working example:
import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, relationship
connection_uri = (
"mssql+pyodbc://#localhost:49242/myDb?driver=ODBC+Driver+17+for+SQL+Server"
)
engine = sa.create_engine(
connection_uri,
future=True,
echo=False,
)
Base = declarative_base()
class Project(Base):
__tablename__ = "project"
id = sa.Column(sa.Integer, primary_key=True)
title = sa.Column(sa.Unicode(100), nullable=False)
archived = sa.Column(sa.Boolean, nullable=False, default=False)
class ProjectNote(Base):
__tablename__ = "project_note"
id = sa.Column(sa.Integer, primary_key=True)
project_id = sa.Column(sa.Integer, sa.ForeignKey("project.id"))
project = relationship(Project)
note_text = sa.Column(sa.Unicode(255), nullable=False)
archived = sa.Column(sa.Boolean, nullable=False, default=False)
#sa.event.listens_for(Project, "after_update")
def archive_remaining_project_notes(mapper, connection, target):
if target.archived:
sql = """\
UPDATE project_note SET archived = :yes
WHERE project_id = :proj_id
AND archived = :no
"""
connection.execute(
sa.text(sql),
{"yes": True, "no": False, "proj_id": target.id},
)
# <just for testing>
Base.metadata.drop_all(engine, checkfirst=True)
Base.metadata.create_all(engine)
# </just for testing>
p1 = Project(title="project 1")
p1n1 = ProjectNote(
project=p1, note_text="project 1, note 1, archived", archived=True
)
p1n2 = ProjectNote(project=p1, note_text="project 1, note 2, not archived")
with sa.orm.Session(engine, future=True) as session:
session.add_all([p1, p1n1, p1n2])
session.commit()
print(f"p1n2.archived is: {p1n2.archived}") # p1n2.archived is: False
p1.archived = True
session.commit()
print(f"p1.archived is: {p1.archived}") # p1.archived is: True
print(f"p1n2.archived is: {p1n2.archived}") # p1n2.archived is: True
I want to attach a column of a related model to another model and use it like any other sqlalchemy column attribute. As mentioned in docs, association_proxy makes it easy to use.
from sqlalchemy.ext.associationproxy import association_proxy
from app import db
class User(db.Model):
id = db.Column(...)
name = db.Column(...)
class Book(db.Model):
author_id = db.Column(...)
author = db.relationship('User', foreign_keys=[author_id])
author_name = association_proxy('author', 'name')
But when I call Book.query.order_by(Book.author_name.asc()).all() I got this:
psycopg2.errors.SyntaxError: syntax error at or near "ASC"
LINE 4: ...user.id = book.author_id AND user.name ASC)
Sqlalchemy generates EXISTS statement:
...
EXISTS (SELECT 1
FROM user
WHERE user.id = book.author_id AND user.name ASC)
So how to use ordering properly with an associated attribute?
In this case it is probably best to use a column_property:
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer(), primary_key=True)
name = Column(Unicode())
class Book(Base):
__tablename__ = 'book'
id = Column(Integer(), primary_key=True)
author_id = Column(Integer(), ForeignKey('user.id'), nullable=False)
author = relationship('User')
Book.author_name = column_property(select([User.name]).where(User.id == Book.author_id))
# sample ORM query
books = session.query(Book).order_by(Book.author_name)
Here is my model:
user_map = Table(
"user_map",
Column('user_id', Integer, ForeignKey('user.id'), primary_key=True),
Column('map_id', Integer, ForeignKey('map.id'), primary_key=True),
PrimaryKeyConstraint('user_id', 'map_id', name='pk_user_map')
)
class Map(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
owner_id = Column(Integer, ForeignKey('user.id'))
shared_maps = relationship(
'User',
secondary=user_map,
backref=backref('maps', lazy='dynamic')
)
class User(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String, unique=True)
shared_maps = Map.query.filter(Map.shared_maps.any()).all()
I want to query the user_map table, using the join condition "Map.id == user_map.map_id", but SQLAlchemy is trying to join using "Map.id == user_map.map_id and Map.owner_id == user_map.user_id". How can I specify my join condition?
I tried to use primaryjoin attribute in the relationship and to specify the condition inside the .join() but without success. Thanks in advance!
Based on your code, I've rebuilt the setup; I guess your relationships were mixed up. Furthermore, I've hardly ever seen primary keys (or PrimaryKeyConstraints) in sqlalchemy's many-to-many association tables. It may make sense from a non-orm perspective, but as far as I know, it is unusual or even not required at all.
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
Base = declarative_base()
UsersXMaps = sa.Table(
'users_x_maps',
Base.metadata,
sa.Column('user', sa.Integer, sa.ForeignKey('users.id')),
sa.Column('map', sa.Integer, sa.ForeignKey('maps.id'))
)
class User(Base):
__tablename__ = 'users'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
mail = sa.Column(sa.String, unique=True)
own_maps = relationship('Map', back_populates='owner')
maps = relationship(
'Map',
secondary=UsersXMaps,
back_populates='users'
)
def __str__(self):
return '{} ({}) with {} maps'.format(
self.name, self.mail, len(self.own_maps))
class Map(Base):
__tablename__ = 'maps'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
owner_id = sa.Column(sa.Integer, sa.ForeignKey('users.id'))
owner = relationship('User', back_populates='own_maps')
users = relationship(
'User',
secondary=UsersXMaps,
back_populates='maps'
)
def __str__(self):
return '{} (by {})'.format(self.name, self.owner.name)
So far for the setup; I've extended it a bit for proper output when printing strings. Additionally, your Map.shared_maps relationship actually refers to Users, not Maps, so I also renamed that one.
When binding your association table to the two classes, you can refer to it from both sides (even though back_populates seems to overwrite/replace the original definition) - this simplifies joins from either side.
Executing the following works as expected:
if __name__ == '__main__':
engine = sa.create_engine('sqlite:///usermaps.db')
sfactory = sessionmaker(engine)
session = sfactory()
Base.metadata.create_all(bind=engine)
bilbo = User(id=1, name='Bilbo', mail='bilbo#shire.nz')
frodo = User(id=2, name='Frodo', mail='frodo#shire.nz')
mordor = Map(id=1, name='Mordor', owner=frodo, users=[bilbo, frodo])
gondor = Map(id=2, name='Gondor', owner=bilbo, users=[bilbo, frodo])
rohan = Map(id=3, name='Rohan', owner=bilbo, users=[bilbo, frodo])
session.add_all([frodo, bilbo, mordor, gondor, rohan])
session.commit()
print('Maps by owner:')
for owner in [bilbo, frodo]:
print(owner)
for item in session.query(Map).filter(Map.owner == owner).all():
print(' - ' + str(item))
print('Maps by users:')
for item in session.query(Map).filter(Map.users.any()).all():
print(' - ' + str(item))
The output is:
Maps by owner:
Bilbo (bilbo#shire.nz) with 2 maps
- Gondor (by Bilbo)
- Rohan (by Bilbo)
Frodo (frodo#shire.nz) with 1 maps
- Mordor (by Frodo)
Maps by users:
- Mordor (by Frodo)
- Gondor (by Bilbo)
- Rohan (by Bilbo)
I'm trying to batch delete objects from an association table by filtering on a column in one of the relationships. I use the following call in SQLAlchemy to make the delete
db.session.query(UserPaper).join(Paper, (UserPaper.paper_id ==
Paper.id)).filter(UserPaper.user_id == user.id).filter(Paper.journal_id
== journal.id).delete()
and it results in the following error
OperationalError: (OperationalError) (1054, "Unknown column 'papers.journal_id'
in 'where clause'") 'DELETE FROM userpapers WHERE userpapers.user_id = %s AND
papers.journal_id = %s' (1L, 1L)
Without the delete at the end, the SQLAlchemy query is
SELECT userpapers.user_id AS userpapers_user_id, userpapers.paper_id AS
userpapers_paper_id, userpapers.created AS userpapers_created,
userpapers.read_at AS userpapers_read_at, userpapers.score AS userpapers_score
FROM userpapers JOIN papers ON userpapers.paper_id = papers.id
WHERE userpapers.user_id = :user_id_1 AND papers.journal_id = :journal_id_1
which is correct. From the error I can see that when I append delete() to the query the join part of SQL statement gets lost and the database doesn't know how to find the papers.journal_id column obviously. What I don't understand is why does that happen?
This is the setup of my ORM objects
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
papers = db.relationship("UserPaper", backref=db.backref('users'), lazy='dynamic')
class Paper(db.Model):
__tablename__ = 'papers'
id = db.Column(db.Integer, primary_key = True)
title = db.Column(db.String(1024))
journal_id = db.Column(db.Integer, db.ForeignKey('journals.id'))
class UserPaper(db.Model):
__tablename__ = 'userpapers'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
paper_id = db.Column(db.Integer, db.ForeignKey('papers.id'), primary_key=True)
paper = db.relationship("Paper", backref=db.backref('user_paper'))
read_at = db.Column(db.DateTime)
score = db.Column(db.Integer)
class Journal(db.Model):
__tablename__ = 'journals'
id = db.Column(db.Integer, primary_key = True)
title = db.Column(db.String(100), index = True, unique = True)
papers = db.relationship('Paper', backref = 'journal', lazy = 'dynamic')
I had the same problem with SQLALchemy 0.9 using MySQL 5.6. It looks like a bug/limitation. However, one better way to get arround (in comparison to creating the query, looping through the results and deleting them one by one) is to perform this task in two subsequent queries:
paperQuery = db.session.query(Paper.id)\
filter(Paper.journal_id == journal.id)
baseQuery = db.session.query(UserPaper)\
.filter(UserPaper.paper_id.in_(paperQuery.subquery()))
.filter(UserPaper.user_id == user.id).delete(synchronize_session='fetch')
It worked well for me, it should solve you issue too.
In a Pyramid application I'm working on, I have the following scenario:
class Widget(Base):
__tablename__ = 'widgets'
id = Column(Integer, primary_key=True)
name = Column(String(50))
sidebar = Column(mysql.TINYINT(2))
def __init__(self, name, sidebar):
self.name = name
self.sidebar = sidebar
class Dashboard(Base):
__tablename__ = 'dashboard'
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
widget_id = Column(Integer, ForeignKey('widgets.id'), primary_key=True)
delta = Column(mysql.TINYINT)
widget = relationship('Widget')
def __init__(self, user_id, widget_id, delta):
self.user_id = user_id
self.widget_id = widget_id
self.delta = delta
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
login = Column(Unicode(255), unique=True)
password = Column(Unicode(60))
fullname = Column(Unicode(100))
dashboard = relationship('Dashboard', order_by='Dashboard.widget.sidebar, Dashboard.delta')
def __init__(self, login, password, fullname):
self.login = login
self.password = crypt.encode(password)
self.fullname = fullname
So, I want the User 'dashboard' relationship to have the dashboard records for the user but ordered by 'sidebar' (which is a relationship property of Dashboard). Currently I am getting this error:
sqlalchemy.exc.InvalidRequestError: Property 'widget' is not an instance of ColumnProperty (i.e. does not correspond directly to a Column).
Is this ordering possible in a relationship declaration?
Thanks!
With this, try to think what SQL SQLAlchemy should emit when it tries to load User.dashboard. Like SELECT * FROM dashboard JOIN widget ... ORDER BY widget.sidebar ? Or SELECT * FROM dashboard ORDER BY (SELECT sidebar FROM widget... ? ordering the results by a different table is too open-ended of a job for relationship() to decide on it's own. The way this can be done is by providing a column expression in terms of Dashboard that can provide this ordering, when the ORM emits a simple SELECT against dashboard's table, as well as when it refers to it in a not-so-simple SELECT where it might be joining across User, Dashboard tables at once (e.g. eager loading).
We provide custom SQL expressions, particularly those that involve other tables, using column_property(), or alternatively with deferred() when we don't want that expression to be loaded by default (as is likely the case here). Example:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Widget(Base):
__tablename__ = 'widgets'
id = Column(Integer, primary_key=True)
name = Column(String(50))
sidebar = Column(Integer)
class Dashboard(Base):
__tablename__ = 'dashboard'
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
widget_id = Column(Integer, ForeignKey('widgets.id'), primary_key=True)
delta = Column(Integer)
widget = relationship('Widget')
widget_sidebar = deferred(select([Widget.sidebar]).where(Widget.id == widget_id))
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
login = Column(Unicode(255), unique=True)
dashboard = relationship('Dashboard', order_by='Dashboard.widget_sidebar, Dashboard.delta')
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
w1, w2 = Widget(name='w1', sidebar=1), Widget(name='w2', sidebar=2)
s.add_all([
User(login='u1', dashboard=[
Dashboard(
delta=1, widget=w1
),
Dashboard(
delta=2, widget=w2
)
]),
])
s.commit()
print s.query(User).first().dashboard
the final SQL emitted by the load of ".dashboard" is:
SELECT dashboard.user_id AS dashboard_user_id, dashboard.widget_id AS dashboard_widget_id, dashboard.delta AS dashboard_delta
FROM dashboard
WHERE ? = dashboard.user_id ORDER BY (SELECT widgets.sidebar
FROM widgets
WHERE widgets.id = dashboard.widget_id), dashboard.delta
Keep in mind that MySQL does a terrible job optimizing for subqueries like the one above. If you need high performance here, you might consider copying the value of "sidebar" into "dashboard", even though that makes consistency more difficult to maintain.