Selecting an item by its associated tables - flask

I'm working on building a recipe database. I'm trying to build a query wehere I get all recipies that include a certain ingredient (such as onions, carrots), but I'm not how build my query. Essentally I'm trying to get a list of recipies that (given the proper amount of joins) have an Ingredient.name = 'onion'. My models are as follows:
ingredients = db.Table('ingredients',
db.Column('modified_ingredient', db.Integer, db.ForeignKey('modified_ingredient.id')),
db.Column('ingredient', db.Integer, db.ForeignKey('ingredient.id'))
)
modifiers = db.Table('modifiers',
db.Column('modified_ingredient', db.Integer, db.ForeignKey('modified_ingredient.id')),
db.Column('modifier', db.Integer, db.ForeignKey('modifier.id'))
)
modified_ingredients = db.Table('modified_ingredients',
db.Column('recipe', db.Integer, db.ForeignKey('recipe.id')),
db.Column('modified_ingredient', db.Integer, db.ForeignKey('modified_ingredient.id'))
)
class Recipe(db.Model):
__tablename__ = 'recipe'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(256))
description = db.Column(db.Text)
directions = db.Column(db.Text)
prep_time = db.Column(db.Integer)
cook_time = db.Column(db.Integer)
image = db.Column(db.LargeBinary())
ingredients = db.relationship('ModifiedIngredient', secondary=modified_ingredients)
class Ingredient(db.Model):
__tablename__ = 'ingredient'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), index=True, unique=True)
class Modifier(db.Model):
__tablename__ = 'modifier'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), index=True, unique=True)
class ModifiedIngredient(db.Model):
__tablename__ = 'modified_ingredient'
id = db.Column(db.Integer, primary_key=True)
amount = db.Column(db.Integer)
unit = db.Column(db.String(20))
ingredients = db.relationship('Ingredient', secondary=ingredients,
backref=db.backref('ingredients', lazy='dynamic'), lazy='dynamic')
modifiers = db.relationship('Modifier', secondary=modifiers,
backref=db.backref('modifiers', lazy='dynamic'), lazy='dynamic')
It's mostly my inexperience with SQL and SQLAlchemy that is stumping me. I know that I'm joining something, but I'm not exactly sure how to phrase it in a way that works.

Option-1: very tidy, but might not be the most efficient due to nested EXISTS clause:
q = (db.session.query(Recipe)
.filter(Recipe.ingredients.any(
ModifiedIngredient.ingredients.any(
Ingredient.name == 'onion')
)))
Option-2: should be faster, but if you query only certain columns (use query(Recipe.name, ..) instead of whole objects as below), you will end with with multiple results per each Recipe row because of JOINs:
q = (db.session.query(Recipe)
.join(Recipe.ingredients)
.join(Ingredient, ModifiedIngredient.ingredients)
.filter(Ingredient.name == 'onion')
)

Related

Flask_sqlalchemy many to many query

I have created the following models:
tag_post = db.Table('tag_post',
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True))
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(180))
body = db.Column(db.Text, nullable=False)
tags = db.relationship('Tag', secondary=tag_post, backref=db.backref('posts_associated', lazy="dynamic"))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
How can I query all the posts tagged with a tag, by using the tag.id?
Thanks in advance
The solution is to use a join.
You want all posts that meet a condition. The query should therefore start with the table Post.
Post.query
Then you bind the table posts with your join table using the two columns that should fit. In this case the column post_id in the tag_post table and id in the Post table. Since you use your association table directly, the columns used are referenced using the name via the attribute c.
Post.query\
.join(tag_post, tag_post.c.post_id == Post.id)
Then you filter the second column of your join table based on your condition.
tag_id = 1
Post.query\
.join(tag_post, tag_post.c.post_id == Post.id)\
.filter(tag_post.c.tag_id == tag_id)
Since you want all posts and not just one, close the request with all().
tag_id = 1
tagged_posts = Post.query\
.join(tag_post, tag_post.c.post_id == Post.id)\
.filter(tag_post.c.tag_id == tag_id)\
.all()
The following is a detailed query with the same result, which also includes the third table.
tag_id = 1
tagged_posts = Post.query\
.join(tag_post, tag_post.c.post_id == Post.id)\
.join(Tag, tag_post.c.tag_id == Tag.id)\
.filter(Tag.id == tag_id)\
.all()

