9

I have a four classes like so: Group, Parent, Child, Toy.

  • Group has a parents relationship pointing to Parent
  • Parent has a children relationship pointing to Child
  • Child has a toys relationship pointing to Toy

Parent has a toys association_proxy that produces all the Toys that the Parent's children have.

I want to be able to get all the Toys in a Group. I tried to create an association_proxy on Group that links to Parent's toys, but it produces this:

[[<Toy 1>, <Toy 2>], [], [], []]

when I want this:

[<Toy 1>, <Toy 2>]

If the Parent's children don't have any Toys, then the toys association proxy is []. However, the second association proxy doesn't know to exclude the empty lists. Also, the lists should be collapsed. Is there anyway to get this to work?

class Group(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    created_at = db.Column(db.DateTime, default=utils.get_now_datetime)
    name = db.Column(db.String(80, convert_unicode=True))
    # relationships
    parents = db.relationship('Parent', backref='group')
    toys = association_proxy('parents', 'toys')

class Parent(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    group_id = db.Column(db.Integer, db.ForeignKey('group.id', ondelete='CASCADE'))
    created_at = db.Column(db.DateTime, default=utils.get_now_datetime)
    first_name = db.Column(db.String(80, convert_unicode=True))
    last_name = db.Column(db.String(80, convert_unicode=True))
    children = db.relationship('Child', backref='parent', cascade='all, delete')
    toys = association_proxy('children', 'toys')

class Child(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
    created_at = db.Column(db.DateTime, default=utils.get_now_datetime)

class Toy(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    child_id = db.Column(db.Integer, db.ForeignKey('child.id', ondelete='CASCADE'))
    created_at = db.Column(db.DateTime, default=utils.get_now_datetime)
    child = db.relationship('Child', backref=db.backref("toys",cascade="all, delete-orphan", order_by="desc(Toy.id)"))
Patrick Yan
  • 2,338
  • 5
  • 25
  • 33
  • Is this navigation from (grand-)grand-parent only for viewing or also for modifying the underlying collections? – van Dec 03 '14 at 22:11
  • OK, in such case how exactly do you envision adding toys to the `Group`? if you do `my_group.toys.append(my_new_toy)`, to which `Parent` and `Child` will it be assigned? – van Dec 04 '14 at 05:05
  • Also, is it true that Group2Parent is one-to-many relationship, and so is Parent2Child, and Child2Toys? – van Dec 04 '14 at 05:06
  • @PatrickYan Can you add the relevant class with the `association_proxy` code? – Anshul Goyal Dec 04 '14 at 12:57
  • @van - No adding since the parent would be ambiguous. Yes, they are one-to-many. – Patrick Yan Dec 04 '14 at 14:39
  • But where is your `Child` table? – van Dec 04 '14 at 14:51

1 Answers1

9

Given that those are for retrieval and view only (as you mentioned in the comment, adding would be ambiguous), I would rather do a viewonly relationship without an association_proxy:

class Group(db.Model):
    # ...
    toys = relationship('Toy',
        secondary="join(Group, Parent, Group.id == Parent.group_id).join(Child, Parent.id == Child.parent_id)",
        primaryjoin="and_(Group.id == Parent.group_id, Parent.id == Child.parent_id)",
        secondaryjoin="Child.id == Toy.child_id",
        viewonly=True,
    )

Note that this is a new feature of SQLAlchemy and is describe in the Composite “Secondary” Joins section of the documentation.

Then you can use it just for query:

group_id = 123
group = session.query(Group).get(group_id)
print(group.toys)

Or you can even use it to filter, so to find a group which contains a toy with name "Super Mario" you can do:

group = session.query(Group).filter(Group.toys.any(Toy.name == "Super Mario"))

But in reality all this you can do with simple query, or create a query-enabled property. See Customizing Column Properties section of the documentation, where you can use any of the simple property, column_property or hybrid attribute.

van
  • 74,297
  • 13
  • 168
  • 171
  • Cool, haven't seen anything about these composite join relationships! I will try that. Yeah, I've been different queries with several joins, but it adds extra lines that I'd like to get rid of. Plus filtering makes this even better! – Patrick Yan Dec 04 '14 at 17:36