Check join condition of a relationship in SqlAlchemy - sqlalchemy

Say I have a grouped parent / child relationship, with composite foreign keys, like this:
class Group(Base):
__tablename__ = 'group'
id_ = Column('id', GUID, primary_key=True)
class Parent(Base):
__tablename__ = 'parent'
id_ = Column('id', GUID, primary_key=True)
group_id = Column(GUID, ForeignKey('group.id'), primary_key=True)
group = relationship(Group)
class Child(Base):
__tablename__ = 'child'
id_ = Column('id', GUID, primary_key=True)
group_id = Column(GUID, ForeignKey('group.id'), primary_key=True)
parent_id = Column(GUID)
__table_args__ = (
ForeignKeyConstraint(
['parent_id', 'group_id'],
['parent.id', 'parent.group_id']
),
)
group = relationship(Group)
parent = relationship(Parent, foreign_keys=[parent_id], backref='children')
My actual question is: how can I find out what the join condition would be? I'm hoping for something like Child.parent.join_condition
Bonus points: will Child.parent produce a join condition like this: ON (child.group_id = parent.group_id AND child.parent_id = parent.id) Or do I need to be explicit and use a primaryjoin argument? Edit I just re-read this section of the docs, and it looks like it won't.

You do not need to use primaryjoin in this case. It is enough to define foreign_keys by including group_id as well:
class Child(Base):
...
parent = relationship(
Parent,
foreign_keys=[parent_id, group_id],
backref='children',
)
In this case including join like:
q = session.query(Child).join(Parent)
print(q)
will produce:
SELECT child.id AS child_id,
child.group_id AS child_group_id,
child.parent_id AS child_parent_id
FROM child
JOIN parent
ON parent.group_id = child.group_id
AND parent.id = child.parent_id
Note: one does not need parenthesis around the ON condition.

Related

Sqlalchemy get records with less than N parents

How to get records with less than 3 parents or without parents?
Hello everyone! I have 3 tables: Product, Order and User. Product and Order, Product and User are linked many to many.
How to get products with less than 3 orders and users?
My query is:
stmt = select(User).where(Product.user_id == User.id).subquery()
stmt1 = select(Order).where(Order.user_id == User.id).subquery()
select(Product)
.outerjoin(Product.orders)
.outerjoin(Product.users)
filter(
and_(
select(func.count("*")).select_from(stmt).as_scalar() < 3,
select(func.count("*")).select_from(stmt1).as_scalar() <3,
)
)
If I remove the second condition from _and, I get 36 products, although I only have 20.
If I apply scalars().unique().all(), I get 20 products, although I should get less, because some of them already have 3 users or 3 orders.
With second _and condition I get 0 products
Thanks you in advance!
Since you have many-to-many relationships, I'll assume you must have at least association tables (if not association objects).
I have managed to extend from your query, using the association tables and it returns the expected results.
stmt = (
select(Product)
.outerjoin(Product.orders)
.outerjoin(Product.users)
.group_by(Product.id)
.having(
and_(
select(func.count())
.select_from(product_order_AT)
.where(product_order_AT.c.product_id == Product.id)
.as_scalar()
< 3,
select(func.count())
.select_from(product_user_AT)
.where(product_user_AT.c.product_id == Product.id)
.as_scalar()
< 3,
)
)
)
Now, it's not very pretty but it is readable, and produces the output you seem to want.
Full demo.
from sqlalchemy import (
Column,
ForeignKey,
Integer,
Table,
and_,
create_engine,
func,
select,
)
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
product_order_AT = Table(
"product_order_AT",
Base.metadata,
Column("order_id", ForeignKey("order.id"), primary_key=True),
Column("product_id", ForeignKey("product.id"), primary_key=True),
)
product_user_AT = Table(
"product_user_AT",
Base.metadata,
Column("user_id", ForeignKey("user.id"), primary_key=True),
Column("product_id", ForeignKey("product.id"), primary_key=True),
)
class Product(Base):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
orders = relationship(
"Order", secondary=product_order_AT, back_populates="products"
)
users = relationship(
"User", secondary=product_user_AT, back_populates="products"
)
def __repr__(self):
return f"Product(orders={self.orders}, users={self.users})"
class Order(Base):
__tablename__ = "order"
id = Column(Integer, primary_key=True)
products = relationship(
"Product", secondary=product_order_AT, back_populates="orders"
)
def __repr__(self):
return f"Order(id={self.id})"
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
products = relationship(
"Product", secondary=product_user_AT, back_populates="users"
)
def __repr__(self):
return f"User(id={self.id})"
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
with Session(engine) as session:
p1 = Product()
p2 = Product()
p3 = Product()
p4 = Product()
o1 = Order()
o2 = Order()
o3 = Order()
o4 = Order()
u1 = User()
u2 = User()
u3 = User()
u4 = User()
p1.orders.extend([o1, o2, o3, o4])
p1.users.extend([u1, u2, u3, u4])
p2.orders.extend([o1, o2, o3, o4])
p2.users.extend([u1, u2])
p3.orders.extend([o1, o2])
p3.users.extend([u1, u2, u3, u4])
p4.orders.extend([o1, o2])
p4.users.extend([u1, u2])
session.add_all([p1, p2, p3, p4])
session.commit()
with Session(engine) as session:
stmt = (
select(Product)
.outerjoin(Product.orders)
.outerjoin(Product.users)
.group_by(Product.id)
.having(
and_(
select(func.count())
.select_from(product_order_AT)
.where(product_order_AT.c.product_id == Product.id)
.as_scalar()
< 3,
select(func.count())
.select_from(product_user_AT)
.where(product_user_AT.c.product_id == Product.id)
.as_scalar()
< 3,
)
)
)
print(session.scalars(stmt).all())
prints
[Product(orders=[Order(id=2), Order(id=1)], users=[User(id=1), User(id=2)])]

