[starting 1.4.7] Children objects are duplicated when setting backref #6294
Replies: 13 comments 6 replies
-
ok, you're using lazy="immediate" and we necessarily changed some things about it in #6232. I havent deeply understood waht's happening here yet, but if we take out those "immediateload" settings, then run your script in any SQLAlchemy version, the assertion at the end fails in the same way. the "immediateload" directive should not change any behaviors, so this suggests this script is somehow relying on the previously broken behavior of "immediateload", since now the script behaves consistently no matter what loader strategy is used. it looks like in your operation you are essentially doing this:
whether or not we like what that says, that's the normal behavior. what I'm not getting yet is how the former "immediateload" behavior was changing this, as it seems to be, so i will do that now but at the moment it's looking like this program somehow relies upon a weird unexpected behavior. |
Beta Was this translation helpful? Give feedback.
-
Thanks for the impressively fast response :-) I am not quite sure to understand how what you show is a normal behaviour ? Naively, I would expect that this |
Beta Was this translation helpful? Give feedback.
-
when you append to a relationship collection that has a backref to a many-to-one, or you assign to a many-to-one that has a backref to a collection, there's an attribute event that will populate in the other direction. What seems to be happening here is that the "immediateload" was previously being fired off on the other side of the So what would happen is that since Child._parent was already loaded, when you set it again, no backref event would grow the collection. With the repaired behavior, Child._parent is not loaded so when you assign to it, the relationship mechanics don't try to guess what the old value was, the new one goes right in, which fires off the event. the overall problem with the program here is that it is doubling the work that backref already does. if you simply remove the whole _notify() step, which is unnecessary, as well as the redundant child._parent assignments that follow the equivalent collection operation. you get a much shorter script and everything works: class Parent(object):
""" Parent class, managing children """
def __init__(self):
self.ParentName = None
def _notify(self):
pass
# for child in self.__children__:
# this should never be done if backrefs are in use
# child._parent = self
def __contains__(self, child):
return bool(child in self.__children__)
def __iadd__(self, child):
"""Add a Child to the list of children, and
set the backref
"""
if child not in self:
self.__children__.append(child)
# child._parent = self # redundant
# self._notify()
return self
def __delitem__(self, i):
self.__children__.__delitem__(i)
# self._notify() # redundant
def __len__(self):
""" nb of subParents """
return len(self.__children__) |
Beta Was this translation helpful? Give feedback.
-
also if you want to avoid dupes in a collection, you would use collection_class=set. |
Beta Was this translation helpful? Give feedback.
-
I see thanks. So basically, what I need to make sure is that I do not set the |
Beta Was this translation helpful? Give feedback.
-
you are....mapping classes after you've used instances of those classes? |
Beta Was this translation helpful? Give feedback.
-
Yep :-/ We create the instances, do whatever we have to do with them, and later on persist them with SQLAlchemy in the DB. The reason being that they are created on some machines without access to the DB (nor to SQLAlchemy in fact), used, and then serialized over the network to later be persisted. And they go the other way around too. |
Beta Was this translation helpful? Give feedback.
-
OK but I would assume the notion is you have code that deals with objects in both states and does not know which state they are in. this is a really dangerous pattern and I think your app should "know" whether or not classes are mapped, and have distinct codepaths for dealing with them. Also no database access is implied by having SQLAlchemy installed, imported, and the classes mapped. It doesn't make sense that you have your software installed in Python environments where SQLAlchemy is not installed, you should add it to your requirements for those installations and the classes should just be mapped up front so that they behave consistently. all of that aside, in this case if you are duplicating the behavior of a backref because your current envrionment may or may not have imported SQLAlchemy and mapped the classes or not, you should check if "child._parent is self" before setting it. |
Beta Was this translation helpful? Give feedback.
-
I totally agree that it is a bad design to start with. A redesign I have on my plate is long overdue :-/ SQLAlchemy 1.4 seems to have cleaned a lot of things, so the flaws in my piece of code start showing up.
That actually raises a |
Beta Was this translation helpful? Give feedback.
-
OK look inside |
Beta Was this translation helpful? Give feedback.
-
Thanks. turns out I also need to check Clearly, I must redesign my code... It actually comes from legacy code, before using SQLAlchemy. At the time of introducing, one requirement we had that we could still have clients without sqlalchemy installed at all. I understand that this mixed environment is clearly not something you recommend ? |
Beta Was this translation helpful? Give feedback.
-
it just seems unnecessary unless this is an application running on raspberry pi's or cellphones? |
Beta Was this translation helpful? Give feedback.
-
So I tried to let SQLAlchemy deal with the from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from sqlalchemy.orm import relationship, backref, sessionmaker, mapper
from sqlalchemy import create_engine, Table, Column, MetaData, ForeignKey, \
Integer, String
class Parent(object):
def __init__(self, ParentName):
self.ParentName = ParentName
class Child(object):
def __init__(self, ChildName):
self.ChildName = ChildName
metadata = MetaData()
childTable = Table('Child', metadata,
Column('ChildName', String(64), nullable=False),
Column('ChildID', Integer, primary_key=True),
Column('ParentID', Integer,
ForeignKey('Parent.ParentID', ondelete='CASCADE'),
nullable=False),
mysql_engine='InnoDB')
mapper(Child, childTable)
parentTable = Table('Parent', metadata,
Column('ParentName', String(255), nullable=False),
Column('ParentID', Integer, primary_key=True),
mysql_engine='InnoDB')
mapper(Parent, parentTable, properties={'__children__': relationship(Child,
backref=backref('_parent',
lazy='joined'),
lazy='joined',
passive_deletes=True,
cascade="all, delete-orphan")})
engine = create_engine('mysql://root:password@mysql:3306/Test')
metadata.bind = engine
DBSession = sessionmaker(bind=engine)
metadata.create_all(engine)
#######################################
# Create the test data
parent = Parent("Father")
ch1 = Child("Child1")
parent.__children__.append(ch1)
with DBSession(expire_on_commit=False) as session:
parent = session.merge(parent)
session.add(parent)
session.commit()
session.expunge_all()
parentID = parent.ParentID
###############################
# Get the data back
with DBSession(expire_on_commit=False) as session:
parent2 = session.query(Parent) \
.filter(Parent.ParentID == parentID)\
.one()
session.expunge_all()
session.close()
print(parent.__children__)
print(parent2.__children__)
print(parent.__children__[0]._parent)
# That last one fails
print(parent2.__children__[0]._parent) This is the output
I would expect that the |
Beta Was this translation helpful? Give feedback.
-
Describe the bug
I have
Parent
andChild
classes, with a one-to-many relationship. TheChild
class has abackref
to theParent
called_parent
. These objects often live outside ofSQLAlchemy
, so I need to maintain this reference myself sometimes.If I get a family from the DB, and then try to assign myself the
backref
parameter, the children objects are duplicatedExpected behavior
This behaviour is new in
1.4.7
and still happens against the master branch of the repo.When running the example bellow with 1.4.7 and 1.4.6, the debug logs are perfectly identical.
To Reproduce
Error
Versions.
Have a nice day!
Beta Was this translation helpful? Give feedback.
All reactions