Order by issue when outer joining two tables in sqlalchemy - python

I am a noob trying to use flask with sqlalchemy and am having an issue sorting result from a base query.
I have a parent table and two joined many-to-many association tables:
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
(...)
qty_stock = db.Column(db.Integer)
requested_products = db.relationship('RequestedProducts')
ordered_products = db.relationship('OrderedProducts')
class OrderedProducts(db.Model):
__tablename__ = 'orderedproducts'
order_id = db.Column(db.Integer, db.ForeignKey('order.id'), primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'), primary_key=True)
quantity = db.Column(db.Integer, default=1)
qty_delivered = db.Column(db.Integer, default=0)
product = db.relationship('Product', backref='order_assocs')
class RequestedProducts(db.Model):
__tablename__ = 'requestedproducts'
request_id = db.Column(db.Integer, db.ForeignKey('request.id'), primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'), primary_key=True)
quantity = db.Column(db.Integer, default=1)
qty_supplied = db.Column(db.Integer, default=0)
product = db.relationship('Product', backref='request_assocs')
In my view class there are 4 table columns for each product showing stock quantity, number of requested products, number of ordered products and a net stock amount, which is basically (stock quantity - requested + ordered). This is the query for the net stock values I'm trying to get working:
products = Product.query.filter_by(active_flg=True)
.filter_by(category_id=int(g.category_id))
.outerjoin(Product.requested_products)
.outerjoin(Product.ordered_products)
.group_by(Product.id)
#Count requested amount for each product
reqs = func.coalesce((func.sum(RequestedProducts.quantity) - func.sum(RequestedProducts.qty_supplied)), 0)
#Count ordered amount for each product
ords = func.coalesce((func.sum(OrderedProducts.quantity) - func.sum(OrderedProducts.qty_delivered)), 0)
result = (Product.qty_stock - reqs + ords)
products = products.order_by(result.desc())
Now, the functions work as expected, the only problem is with the order_by function - the order is scrambled. I've found out that the cause is probably in the double outer join. Does anyone have an idea how to deal with that?
Also, I am really a beginner with sqlalchemy and flask so I'd be very grateful for any advice or a better solution (executable with my limited skills). Thank you!

If you already really use Hybrid Attributes for partial sums, then it should be pretty easy to combine them together.
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
qty_stock = db.Column(db.Integer)
requested_products = db.relationship('RequestedProducts')
ordered_products = db.relationship('OrderedProducts')
#hybrid_property
def diff_orders(self):
return sum(op.quantity - op.qty_delivered
for op in self.ordered_products)
#diff_orders.expression
def diff_orders(cls):
return (db.select([db.func.coalesce(
db.func.sum(
db.func.coalesce(
OrderedProducts.quantity - OrderedProducts.qty_delivered, 0)
), 0)])
.where(OrderedProducts.product_id == cls.id)
.label("diff_orders")
)
#hybrid_property
def diff_requests(self):
return sum(op.quantity - op.qty_supplied
for op in self.requested_products)
#diff_requests.expression
def diff_requests(cls):
return (db.select([db.func.coalesce(
db.func.sum(
db.func.coalesce(
RequestedProducts.quantity - RequestedProducts.qty_supplied, 0)
), 0)])
.where(RequestedProducts.product_id == cls.id)
.label("diff_requests")
)
In which case usage can be similar to:
products = db.session.query(
Product,
# Product.diff_orders,
# Product.diff_requests,
# Product.qty_stock + Product.diff_requests - Product.diff_orders,
).order_by((Product.qty_stock + Product.diff_requests - Product.diff_orders).desc())
for x in products:
print(x)

Related

How to access column values in SQLAlchemy result list after a join a query

I need to access colums of result query. I have these models
class Order(Base):
__tablename__ = "orders"
internal_id = Column(Integer, primary_key=True)
total_cost = Column(Float, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()"))
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="CASCADE"), nullable=False)
customer = relationship("Customer")
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, nullable=False)
internal_id = Column(Integer, nullable=False)
price = Column(Float, nullable=False)
description = Column(String, nullable=False)
order_id = Column(Integer, ForeignKey("orders.internal_id", ondelete="CASCADE"), nullable=False)
order = relationship("Order")
Now I run this left join query that gives me all the columns from both tables
result = db.query(Order, Item).join(Item, Item.order_id == Order.internal_id, isouter=True).filter(Item.order_id == order_id).all()
I get back a list of tuples. How do I access a particular column of the result list? Doing something like this
for i in result:
print(i.???) # NOW WHAT?
Getting AttributeError: Could not locate column in row for column anytime i try to fetch it by the name I declared.
this is the full function where I need to use it
#router.get("/{order_id}")
def get_orders(order_id: int, db: Session = Depends(get_db)):
""" Get one order by id. """
# select * from orders left join items on orders.internal_id = items.order_id where orders.internal_id = {order_id};
result = db.query(Order, Item).join(Item, Item.order_id == Order.internal_id, isouter=True).filter(Item.order_id == order_id).all()
for i in result:
print(i.description) # whatever value i put here it errors out
This is the traceback
...
print(i.description) # whatever value i put here it errors out
AttributeError: Could not locate column in row for column 'description'
At least if I could somehow get the column names.. But i just cant get them. Trying keys(), _metadata.keys .. etc. Nothing works so far.
If additional implicite queries are not an issue for you, you can do something like this:
class Order(Base):
__tablename__ = "orders"
internal_id = Column(Integer, primary_key=True)
total_cost = Column(Float, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()"))
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="CASCADE"), nullable=False)
customer = relationship("Customer")
items = relationship("Item", lazy="dynamic")
order = session.query(Order).join(Item, Order.internal_id == Item.order_id, isoutrr=True).filter(Order.internal_id == order_id).first()
if order:
for i in order.items:
print(i.description)
print(order.total_cost)
However to avoid additional query when accessing items you can exploit contains_eager option:
from sqlalchemy.orm import contains_eager
order = session.query(Order).join(Item, Order.internal_id == Item.order_id, isoutrr=True).options(contains_eager("items").filter(Order.internal_id == order_id).all()
Here you have some examples: https://jorzel.hashnode.dev/an-orm-can-bite-you
Ok, so acctualy the answer is quite simple. One just simply needs to use dot notation like i.Order.total_cost or whichever other field from the Order model
result = db.query(Order, Item).join(Item, Item.order_id == Order.internal_id, isouter=True).filter(Item.order_id == order_id).all()
for i in result:
print(i.Order.total_cost)
print(i.Item.description)

define expression used when querying the model

I am trying to calculate the average rating of a game and order by descending.
In my models.py I have defined a hybrid property
class Review(db.Model):
id = db.Column(db.Integer, primary_key=True)
rating = db.Column(db.Numeric(precision=2, scale=1), index=True)
body = db.Column(db.String(140))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
game_id = db.Column(db.Integer, db.ForeignKey('game.id'))
class Game(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), index=True, unique=True)
reviews = db.relationship('Review', backref='game', lazy='dynamic')
#hybrid_property
def avg_rating(self):
total_rating = 0
reviews = Review.query.filter_by(game_id=self.id).all()
for review in reviews:
total_rating += review.rating
return total_rating / len(reviews)
but when I try and query for the average rating in my routes.py
games = Game.query.order_by(Game.avg_rating.desc())
I get this error
AttributeError: 'decimal.Decimal' object has no attribute 'desc'
I probably need to do something like this in my models.py but how?
#avg_rating.expression
def avg_rating(cls):
# what to write here
What you want to do is described in the sqlalchemy documentation as a Correlated Subquery Relationship Hybrid
This is a pure SqlAlchemy version:
import sqlalchemy as sa
#avg_rating.expression
def avg_rating(cls):
return sa.select([sa.func.avg(Review.rating)]).where(Review.game_id == cls.id).label('avg_rating')
The flask-sqlalchemy version should look like this (but I don't have it here, so can't be 100% certain):
#avg_rating.expression
def avg_rating(cls):
return db.select([db.func.avg(Review.rating)]).where(Review.game_id == cls.id).label('avg_rating')