Querying multiple related tables with join to generate single table using sqlalchemy

I am working on a web app which manages the inventory system and selling books and items. I want to generate the monthly based report on how many books where sold and ordered to Inventory in given period of time. In order to do this I have to join several tables. Here are my tables:
- Book Table
class Book(ResourceMixin, db.Model):
__tablename__ = 'book'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(85))
stock_amount = db.Column(db.Integer, nullable=False, server_default='0')
#Foreign Key
category_id = db.Column(db.Integer, db.ForeignKey('category.id',
onupdate='CASCADE',
ondelete='CASCADE'),
index=True, nullable=False)
Category Table
class Category(ResourceMixin, db.Model):
__tablename__ = 'category'
id = db.Column(db.Integer, primary_key=True)
category_name = db.Column(db.String(85))
isbn1 = db.Column(db.String(13))
isbn2 = db.Column(db.String(13))
total_stock_amount = db.Column(db.Integer)
unit_price = db.Column(db.Float)
selling_price = db.Column(db.Float)
bank_transfer_price = db.Column(db.Float)
unit_cost = db.Column(db.Float)
author = db.Column(db.String(100))
ordered = db.Column('is_ordered', db.Boolean(), nullable=False, server_default='0')
#Association Proxies
orders = association_proxy('book_orders', 'order')
stores = association_proxy('book_store', 'store')
supplier_id = db.Column(db.Integer, db.ForeignKey('suppliers.id',
onupdate='CASCADE',
ondelete='CASCADE'),
index=True, nullable=False)
#Relationship with Books
books = db.relationship(Book, backref=db.backref('book_category'), innerjoin=True)
BookStore table which inherits from Category table
class BookStore(ResourceMixin, db.Model):
__tablename__ = 'book_store'
id = db.Column(db.Integer, primary_key=True)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
store_id = db.Column(db.Integer, db.ForeignKey('store.id'))
isbn1 = db.Column(db.String)
isbn2 = db.Column(db.String)
book_amount = db.Column(db.Integer)
#Bidirectional attribute/collection of 'category'/'book_store'
category = db.relationship('Category', backref = db.backref('book_store', cascade='all, delete-orphan'),
lazy='joined', innerjoin=True,
order_by='Category.category_name')
bookstore_cart = db.relationship('StoreCart', backref='book_store_cart', passive_deletes=True)
book_purchases = association_proxy('book_orders', 'customer_purchases')
#Reference to the 'Store' object
store = db.relationship('Store')
CustomerPurchase table which is inherits from BookStore
class CustomerPurchase(ResourceMixin, db.Model):
__tablename__ = 'customer_purchase'
id = db.Column(db.Integer, primary_key=True)
book_store_category_id = db.Column(db.Integer, db.ForeignKey('book_store.id'))
customer_order_id = db.Column(db.Integer, db.ForeignKey('customer_orders.id'))
book_title = db.Column(db.Text)
unit_price = db.Column(db.Float)
quantity = db.Column(db.Float)
total_price = db.Column(db.Float)
store_id = db.Column(db.Integer, db.ForeignKey('store.id',
onupdate='CASCADE',
ondelete='CASCADE'),
index=True)
#Bidirectional attribute/collection of 'bookstore'/'customer purchase'
book_purchase = db.relationship('BookStore', backref = db.backref('customer_purchases', cascade='all, delete-orphan'),
lazy='joined', innerjoin=True,
order_by='BookStore.isbn1')
#Bidirectional attribute/collection of 'store'/'purchase'
customer_order = db.relationship('CustomerOrders')
Let me explain you the working principle. So in order to sell books user has to transfer Category to the BookStore and then s/he can sell it. Sales are stored in CustomerPurchase table. If books in inventory are finished, then the User has to order books from supplier and put it in inventory, the date of the input to the inventory is captured to generate report in future.
My desired report table should include these columns
book_title | unit_cost | amount_in_stock | revenue | amount_ordered | total_price_of_ordered_books | amount_sold | revenue | amount_left | total_price_of_amount_left |
My query is:
bp = db.session.query(CustomerPurchase.book_store_category_id, BookStore.category, func.sum(CustomerPurchase.quantity).label('quantity'))\
.filter(CustomerPurchase.created_on >= start_date)\
.filter(CustomerPurchase.created_on <= end_date)\
.group_by(CustomerPurchase.book_store_category_id, BookStore.id, Category.id)\
.subquery()
cp = db.session.query(BookStore, bp.c.quantity)\
.join(bp, BookStore.category_id == bp.c.book_store_category_id)\
.distinct(bp.c.book_store_category_id)\
.order_by(bp.c.book_store_category_id)\
.all()
It outputs the CustomerPurchase and BookStore table but doesn't go beyond it for example I cannot go down CustomerPurchase.book_purchase.category.books.stock_amount or BookStore.category.books.stock_amount
When I want to access CustomerPurchase.book_purchase it throws me an error
UndefinedError: 'sqlalchemy.util._collections.result object' has no attribute 'book_purchase'
Any help is greatly appreciated! Thanks in advance!
After 2 weeks of research and collaboration I managed to solve the problem:
Now it outputs all the desired columns and returns even though a column is NULL:
my_query = db.session.query(CustomerPurchase.book_title, Category.unit_cost, Category.total_stock_amount, BookStore.book_amount,
func.sum(CustomerPurchase.quantity).label('quantity'),
func.sum(Book.stock_amount).label('book_stock_amount'))\
.join(BookStore)\
.join(Category)\
.outerjoin(Book)\
.filter(CustomerPurchase.created_on >= report.start_date.date())\
.filter(CustomerPurchase.created_on <= report.end_date.date())\
.group_by(CustomerPurchase.book_title,
Category.unit_cost, Category.total_stock_amount,
BookStore.book_amount).all()
As simple as that! Basically I have to cherry pick every column that I want to display. Also I was concerned about NULL return value of Book.stock_amount but after I used outerjoin on Book table I managed to pull out all the data I needed for my report.
I hope it will help for some people out there! :)