Many to many relationship through association

I have the following models:
Group:
class Group(db.Model):
__tablename__ = 'Groups'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), index=True, unique=True)
description = db.Column(db.Text, nullable=False)
created_on = db.Column(db.DateTime, default=datetime.now)
updated_on = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
users = db.relationship(
"User",
secondary=users_groups_assocation_table,
back_populates="groups")
analysis = db.relationship(
"Analysis",
secondary=analysis_groups_assocation_table,
back_populates="groups"
User:
class User(db.Model):
__tablename__ = 'Users'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
alias = db.Column(db.String(120), index=True, unique=True)
name = db.Column(db.String(120), index=True, unique=False)
lastname = db.Column(db.String(120), index=True, nullable=False)
email = db.Column(db.String(120), index=True, nullable=False)
password = db.Column(db.String(120), index=False, nullable=True)
created_on = db.Column(db.DateTime, default=datetime.now)
updated_on = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
admin = db.Column(db.Boolean, default=False)
local_auth = db.Column(db.Boolean, default=False)
override_tableau = db.Column(db.Boolean, default=False)
groups = db.relationship(
"Group",
secondary=users_groups_assocation_table,
back_populates="users")
Analysis:
class Analysis(db.Model):
__tablename__ = 'Analysis'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), index=True, unique=True)
description = db.Column(db.Text, nullable=False)
embed_analysis = db.Column(db.Text, nullable=True)
service_account = db.Column(db.Text, nullable=True)
img = db.Column(db.LargeBinary(length=(2**32)-1), nullable=True)
img_mimetype = db.Column(db.Text, nullable=True)
img_name = db.Column(db.Text, nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('Categories.id'), nullable=True)
created_on = db.Column(db.DateTime, default=datetime.now)
updated_on = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
category = db.relationship("Category", back_populates="analysis")
draft = db.Column(db.Boolean, default=False)
groups = db.relationship(
"Group",
secondary=analysis_groups_assocation_table,
back_populates="analysis")
Association tables:
users_groups_assocation_table = db.Table('users_groups',
db.Column('user_id', db.Integer, db.ForeignKey('Users.id')),
db.Column('group_id', db.Integer, db.ForeignKey('Groups.id'))
)
analysis_groups_assocation_table = db.Table('analysis_groups',
db.Column('analysis_id', db.Integer, db.ForeignKey('Analysis.id')),
db.Column('group_id', db.Integer, db.ForeignKey('Groups.id'))
)
So there is an implicimit many to many relationship between Users <-> Analysis. How do I create such an association through groups? Coming from ruby on rails there is a :through keyword in this case, is there anything similar for flask sqlalchemy?
I want to have something like User.query.first().analysis
Many things are possible with sqlalchemy's relationships but I find that complex ones can be hard to get right/predictable and that one-off queries are easier to maintain (at least for complex relationships).
I attempted to solve the relationship you asked about, as User.analyses but included a one off query at the end.
I used a simplified version of joining across multiple tables in the docs here: https://docs.sqlalchemy.org/en/14/orm/join_conditions.html#composite-secondary-join
Note that the join uses a mix of Table objects that require columns referenced of .c. and mapped classes like Analysis that have columns directly referenced.
You also probably would want an order_by argument to relationship otherwise this relation would probably be meaningless.
from datetime import datetime, date
from sqlalchemy import (
create_engine,
Text,
Integer,
String,
ForeignKey,
UniqueConstraint,
update,
DateTime,
Date,
Boolean,
LargeBinary,
)
from sqlalchemy.schema import (
Table,
Column,
MetaData,
)
from sqlalchemy.sql import select
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
Base = declarative_base()
engine = create_engine("sqlite://", echo=False)
users_groups_table = Table('users_groups', Base.metadata,
Column('user_id', Integer, ForeignKey('Users.id')),
Column('group_id', Integer, ForeignKey('Groups.id'))
)
analysis_groups_table = Table('analysis_groups', Base.metadata,
Column('analysis_id', Integer, ForeignKey('Analysis.id')),
Column('group_id', Integer, ForeignKey('Groups.id'))
)
class Group(Base):
__tablename__ = 'Groups'
__table_args__ = {'extend_existing': True}
id = Column(Integer, primary_key=True)
name = Column(String(120), index=True, unique=True)
users = relationship(
"User",
secondary=users_groups_table,
back_populates="groups")
analysis = relationship(
"Analysis",
secondary=analysis_groups_table,
back_populates="groups")
class User(Base):
__tablename__ = 'Users'
__table_args__ = {'extend_existing': True}
id = Column(Integer, primary_key=True)
name = Column(String(120), index=True, unique=False)
groups = relationship(
"Group",
secondary=users_groups_table,
back_populates="users")
analyses = relationship("Analysis",
# The middle
secondary="join(users_groups, analysis_groups, users_groups.c.group_id == analysis_groups.c.group_id)",
# Join from left to the middle
primaryjoin="User.id == users_groups.c.user_id",
# Join from right to the middle
secondaryjoin="Analysis.id == analysis_groups.c.analysis_id",
uselist=True,
viewonly=True
)
class Analysis(Base):
__tablename__ = 'Analysis'
__table_args__ = {'extend_existing': True}
id = Column(Integer, primary_key=True)
name = Column(String(120), index=True, unique=True)
groups = relationship(
"Group",
secondary=analysis_groups_table,
back_populates="analysis")
Base.metadata.create_all(engine)
with Session(engine) as session:
users = {}
for name in ('a', 'b'):
users[name] = User(name=name)
session.add(users[name])
groups = {}
for name in ('a', 'b'):
groups[name] = Group(name=name)
session.add(groups[name])
analyses = {}
for name in ('x', 'y', 'z'):
analyses[name] = Analysis(name=name)
session.add(analyses[name])
groups['a'].users.append(users['a'])
groups['b'].users.append(users['b'])
analyses['x'].groups.append(groups['a'])
analyses['y'].groups.append(groups['b'])
analyses['z'].groups.append(groups['b'])
session.commit()
print (users['a'].analyses[0].name)
# One-off ad-hoc query.
q = session.query(Analysis).join(Analysis.groups).join(Group.users).filter(User.id == users['a'].id)
print (q)
res = q.first()
print (res.name)

