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()
Related
I am trying to delete an association object. It is deleted from the table but the parent object still has the child object in its collection. Here is my Code:
import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
engine = sqlalchemy.create_engine('sqlite:///:memory:')
Base = declarative_base()
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
children = relationship("Association", back_populates="parent")
def __repr__(self) -> str:
return f"Parent: {self.id}, {self.children}"
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parents = relationship("Association", back_populates="child")
def __repr__(self) -> str:
return f"Child: {self.id}, {self.parents}"
class Association(Base):
__tablename__ = 'association'
parent_id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
child_id = Column(Integer, ForeignKey('child.id'), primary_key=True)
child = relationship("Child", back_populates="parents")
parent = relationship("Parent", back_populates="children")
def __repr__(self) -> str:
return f"Association: ({self.parent}); ({self.child};)"
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
session = Session()
p = Parent()
c = Child()
session.add(p)
session.add(c)
session.commit()
association = Association(parent=p, child=c)
session.add(association)
session.commit()
assert session.query(Association).all()
assert session.query(Parent).all()[0].children
assert session.query(Child).all()[0].parents
session.delete(association)
session.commit()
assert not session.query(Association).all()
assert not session.query(Parent).all()[0].children
assert not session.query(Child).all()[0].parents
Now when I delete an association object and query it from the table association it's gone. However if I query the parent or child object the association is still in their collection field. How can I configure it, so when I delete the association object the object is also removed completely from the Parent collection and the Child collection.
This is because you've set expire_on_commit=False. This means that any time you commit, existing instances are not expired.
If you set it to True (the default value), you'll get the behaviour you'd expect.
See the SQLAlchemy documentation for details.
If using expire_on_commit=True is not desired for some reason, you can manually expire instances using Session#expire or Session#expire_all.
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)]
I have a class Parent and two of its subclass Child and ChildOne.
I am able to add data to each of the table and read the data.
Now I want to update Child class's row such that it becomes a row in ChildOne table that is when I update type to "old" from "young", I want the row to get deleted from child_one table and added to child table, but without losing the "id" value (which is the primary key).
Is there any way that SQLAlchemy itself handle this? or any other idea to achieve this?
from sqlalchemy import create_engine, ForeignKey
from sqlalchemy import Column, Integer, String, case, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base, declared_attr
engine = create_engine('sqlite:///testing.db', echo=True)
Base = declarative_base()
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
type = Column(String)
def __init__(self, name):
self.name = name
self.type = type
__mapper_args__ = {
'polymorphic_identity':'parent',
'polymorphic_on': case(
[(type == "young", "child_one"),
(type == "old", "child")]
)
}
class Child(Parent):
__tablename__ = "child"
id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
school = Column(String, default="some high school")
def __init__(self, name, school):
self.name = name
self.type = type
self.school = school
__mapper_args__ = {
'polymorphic_identity':'child'
}
class ChildOne(Parent):
__tablename__ = "child_one"
id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
school = Column(String, default="UCLA")
mode = Column(Boolean, default=1)
bool = Column(Boolean, default=0)
def __init__(self, name, type, school, mode, bool):
self.name = name
self.type = type
self.school = school
self.mode = mode
self.bool = bool
__mapper_args__ = {
'polymorphic_identity':'child_one'
}
from sqlalchemy import event
# standard decorator style
#event.listens_for(Child, 'after_update')
def receive_after_update(mapper, connection, target):
"listen for the 'after_update' event"
if target.type == "young":
ChildOne(id=target.id)
But honestly, you should just be creating triggers in the database from something like this. It doesn't require overly complex sqlalchemy code.
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.
Edit: I would like to model a 1 to 0:1 relationship between User and Comment (a User can have zero or one Comment). Instead of accessing the object Comment I would rather directly access the comment itself. Using SQLAlchemys association_proxy works perfect for that scenario except for one thing: accessing User.comment before having a Comment associated. But in this case I would rather expect None instead of AttributeError as result.
Look at the following example:
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy import Column, Integer, Text, ForeignKey, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __init__(self, name):
self.name = name
# proxy the 'comment' attribute from the 'comment_object' relationship
comment = association_proxy('comment_object', 'comment')
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
comment = Column('comment', Text, nullable=False, default="")
user_id = Column(ForeignKey('users.id'), nullable=False, unique=True)
# user_id has to be unique to ensure that a User can not have more than one comments
def __init__(self, comment):
self.comment = comment
user_object = orm.relationship(
"User",
uselist=False, # added after edditing the question
backref=orm.backref('comment_object', uselist=False)
)
if __name__ == "__main__":
engine = sa.create_engine('sqlite:///:memory:', echo=True)
Session = orm.sessionmaker(bind=engine)
Base.metadata.create_all(engine)
session = Session()
Now, the following code throws an AttributeError:
u = User(name="Max Mueller")
print u.comment
What would be the best way to catch that exception and provide a default value instead (like an empty string)?
You don't really need association_proxy for this. You could really get by just fine with a regular property. The AttributeError is (probably) caused because the comment_object is itself None, since there is no dependent row, and None has no comment attribute.
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __init__(self, name):
self.name = name
# proxy the 'comment' attribute from the 'comment_object' relationship
#property
def comment(self):
if self.comment_object is None:
return ""
else:
return self.comment_object.comment
#comment.setter
def comment(self, value):
if self.comment_object is None:
self.comment_object = Comment()
self.comment_object.comment = value
Try this
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy import Column, Integer, Text, ForeignKey, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __init__(self, name):
self.name = name
# proxy the 'comment' attribute from the 'comment_object' relationship
comment = association_proxy('comment_object', 'comment')
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
comment = Column('comment', Text, nullable=False, default="")
user_id = Column(ForeignKey('users.id'), nullable=False)
def __init__(self, comment):
self.comment = comment
user_object = orm.relationship(
"User",
backref=orm.backref('comment_object'),
uselist=False
)
if __name__ == "__main__":
engine = sa.create_engine('sqlite:///:memory:', echo=True)
Session = orm.sessionmaker(bind=engine)
Base.metadata.create_all(engine)
session = Session()
u = User(name="Max Mueller")
# comment = Comment("")
# comment.user_object = u
# session.add(u)
# session.commit()
print "SS :", u
print u.comment
You gave uselist in backref which must be in relationship.
I do not see any answer that would solve the issue and also would work with "sort_by" for example.
Maybe it is just better to use 'column_property", see Order by association proxy: invalid sql.