I want to make a property for model which contains count.
Since I always need the property, I want to make query with JOIN like sqlalchemy.orm.relationship with lazy='joined'
For example, I defined models like following
import sqlalchemy as s, func
from sqlalchemy.orm import relatioship
# ...
class Foo(Base):
__tablename__ = 'foo'
id = s.Column(s.Integer, primary_key=True)
bar_id = s.Column(s.Integer, s.ForeignKey('bar.id'))
bar = relationship('Bar')
class Bar(Base):
__tablename__ = 'bar'
id = s.Column(s.Integer, primary_key=True)
#property
def foo_count(self):
return Foo.query.filter_by(bar=self).count()
When I access to property foo_count, then it will send query to DBMS.
Since I always access to this property, I want to load it eagerly the counting property like this
# Not session.query(Bar, func.count(Foo.id)).join(Foo) ...
bar = Bar.query.first()
SQL will be like this
SELECT id, COUNT(Foo.id)
FROM bar
INNER JOIN foo
ON bar.id = foo.id
Then bar.foo_count will not occur SQL query.
How can I make a property like foo_count?
I solved it by using sqlalchemy.orm.column_property
I replaced the foo_count by following
import sqlalchemy as s, func, select
from sqlalchemy.orm import relationship, column_property
# ...
class Foo(Base):
__tablename__ = 'foo'
id = s.Column(s.Integer, primary_key=True)
bar_id = s.Column(s.Integer, s.ForeignKey('bar.id'))
bar = relationship('Bar')
class Bar(Base):
__tablename__ = 'bar'
id = s.Column(s.Integer, primary_key=True)
foo_count = column_property(
select([func.count(Foo.id)])
.where(Foo.bar_id == id)
)
Please take a look at the Hybrid Attribute extension.
Your object model will look similar to the below:
class Foo(Base):
__tablename__ = 'foo'
id = Column(Integer, primary_key=True)
bar_id = Column(Integer, ForeignKey('bar.id'))
bar = relationship('Bar')
class Bar(Base):
__tablename__ = 'bar'
id = Column(Integer, primary_key=True)
#hybrid_property
def foo_count(self):
return object_session(self).query(Foo).filter(Foo.bar==self).count()
#foo_count.expression
def foo_count(cls):
return select([func.count(Foo.id)]).where(Foo.bar_id == cls.id).label('foo_count')
foo_count will not be eagerly loaded, but you can use it in queries like below (both in SELECT and in WHERE clause:
qry = session.query(Bar, Bar.foo_count).filter(Bar.foo_count > 0)
for (bar, bar_foo_count) in qry:
print bar, bar_foo_count
As you can see, the query will return tuples of (Bar, foo_count) in just one query, and now you can do what you wish with that.
Related
I have two tables bound by a M2M relationship. Books and Writers, writers can have many books and books can have many writers.
I want to have a count property on both books and writers so I could sort them by, for example, the writer who wrote the most books.
# many to many association table
book_writer_association_table = Table('book_writer_association',Base.metadata,
Column('book_id',ForeignKey('book.id'), primary_key=True),
Column('Writer',ForeignKey('writer.id'), primary_key=True)
)
class Book(Base):
__tablename__ = 'base'
id = Column(Integer, primary_key=True)
name = Column(String)
writers = relationship(Writer,secondary=book_writer_association_table,back_populates="books")
class Writer(Base):
__tablename__ = 'writer'
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship(Book,secondery=book_writer_association_table,back_populates="writers")
#hybrid_property
def book_count(self):
return len(self.books)
#book_count.expression
def book_count(cls):
#what goes here?
I tried various approaches like detailed here:
class Foo(Base):
__tablename__ = 'foo'
id = Column(Integer, primary_key=True)
bar_id = Column(Integer, ForeignKey('bar.id'))
bar = relationship('Bar')
class Bar(Base):
__tablename__ = 'bar'
id = Column(Integer, primary_key=True)
#hybrid_property
def foo_count(self):
return object_session(self).query(Foo).filter(Foo.bar==self).count()
#foo_count.expression
def foo_count(cls):
return select([func.count(Foo.id)]).where(Foo.bar_id == cls.id).label('foo_count')
However, in this example, there are only two tables and I'm unsure how to achieve a more complicated join here. Another user suggested using column_property but I run into exactly the same problem there. I'm unsure how to further add tables to the join.
You can customize idea from here to M2M case. For this you should mention association_table in hybrid_property instead of Book table. So, you eliminate join with Book table and simplify your case to One-to-Many relation.
I came up with this solution.
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String, select, func, create_engine, Table
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, object_session, sessionmaker, Session
# Declare models
#as_declarative()
class Base:
pass
book_writer_association_table = Table('book_writer_association',Base.metadata,
Column('book_id',ForeignKey('book.id'), primary_key=True),
Column('writer_id',ForeignKey('writer.id'), primary_key=True)
)
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
name = Column(String)
writers = relationship("Writer", secondary=book_writer_association_table, back_populates="books")
class Writer(Base):
__tablename__ = 'writer'
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship("Book", secondary=book_writer_association_table, back_populates="writers")
#hybrid_property
def book_count(self):
return object_session(self).query(book_writer_association_table).filter(book_writer_association_table.c.writer_id == self.id).count()
#book_count.expression
def book_count(cls):
return select([func.count(book_writer_association_table.c.book_id)]).where(book_writer_association_table.c.writer_id == cls.id).label('book_count')
# Load DB schema
engine = create_engine('sqlite:///sqlite3.db')
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(autocommit=True, bind=engine)
db: Session = SessionLocal()
# Creating test instances
b1 = Book(name="Book 1")
b2 = Book(name="Book 2")
db.add(b1)
db.add(b2)
w1 = Writer(name="Writer 1")
w2 = Writer(name="Writer 2")
db.add(w1)
db.add(w2)
b1.writers.append(w1)
b1.writers.append(w2)
b2.writers.append(w1)
query = db.query(Writer, Writer.book_count)
print(str(query)) # checking query
print()
writers: List[Writer] = query.all() # testing query
for writer, book_count in writers:
print(f"{writer.name}: {book_count}")
Result:
> Writer 1: 2
> Writer 2: 1
I'm unsure how to further add tables to the join.
SQL from here db.query(Writer, Writer.book_count) looks clean, without any joins. So, I think you shouldn't have any problems with subsequent joins.
> SELECT writer.id AS writer_id, writer.name AS writer_name, (SELECT count(book_writer_association.book_id) AS count_1
> FROM book_writer_association
> WHERE book_writer_association.writer_id = writer.id) AS book_count
> FROM writer
Edit: If you need join Book table to provide additional filtering you can do it like this. Here I filtered book with price less than 150
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String, select, func, create_engine, Table
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, object_session, sessionmaker, Session
# Declare models
#as_declarative()
class Base:
pass
book_writer_association_table = Table('book_writer_association',Base.metadata,
Column('book_id',ForeignKey('book.id'), primary_key=True),
Column('writer_id',ForeignKey('writer.id'), primary_key=True)
)
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
name = Column(String)
price = Column(Integer)
writers = relationship("Writer", secondary=book_writer_association_table, back_populates="books")
class Writer(Base):
__tablename__ = 'writer'
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship("Book", secondary=book_writer_association_table, back_populates="writers")
#hybrid_property
def book_count(self):
return (
object_session(self)
.query(book_writer_association_table)
.join(Book, Book.id == book_writer_association_table.c.book_id)
.filter(book_writer_association_table.c.writer_id == self.id)
.filter(Book.price > 150)
.count()
)
#book_count.expression
def book_count(cls):
# return select([func.count(book_writer_association_table.c.book_id)]).where(book_writer_association_table.c.writer_id == cls.id).label('book_count')
#
return (
select([func.count(book_writer_association_table.c.book_id)])
.join(Book, Book.id == book_writer_association_table.c.book_id)
.where(book_writer_association_table.c.writer_id == cls.id)
.filter(Book.price > 150)
.label('book_count')
)
# Load DB schema
engine = create_engine('sqlite:///sqlite3.db')
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(autocommit=True, bind=engine)
db: Session = SessionLocal()
# Creating test instances
b1 = Book(name="Book 1", price=100)
b2 = Book(name="Book 2", price=200)
db.add(b1)
db.add(b2)
w1 = Writer(name="Writer 1")
w2 = Writer(name="Writer 2")
db.add(w1)
db.add(w2)
b1.writers.append(w1)
b1.writers.append(w2)
b2.writers.append(w1)
query = db.query(Writer, Writer.book_count)
print(str(query)) # checking query
print()
writers: List[Writer] = query.all() # testing query
for writer, book_count in writers:
print(f"{writer.name}: {book_count}")
query:
SELECT writer.id AS writer_id,
writer.name AS writer_name,
(SELECT count(book_writer_association.book_id) AS count_1
FROM book_writer_association
JOIN book ON book.id = book_writer_association.book_id
WHERE book_writer_association.writer_id = writer.id
AND book.price > ?) AS book_count
FROM writer
I have some models that I am trying to search, so I am looking to show return a result of all Parent objects, where either the Parent name is "foo" or the Child name is "foo".
I have the query:
parents = Session.query(Parent).\
join(Child_s3).\
filter(Parent.name.ilike("%foo%")).\
filter(Child_s3.name.ilike("%foo%")).\
order_by(asc(Product.name))
And the models:
class Parent(BaseSO):
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
name = Column(Unicode(100), nullable=False, unique=True)
colours = relationship('Child_s3', secondary=Parent_images, backref='Parentc')
class Child_s3(BaseSO):
__tablename__ = 'children'
id = Column(Integer, primary_key=True)
name = Column(Unicode)
Parent_images = Table(
'Parent_images', BaseSO.metadata,
Column('parent_id', Integer, ForeignKey('parents.id')),
Column('child_id', Integer, ForeignKey('children.id'))
)
The query I have shows parents with the name of "foo" but does not show any parent objects, that also have children called "foo", can anyone help build this query to search both tables for the corresponding parent objects?
This code shows how to get the result using either an explicit join or a subquery:
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Parent_images = sa.Table(
'Parent_images', Base.metadata,
sa.Column('parent_id', sa.Integer, sa.ForeignKey('parents.id')),
sa.Column('child_id', sa.Integer, sa.ForeignKey('children.id'))
)
class Parent(Base):
__tablename__ = 'parents'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Unicode(100), nullable=False, unique=True)
colours = orm.relationship('Child_s3', secondary=Parent_images, backref='parents')
def __repr__(self):
return 'Parent(name=%s)' % self.name
__str__ = __repr__
class Child_s3(Base):
__tablename__ = 'children'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Unicode)
def __repr__(self):
return 'Child_s3(name=%s)' % self.name
__str__ = __repr__
if __name__ == '__main__':
engine = sa.create_engine('sqlite:///')
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
Session = orm.sessionmaker(bind=engine)
session = Session()
for parent, child in [('boofoo', 'spam'), ('baz', 'foobar'), ('bar', 'quux')]:
p1 = Parent(name=parent)
session.add(p1)
p1.colours.append(Child_s3(name=child))
session.commit()
print('Join')
session = Session()
q = (session.query(Parent)
.join(Child_s3, Parent.colours)
.filter(sa.or_(Parent.name.ilike('%foo%'),
Child_s3.name.ilike('%foo%'))))
for p in q.all():
print(p, p.colours)
session.commit()
print()
print('Subquery')
session = Session()
q = (session.query(Parent)
.filter(sa.or_(Parent.name.ilike('%foo%'),
Parent.colours.any(Child_s3.name.ilike('%foo%')))))
for p in q.all():
print(p, p.colours)
session.commit()
print()
The join query
q = (session.query(Parent)
.join(Child_s3, Parent.colours)
.filter(sa.or_(Parent.name.ilike('%foo%'),
Child_s3.name.ilike('%foo%'))))
generates this SQL
SELECT parents.id AS parents_id, parents.name AS parents_name
FROM parents JOIN "Parent_images" AS "Parent_images_1" ON parents.id = "Parent_images_1".parent_id JOIN children ON children.id = "Parent_images_1".child_id
WHERE lower(parents.name) LIKE lower(?) OR lower(children.name) LIKE lower(?)
The subquery
q = (session.query(Parent)
.filter(sa.or_(Parent.name.ilike('%foo%'),
Parent.colours.any(Child_s3.name.ilike('%foo%')))))
generates this SQL:
SELECT parents.id AS parents_id, parents.name AS parents_name
FROM parents
WHERE lower(parents.name) LIKE lower(?) OR (EXISTS (SELECT 1
FROM "Parent_images", children
WHERE parents.id = "Parent_images".parent_id AND children.id = "Parent_images".child_id AND lower(children.name) LIKE lower(?)))
The script produces this output from the sample data:
Join
Parent(name=baz) [Child_s3(name=foobar)]
Parent(name=boofoo) [Child_s3(name=spam)]
Subquery
Parent(name=boofoo) [Child_s3(name=spam)]
Parent(name=baz) [Child_s3(name=foobar)]
In my sqlalchemy classes I have the following classes:
class FooBar(Model):
__tablename__ = ‘foobar’
id = Column('id', Integer, primary_key=True)
foonr = Column('foonr', Integer, ForeignKey('foo.nr'), nullable=False)
barnr = Column('barnr', String, ForeignKey('bar.nr'), nullable=False)
class Foo(Model):
__tablename__ = ‘foo’
nr = Column('nr', Integer, primary_key=True)
foo_name = Column(‘name’,String)
class Bar(Model):
__tablename__ = ‘bar’
nr = Column('nr', Integer, primary_key=True)
bar_name = Column(‘name’,String)
foo_bar = relationship('foobar', uselist=False)
When I try to nest the classes Foo or Bar in a Marshmallow Schema for FooBar I’m not getting any results (the dictionaries don't have any references to the classes Foo or Bar).
class FooBarSchema(Schema):
id = fields.Int()
foo = fields.Nested('FooSchema', many=False)
bar = fields.Nested('BarSchema', many=False)
How can I get the Foo and Bar classes in the results of the FooBarSchema?
Ok... I'll give you the solution to your problem.
class FooBar(Model):
__tablename__ = 'foobar'
id = Column('id', Integer, primary_key=True)
foonr = Column('foonr', Integer, ForeignKey('foo.nr'), nullable=False)
barnr = Column('barnr', String, ForeignKey('bar.nr'), nullable=False)
foo = relationship("Foo", uselist=False)
bar = relationship("Bar", uselist=False)
class FooBarSchema(Schema):
id = fields.Int()
foo = fields.Nested('FooSchema', many=False)
bar = fields.Nested('BarSchema', many=False)
But analyzing your code I think we can make it more pythonic.
If, and only if, you do not have extra data in the association table, we can change some things.
Looking at SQLAlchemy doc's for an Many To Many relationship, we can to use the secondary parameter of the relationship().
We have to keep class as you currently have and the class Bar like that:
class Bar(Model):
__tablename__ = 'bar'
nr = Column('nr', Integer, primary_key=True)
bar_name = Column('name',String)
foos = relationship("Foo", secondary="foobar", backref="bars")
So in Bar.foos we have a list of Foo objects, and the backref also makes it possible to have an Bar list in Foo.bars.
Now we have to configure the BarSchema and FooSchema classes.
class FooSchema(Schema):
nr = fields.Int()
foo_name = fields.Str()
bars = fields.Nested('BarSchema', exclude=('foos',), many=True)
class BarSchema(Schema):
nr = fields.Int()
bar_name = fields.Str()
foos = fields.Nested('FooSchema', exclude=('bars',), many=True)
The exclude is to avoid recursive problems.
So I want to execute a filter on all Columns of my Database Model which uses table inheritance. I am by no means sure if this is actually do-able or not.
To get started let's use the same inheritance example from the SQLAlchemy Doc just slightly modified. I've omitted the imports here.
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
name = Column(String(50))
type = Column(String(50))
__mapper_args__ = {
'polymorphic_identity':'employee',
'polymorphic_on':type
}
#classmethod
def get_all(cls, session, query):
_filters = []
for prop in class_mapper(cls).iterate_properties:
if isinstance(prop, ColumnProperty):
_col = prop.columns[0]
_attr = getattr(cls, _cls.name)
_filters.append(cast(_attr, String).match(query))
result = session.query(cls)
result = result.filter(or_(*_filters))
return result.all()
class Engineer(Employee):
__tablename__ = 'engineer'
id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
engineer_name = Column(String(30))
foo = Column(String(10))
__mapper_args__ = {
'polymorphic_identity':'engineer',
}
class Manager(Employee):
__tablename__ = 'manager'
id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
manager_name = Column(String(30))
bar = Column(String(20))
__mapper_args__ = {
'polymorphic_identity':'manager',
}
Now let's say I would like to query all Employee where some of the fields matches a query. The method get_all shown above will only query in Columns known to the class Employee.
Is there some way to query in all columns of the entire inheritance chain?
It's pretty ugly, but one way would be to find all the subclasses that inherit from Employee, then left join those tables and add their columns to the query.
How to get subclasses:
https://stackoverflow.com/a/5883218/443900
Have not tested this, but something like this should work.
#classmethod
def get_all(cls, session, query):
_filters = []
for prop in class_mapper(cls).iterate_properties:
if isinstance(prop, ColumnProperty):
_col = prop.columns[0]
_attr = getattr(cls, _cls.name)
_filters.append(cast(_attr, String).match(query))
result = session.query(cls)
result = result.filter(or_(*_filters))
# get the subclasses
subclasses = set()
for child in cls.__subclasses__():
if child not in subclasses:
subclasses.add(child)
# join the subclass
result = result.outerjoin(child)
# recurse to get the columns from the subclass
result = subclass.get_all(session, result)
# return a query, not a result to allow for the recursion.
# you might need to tweak this.
return result
I have a class which has a parent-child relationship:
Base = declarative_base()
class Parent(Base):
__tablename__ = "parent_table"
id = Column(Integer, primary_key=True)
children = relationship("Child", backref="parent")
def all_children(self):
pass # I want self.children + "Child where parent_id = NULL"
class Child(Base):
__tablename__ = "child_table"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent_table.id')
I would like to add a function to my parent which returns the parent's children in the relationship, and also all Child objects which have their parent_id column set to NULL.
The whole situation is a bit more complex because the classes are actually a case of joined table inheritance like this: table_per_related, but I don't even know where to start so would probably be able to figure it out from there.
(The whole thing must be usable in the context of a web service via Flask)
Edit: Update. This is a minimal implementation of what I really wanted to do, as I can't quite manage to translate the first answer into something that works in this context:
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
class BaseCols:
id = Column(Integer, primary_key=True)
name = Column(String)
def __repr__(self):
return "<{}: {} - {}>".format(self.__class__.__name__, self.id, self.name)
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
Base = declarative_base(cls=BaseCols)
class Child(BaseCols):
pass
class HasChild:
#declared_attr
def children(cls):
cls.Child = type("{}Child".format(cls.__name__),
(Child, Base,),
dict(
__tablename__="{}_children".format(cls.__tablename__),
parent_id=Column(Integer, ForeignKey("{}.id".format(cls.__tablename__))),
parent=relationship(cls)
)
)
return relationship(cls.Child)
def all_children(self):
pass
class Foo(Base, HasChild):
__tablename__ = 'foo'
__mapper_args__ = {'concrete': True}
class Bar(Base, HasChild):
__tablename__ = 'bar'
__mapper_args__ = {'concrete': True}
if __name__ == "__main__":
engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)
session = Session(engine)
session.add_all([
Foo(
name = "Foo the first!",
children = [
Foo.Child(name="Heir Apparent."),
Foo.Child(name="Spare.")
]
),
Foo(
name = "Foo the second...",
children = [
Foo.Child(name="Some child."),
]
),
Bar(
name = "Bar the first!",
children = [
Bar.Child(name="Bar's.")
]
),
Foo.Child(name="whoops"),
])
session.commit()
foo1 = session.query(Foo).first()
print(foo1)
print(foo1.children)
print(foo1.all_children(session))
Something like this?
class Child(Base):
# ...
class Parent(Base):
# ...
def all_children(self):
return Child.query.filter((Child.parent_id == self.id) | (Child.parent_id == None)).all()
Note that I moved the Child class above Parent, so that it can be referenced in all_children().
Update: Here is the implementation for the code you added to your question:
def all_children(self, session):
cls = self.__class__.Child
return session.query(cls).filter((cls.parent_id == self.id) |
(cls.parent_id == None)).all()