On Wed, May 2, 2018 at 10:14 AM, Diego Quintana <daquinta...@gmail.com> wrote: > This worked. > > I'm trying to achieve some rather tricky behaviour, where > > Adding a children to some parent will also add the child's pets to the > parent > Removing a children from some parent will also remove every current > relationship that the Parent has with such pet > 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.
it looks like you have an explicit association mapping on the secondary table as described in the green box in the section https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#association-object, but I dont' have your complete mappings so I can't say for sure, you'd need to provide a complete MCVE. > > 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 <daqui...@gmail.com> >> 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 sqlalchemy+...@googlegroups.com. >> > To post to this group, send email to sqlal...@googlegroups.com. >> > 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 sqlalchemy+unsubscr...@googlegroups.com. > To post to this group, send email to sqlalchemy@googlegroups.com. > 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 sqlalchemy+unsubscr...@googlegroups.com. To post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.