SQLAlchemy inserting data into two tables

I am just working on my first App based on SQLAlchemy and after couple hours of work with the documentation and some videos, I still can't fix the issue.
My app is a simple CRUD grocery list. I want to keep the category of the product in separate table so here comes relationship module of the SQLAlchemy. Error msg gives me no hint at all tbh.
engine = create_engine(my_database, echo = True)
connection = engine.connect()
Base = declarative_base()
session = sessionmaker(bind=engine)
class MyEnum(enum.Enum):
one = "pieces"
two = "kg"
class ProductTable(Base):
__tablename__ = 'product'
product_id = Column(Integer, primary_key=True)
product_name = Column(String(30), nullable=False)
product_quantity = Column(Integer, nullable=False)
product_type = Column(Enum(MyEnum), nullable=False)
category_id = Column(Integer, ForeignKey('category.id'), nullable=False)
category = relationship("category", back_populates="product")
product_description = Column(String(255))
class CategoryTable(Base):
__tablename__ = 'category'
id = Column(Integer, primary_key=True)
category_name = Column(String(25), nullable=False)
Base.metadata.create_all(engine)
session = session()
cc_product = ProductTable(product_id=1,
product_name="cucumber",
product_quantity="1",
product_type="kg",
product_description="For the salad")
cc_category= CategoryTable(category_name="vegetables")
session.add(cc_product, cc_category)
session.commit()
I. Creation of the tables finished smoothly with no errors, however, is the creation itself designed properly? Each product has single category but one category should be assigned to one or more product. I made that based on one to one relationship.
II. Inserting data to both tables. I want to insert data like:
Product_id = 1
Product_name = Cucumber
Product_quantity = 1
Product_type = "kg" or "pieces"
Category = Vegetables ( from the category table)
Description = "blah blah blah"
I think there is something wrong not only with the data inserting process but also with the tables creation.
Here is the error, which tbh, doesn't tell me anything:
sqlalchemy.exc.ArgumentError: relationship 'category' expects a class or a mapper argument (received: <class 'sqlalchemy.sql.schema.Table'>)
You have two mistakes:
you wrote "category" as the Mapper class instead of "CategoryTable"
forgot to create products on 'CategoryTable'
class ProductTable(Base):
__tablename__ = 'product'
product_id = Column(Integer, primary_key=True)
product_name = Column(String(30), nullable=False)
product_quantity = Column(Integer, nullable=False)
product_type = Column(Enum(MyEnum), nullable=False)
category_id = Column(Integer, ForeignKey('category.id'), nullable=False)
categories = relationship("CategoryTable", back_populates="products")
product_description = Column(String(255))
class CategoryTable(Base):
__tablename__ = 'category'
id = Column(Integer, primary_key=True)
category_name = Column(String(25), nullable=False)
products = relationship('ProductTable', back_populates='categories')
Some more changes are still needed:
change CategoryTable to Category (also for ProductTable, better names)
you'll have constraints failing after you'll get things running...