How to add relationship instances of a model on sqlalchemy's before_insert event?

I want to add instances of a model's relationship when an instance of this model is created.
While before_insert sqlalchemy event allows to set simple attribute, it does not seem to work with relationship.
Is it possible to do that with this event ? What is the standard way to achieve that ?
audio_project_rel = db.Table(
'audio_project_rel',
db.Column('project_id', db.Integer, db.ForeignKey('project.id'), primary_key=True),
db.Column('audio_id', db.Integer, db.ForeignKey('audio.id'), primary_key=True)
)
class Audio(db.Model):
id = db.Column(db.Integer, primary_key=True)
path = db.Column(db.String, unique=True, nullable=False)
class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True, nullable=False)
audiolist_filename = db.Column(db.String, nullable=False)
audios = db.relationship('Audio',
secondary=audio_project_rel,
lazy=True,
backref=db.backref('projects', lazy=True))
#event.listens_for(Project, 'before_insert')
def get_audiolist_from_file(mapper, connection, project):
with open(project.audiolist_filename, 'r') as audiolist_file:
for line in audiolist_file:
_path = line.strip()
audio = Audio.query.filter(Audio.path==_path).first()
if not audio:
audio = Audio()
audio.path = _path
project.audios.append(audio) # not added
project.name = 'somename' # added
It seems that it is not possible:
https://docs.sqlalchemy.org/en/latest/orm/session_events.html#session-persistence-mapper

Select many to many using function count()

