This worked.
I'm trying to achieve some rather tricky behaviour, where
1. Adding a children to some parent will also add the child's pets to
the parent
2. Removing a children from some parent will also remove every current
relationship that the Parent has with such pet
3. If upon removal of a pet from a Parent, there is a Pet.child that is
also in Parent.children,
- remove that Child from Parent, but keep existing relationships in
Parent.pets *except the pet that is being removed*
- else only remove the pet from the parent
Some code I'm using for this is
@db.event.listens_for(Parent.children, 'append')
def _append_children(parent, child, initiator):
"""
If a new child is appended to the parent, this listener
will also add the pets bound to the child being bound to the parent.
"""
# appends also the pets bound to the child that the
# parent is being appended to
parent.pets.extend(child.pets.all())
@db.event.listens_for(Parent.children, 'remove')
def _remove_children(parent, child, initiator, *args, **kwargs):
"""
If a child is removed from the parent, this listener
will also remove only remove_single_pet --> <Pet>
"""
remove_single_pet = kwargs.get('remove_single_pet', None)
if remove_single_pet is not None:
parent.pets.remove(remove_single_pet)
else: # removes every pet
for pet in child.pets:
parent.pets.remove(pet)
@db.event.listens_for(Parent.pets, 'remove')
def _remove_pets(parent, pet, initiator, *args, **kwargs):
"""
If a pet is removed from the parent, and the parent also is related
to the child that has access to that pet, then
* removes relationship with the child, and
* keeps relationship with the remaining pets, except the one that was
removed
"""
if pet.child in parent.children.all():
remove_single_pet = pet
_remove_children(parent, pet.child, initiator, remove_single_pet)
#### test.py
def test_child_pet_relationship_on_parents(self):
# create new parent
test_parent = Parent(name='test_parent')
# commit parent to the database
db.session.add(test_parent)
db.session.commit()
child1 = Child(id=1,
name='FakeChild1')
child2 = Child(id=2,
name='FakeChild2')
pet1 = Pet(id=1,
name='FakePet1',
child_id=1)
pet2 = Pet(id=2,
name='FakePet2',
child_id=2)
pet3 = Pet(id=3,
name='FakePet3',
child_id=1)
db.session.add(child1)
db.session.add(child2)
db.session.add(pet1)
db.session.add(pet2)
db.session.add(pet3)
db.session.commit()
# add parent to the child
child1.parents.append(test_parent)
# add parent to the child
pet2.parents.append(test_parent)
# persist changes in the db
db.session.add(child1)
db.session.add(pet2)
db.session.commit()
# check that previous relationships are intact
self.assertTrue(child1.pets.all() == [pet1, pet3])
self.assertTrue(child2.pets.all() == [pet2])
# resultant elements should be only child1, its pets and the single
Pet
self.assertTrue(test_parent.children.all() == [child1])
self.assertTrue(test_parent.pets.all() == [pet1, pet2, pet3])
# remove child from parent
pet3.parents.remove(test_parent)
# resultant elements should be remaining pets, and no child
self.assertTrue(test_parent.children.all() == [])
self.assertTrue(test_parent.pets.all() == [pet1, pet2]) # pet2 was
not touched,
# but pet1
should remain since only
# pet3 was
removed
# child1
should be also removed since
#
relationship is unbalanced, i.e.
# user
can't have access to a child if it
# does not
have access to all of the child's pets
I'm having errors
sqlalchemy.orm.exc.StaleDataError: DELETE statement on table
'parent_pets_relationship' expected to delete 1 row(s); Only 0 were matched.
and the logger says nothing much. I suspect I'm falling into some weird
recursion, calling the listener from another listener, where the second
call can't find something that was already deleted.
This might be a long shot, but I'm hoping this pattern might be solved
already.
Am Donnerstag, 26. April 2018 13:00:45 UTC-3 schrieb Mike Bayer:
>
> On Thu, Apr 26, 2018 at 11:04 AM, Diego Quintana <[email protected]
> <javascript:>> wrote:
> > Hello.
> >
> > Say I have three tables in a declarative fashion, `Parent`, `Child`, and
> > `Pet`, in such way that
> >
> > * `Parent` has a many-to-many relationship with both `Child` and `Pet`,
> > meaning that a Parent can own a Child and its pets, and also a Pet
> without
> > its Child.
> > * `Child` has a one-to-many relationship with `Pet`
> >
> > The code for them is (using Flask-SQLAlchemy, although I believe the
> > solution lives in the realm of SQLAlchemy rather than in Flask).
> >
> > class Parent(db.Model):
> > __tablename__ = 'parents'
> >
> > id = db.Column(db.Integer, primary_key=True)
> > name = db.Column(db.String(64))
> >
> > # many to many relationship between parent and children
> > # my case allows for a children to have many parents. Don't ask.
> > children = db.relationship('Child',
> > secondary=parents_children_relationship,
> > backref=db.backref('parents',
> > lazy='dynamic'),
> > lazy='dynamic')
> >
> > # many to many relationship between parents and pets
> > pets = db.relationship('Pet',
> > secondary=users_pets_relationship,
> > backref=db.backref('parents',
> > lazy='dynamic'), #
> > lazy='dynamic')
> >
> > # many to many relationship between parents and children
> > parents_children_relationship =
> > db.Table('parents_children_relationship',
> > db.Column('parent_id', db.Integer, db.ForeignKey('parents.id')),
>
> > db.Column('child_id', db.Integer, db.ForeignKey('children.id')),
>
> > UniqueConstraint('parent_id', 'child_id'))
> >
> > # many to many relationship between User and Pet
> > users_pets_relationship = db.Table('users_pets_relationship',
> > db.Column('parent_id', db.Integer, db.ForeignKey('parents.id')),
>
> > db.Column('pet_id', db.Integer, db.ForeignKey('pets.id')),
> > UniqueConstraint('parent_id', 'pet_id'))
> >
> > class Child(db.Model):
> > __tablename__ = 'children'
> > id = db.Column(db.Integer, primary_key=True)
> > name = db.Column(db.String(64))
> > # parents = <backref relationship with User model>
> >
> > # one to many relationship with pets
> > pets = db.relationship('Pet', backref='child', lazy='dynamic')
> >
> >
> > class Pet(db.Model):
> > __tablename__ = 'pets'
> > id = db.Column(db.Integer, primary_key=True)
> > name = db.Column(db.String(64))
> > # child = backref relationship with cities
> > child_id = db.Column(db.Integer, db.ForeignKey('children.id'),
> > nullable=True)
> > # parents = <relationship backref from User>
> >
> >
> >
> > I would like to do something like this
> >
> > parent_a = Parent()
> > child_a = Child()
> > pet_a = Pet()
> >
> >
> >
> > I can then do this
> >
> > parent_a.children.append(child_a)
> > # commit/persist data
> > parent_a.children.all() # [child_a]
> >
> >
> >
> > I would like to achieve something like this
> >
> > child_a.pets.append(pet_a)
> > parent_a.children.append(child_a)
> > # commit/persist data
> > parent_a.children.all() # [child_a]
> > parent_a.pets.all() # [pet_a], because pet_a gets
> > # automatically added to parent using some
> sorcery
> > # like for child in parent_a.children.all():
> > # parent.pets.append(child.pets.all())
> > # or something like that.
> >
> > I can achieve this with a method in the Parent object like
> > add_child_and_its_pets(), but I would like to override the way
> relationship
> > works, so I don't need to override other modules that may benefit from
> this
> > behaviour, like Flask-Admin for instance.
> >
> >
> > Basically how should I override the backref.append method or the
> > relationship.append method to also append other objects from other
> > relationships at call time i.e. on the python side? How should I
> override
> > the remove methods as well?
>
> this seems like coordination of two separate relationships, so use
> attribute events for that,
>
> http://docs.sqlalchemy.org/en/latest/orm/events.html?highlight=attributeevent#sqlalchemy.orm.events.AttributeEvents
>
> :
>
> @event.listens_for(Parent.children, "append")
> def _append_pets(parent, child, initiator):
> parent.pets.extend(child.pets) # or whatever it is you need
>
> you would need to look at the "append" and "remove" events.
>
>
>
> >
> >
> > I have also posted this question in Stack Overflow, in case it means
> > something.
> >
> >
> > Best!
> >
> >
> >
> > --
> > SQLAlchemy -
> > The Python SQL Toolkit and Object Relational Mapper
> >
> > http://www.sqlalchemy.org/
> >
> > To post example code, please provide an MCVE: Minimal, Complete, and
> > Verifiable Example. See http://stackoverflow.com/help/mcve for a full
> > description.
> > ---
> > You received this message because you are subscribed to the Google
> Groups
> > "sqlalchemy" group.
> > To unsubscribe from this group and stop receiving emails from it, send
> an
> > email to [email protected] <javascript:>.
> > To post to this group, send email to [email protected]
> <javascript:>.
> > Visit this group at https://groups.google.com/group/sqlalchemy.
> > For more options, visit https://groups.google.com/d/optout.
>
--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper
http://www.sqlalchemy.org/
To post example code, please provide an MCVE: Minimal, Complete, and Verifiable
Example. See http://stackoverflow.com/help/mcve for a full description.
---
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.