Subquery with count in SQLAlchemy

Given these SQLAlchemy model definitions:
class Store(db.Model):
__tablename__ = 'store'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
class CustomerAccount(db.Model, AccountMixin):
__tablename__ = 'customer_account'
id = Column(Integer, primary_key=True)
plan_id = Column(Integer, ForeignKey('plan.id'), index=True, nullable=False)
store = relationship('Store', backref='account', uselist=False)
plan = relationship('Plan', backref='accounts', uselist=False)
class Plan(db.Model):
__tablename__ = 'plan'
id = Column(Integer, primary_key=True)
store_id = Column(Integer, ForeignKey('store.id'), index=True)
name = Column(String, nullable=False)
subscription_amount = Column(Numeric, nullable=False)
num_of_payments = Column(Integer, nullable=False)
store = relationship('Store', backref='plans')
How do I write a query to get a breakdown of subscription revenues by plan?
I'd like to get back a list of the plans for a given Store, and for each plan the total revenues for that plan, calculated by multiplying Plan.subscription_amount * Plan.num_of_payments * num of customers subscribed to that plan
At the moment I'm trying with this query and subquery:
store = db.session.query(Store).get(1)
subscriber_counts = db.session.query(func.count(CustomerAccount.id)).as_scalar()
q = db.session.query(CustomerAccount.plan_id, func.sum(subscriber_counts * Plan.subscription_amount * Plan.num_of_payments))\
.outerjoin(Plan)\
.group_by(CustomerAccount.plan_id)
The problem is the subquery is not filtering on the current plan id.
I also tried with this other approach (no subquery):
q = db.session.query(CustomerAccount.plan_id, func.count(CustomerAccount.plan_id) * Plan.subscription_amount * Plan.num_of_payments)\
.outerjoin(Plan)\
.group_by(CustomerAccount.plan_id, Plan.subscription_amount, Plan.num_of_payments)
And while the results seem fine, I don't know how to get back the plan name or other plan columns, as I'd need to add them to the group by (and that changes the results).
Ideally if a plan doesn't have any subscribers, I'd like it to be returned with a total amount of zero.
Thanks!
Thanks to Alex Grönholm on #sqlalchemy I ended up with this working solution:
from sqlalchemy.sql.expression import label
from sqlalchemy.sql.functions import coalesce
from instalment.models import db
from sqlalchemy import func, desc
def projected_total_money_volume_breakdown(store):
subscriber_counts = db.session.query(
CustomerAccount.plan_id,
func.count(CustomerAccount.id).label('count')
).group_by(CustomerAccount.plan_id) \
.subquery()
total_amount_exp = coalesce(
subscriber_counts.c.count, 0
) * Plan.subscription_amount * Plan.num_of_payments
return db.session.query(
Plan,
label('total_amount', total_amount_exp)
) \
.outerjoin(subscriber_counts, subscriber_counts.c.plan_id == Plan.id) \
.filter(Plan.store == store) \
.order_by(desc('total_amount')) \
.all()