I have many to many relationships and i try to find User which has a minimum requests im my subs table but i can't understand how i can do it.
Could you please clarify how i can do it
my Models are:
subs = db.Table('subs',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('request_id', db.Integer, db.ForeignKey('request.id'))
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120))
role = db.Column(db.String(120))
password_hash = db.Column(db.String(120))
requests = db.relationship('Request', secondary=subs,
backref=db.backref('users', lazy='dynamic'))
post = db.relationship('Posts', backref = 'user', lazy = 'dynamic')
request = db.relationship('Request', backref='user', lazy = 'dynamic')
is_active = db.Column(db.String(120))
class Request(db.Model):
__tablename__ = 'request'
id = db.Column(db.Integer, primary_key=True)
org = db.Column(db.String(120))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
cost = db.Column(db.Integer)
created = db.Column(db.DateTime, default= datetime.utcnow)
cost_time = db.Column(db.Integer)
update_time = db.Column(db.DateTime, default = datetime.utcnow())
diff_time = db.Column(db.DateTime)
feedback = db.Column(db.Text, default=update_time)
comment = db.relationship('Posts', backref = 'request', lazy='dynamic')
rate_idea = db.Column(db.Integer)
new = db.Column(db.Text)
cost_buyer = db.relationship('Costs', backref = 'request', lazy='dynamic')
status = db.Column(db.String(120), db.ForeignKey('status.id'))
For example:
User1.requests = [Request_1, 'Request_2, Request_3]
User2.requests = [Request_2, Request_3]
When somebody do a new Request i need to clarify firstly which user has a minimum requests from all of users and then put this request to him.
New_request = Request(org = 'TEST')
In this case User2 must add this New_request to his own User.requests so the final result must be
User1.requests = [Request_1, 'Request_2, Request_3]
User2.requests = [Request_2, Request_3, New_request]
i want to do query something like this, but what is the right and simple solution for this i don't know and i want to know:
db.query.filter(min(len(User.requests))
Something like this should work. But I suggest you to check the docs.
from sqlalchemy import func
db.query(User.id, func.count())
.outerjoin(User.requests)
.group_by(User.id)
.order_by(func.count())
.limit(1)

Mapper Errors while creating Rows

I'm working on building a recipe database. In this, there are ingredients (like onions, carrots, etc) and modifiers (like diced, peeled, etc) which are in part of ModifiedIngredients which is part of a recipe. My models.py is as follows:
from app import db
modifiers = db.Table('modifiers',
db.Column('modified_ingredient', db.Integer, db.ForeignKey('modified_ingredient.id')),
db.Column('modifier', db.Integer, db.ForeignKey('modifier.id'))
)
modified_ingredients = db.Table('modified_ingredients',
db.Column('recipe', db.Integer, db.ForeignKey('recipe.id')),
db.Column('modified_ingredient', db.Integer, db.ForeignKey('modified_ingredient.id'))
)
class Recipe(db.Model):
__tablename__ = 'recipe'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(256))
description = db.Column(db.Text)
directions = db.Column(db.Text)
prep_time = db.Column(db.Integer)
cook_time = db.Column(db.Integer)
image = db.Column(db.LargeBinary())
ingredients = db.relationship('modified_ingredient', secondary=modified_ingredients)
class Ingredient(db.Model):
__tablename__ = 'ingredient'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), index=True, unique=True)
class Modifier(db.Model):
__tablename__ = 'modifier'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), index=True, unique=True)
class ModifiedIngredient(db.Model):
__tablename__ = 'modified_ingredient'
id = db.Column(db.Integer, primary_key=True)
amount = db.Column(db.Integer)
unit = db.Column(db.String(20))
ingredients = db.relationship('ingredient', backref='ingredient', lazy='dynamic')
modifiers = db.relationship('Modifier', secondary=modifiers,
backref=db.backref('modifiers', lazy='dynamic'), lazy='dynamic')
recipe = db.Column(db.Integer, db.ForeignKey('recipe.id'))
However, if I open up a shell and enter in something like "onion = models.Ingredient(name='Onion')" I get the error:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Original exception was: relationship 'ingredients' expects a class or a mapper argument (received: <class 'sqlalchemy.sql.schema.Table'>)
I'm not exactly sure where my setup is going wrong.
Thanks.
if i'm reading the error message correctly then
ingredients = db.relationship('modified_ingredient', secondary=modified_ingredients)
should become
ingredients = db.relationship('ModifiedIngredient', secondary=modified_ingredients)
Note in the error message:
Original exception was: relationship 'ingredients' expects a class or a mapper argument (received: <class 'sqlalchemy.sql.schema.Table'>)
If it expects a class, give it a class :)