SQLAlchemy One-to-Many relationship in single table- declarative

I have category and sub-category, I want to populate parent with child categories. How should I do?
class Category(Base):
__tablename__ = 'category'
id = Column(UUIDType, primary_key=True)
organisation_id = Column(UUIDType)
category_name = Column(String(50))
parent_category_id = Column(UUIDType, ForeignKey("category.id"))
parent_category = relationship("Category", backref='category',
remote_side=parent_category_id)

Sqlalchemy query result order by relationship filter count

I have two models
class Song(db.Model):
__tablename__ = 'song'
id = db.Column(db.Integer, primary_key=True)
collections = db.relationship('SongCollect', backref='song', lazy='dynamic')
class SongCollect(db.Model):
__tablename__ = 'songcollect'
id = db.Column(db.Integer, primary_key=True)
song_id = db.Column(db.Integer, db.ForeignKey('song.id'))
collect_status = db.Column(db.Boolean, default=True)
How can I get the Song query result order by count(SongCollect.collect_status == True)
I use flask-sqlalchemy.
Or how can I translate following sql to sqlalchemy syntax
select s.id, s.songid, s.songname,c.collect_status, sum(c.collect_status) as collect_count from song as s left join songcollect as c on s.id = c.song_id group by s.id order by collect_count desc
# aliases: not really required
s = db.aliased(Song, name="s")
c = db.aliased(SongCollect, name="c")
# expression for the count: simple `sum` might not work on `boolean`
# types for all databases, the used option below is more robust
# collect_count = db.func.sum(c.collect_status)
collect_count = db.func.sum(db.case([(c.collect_status, 1)], else_=0))
# actual query
q = (db.session
.query(s, collect_count.label("collect_count"))
.outerjoin(c)
.group_by(s)
.order_by(collect_count.desc())
)
for song, song_collect_count in q:
print(song, song_collect_count)
print("-"*80)

SQLAlchemy distinct related objects

I have a simple messaging system with Conversation, User and Message objects defined like so:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
class Conversation(db.Model):
id = db.Column(db.Integer, primary_key=True)
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.String)
author = db.relationship('User')
conversation = db.relationship('Conversation', backref=db.backref('messages', lazy='dynamic'))
I would like to get all the Users that participate in a conversations.
It's easy for me in SQL:
select distinct users.*
from users inner join messages on messages.author_id = users.id
where messages.conversation_id = 1;
But I can't figure out how do that with SQLAlchemy's syntax so I get back objects and not just fields. Ideally I would like to implement it as a get_authors method on Conversation.
session.query(User).distinct().\
join(Message, Message.author_id == User.id).\
filter(Message.conversation_id == 1)