How to obtain data from a table that has been joined

I have two tables items and games.
#app.route('/collection/<username>/<int:page>/<platform>/<path:path>')
def collection(username, page=1, platform='DMG', path=None):
# first I get the user by his username
user = User.query.filter_by(username=username).first()
# then I get all items form the user and related games
items = user.items.join(Game)
# until now it works perfectly fine
# now I would like to obtain all titles from the joined table games
game_titles = items.filter(Game.title).all()
# but unfortunately I get only an empty list
What is missing?
Here my models:
class Game(db.Model):
__tablename__ = 'games'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(64), index=True)
publisher = db.Column(db.String(32), index=True)
region = db.Column(db.String(3), index=True)
code_platform = db.Column(db.String(3), index=True)
code_identifier = db.Column(db.String(4), index=True)
code_region = db.Column(db.String(3), index=True)
code_revision = db.Column(db.String(1))
code = db.Column(db.String(16), index=True, unique=True)
year = db.Column(db.Integer)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
items = db.relationship('Item', backref='game', lazy='dynamic')
def __repr__(self):
return '<Game %r>' % (self.title)
class Item(db.Model):
__tablename__ = 'items'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(8), index=True)
cart = db.Column(db.Boolean)
box = db.Column(db.Boolean)
manual = db.Column(db.Boolean)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
game_id = db.Column(db.Integer, db.ForeignKey('game.id'))
def __repr__(self):
return '<Collection %r>' % (self.user_id)
You have two options. Using SQLAlchemy ORM:
game_titles = [i.game.title for i in user.items]
To make this more efficient, you can apply the joinedload optimization:
game_titles = [i.game.title for i in user.items.options(joinedload(Item.game))]
Alternatively, you can use SQLAlchemy core if all you care about are the titles (and nothing else):
game_titles = user.items.join(Item.game).with_entities(Game.title).all()
You can even skip fetching the user altogether if you don't care about the user at all:
game_titles = User.query.join(User.items).join(Item.game).filter(User.username == username).with_entities(Game.title).all()
As an aside, .filter and .filter_by correspond to the selection operator in relational algebra, whereas .with_entities and db.session.query(...) correspond to the projection operator, contrary to what you had initially assumed.
Try something like this:
items.join(Game).options(joinedload(Item.game, innerjoin=True))
Essentially, you're joining with Game and explicitly loading it, where the innerjoin forces it to do so only on the games listed in the table you're joining with (items)

Categories

Resources