Re: question about added status

Jason Madden <[email protected]> Thu, 21 Feb 2019 06:44:19 -0600
Newsgroups gmane.comp.web.zope.zodb
Message-ID <[email protected]>

> On Feb 20, 2019, at 16:24, Qiwen Chen <[email protected]> wrote:
>=20
> when an unsaved persistent object is set as sub-objects of another persis=
tent object with saved status, the unsaved persistent object becomes Added.

'Added' is not a value that Persistent._p_status or Persistent._p_state can=
 have. Those can have the values 'unsaved', 'ghost', 'sticky', 'changed', '=
saved' or -1, 0, 1, 2, respectively (https://github.com/zopefoundation/pers=
istent/blob/master/persistent/interfaces.py). So I'm not quite sure what yo=
u mean.=20

Is 'Added' an application level concept? Are you talking about being presen=
t in the private attribute `_added` of a ZODB `Connection`? Or do you mean =
"reachable in the object graph from the connection's root"? I'll assume thi=
s last meaning because that's the most general (and is basically what it me=
ans to a Connection).

> However, when i check _p_jar, it is still None.

That's correct. Simply because an object is reachable from a connection's r=
oot doesn't mean its `_p_jar` has been set. Most commonly, that happens aut=
omatically at transaction commit time, when a connection conceptually trave=
rses all  objects reachable from its root and takes note of any added or up=
dated ones:

py> import persistent, ZODB, ZODB.DemoStorage, transaction
py> db =3D ZODB.DB(ZODB.DemoStorage.DemoStorage())
py> class O(persistent.Persistent):=20
...     pass
py> parent =3D conn.root.parent =3D O()
# Just because we're reachable doesn't mean we're saved or have a jar
py> parent._p_status
'unsaved'
py> parent._p_jar
py> transaction.commit()
# Once we commit, the modified objects are added to the database and connec=
tion
py> parent._p_status
'saved'
py> parent._p_jar
<Connection at 10bc6ff20>

# The same goes for a new child object added to a saved object
py> child =3D parent.child =3D O()
py> child._p_status
'unsaved'
py> child._p_jar # None
py> transaction.commit()
py> child._p_status
'saved'
py> child._p_jar
<Connection at 10bc6ff20>


> I will have to manually add the object to the connection in order to popu=
late _p_jar with such connection.

I think that generally, manually adding objects to a connection should not =
be necessary. There are cases in multi-database setups where an object is i=
nitially reachable from multiple connections in different databases, and wh=
ich one to add it to is ambiguous; that case can require manually adding an=
 object to the correct database connection or you get an InvalidObjectRefer=
ence exception. But that's rare in my experience.

> Is this a bug?

No, I believe it's by design. Just because an object is *temporarily* reach=
able doesn't mean that it will be reachable at connection commit time. Prem=
aturely adding it to a connection allocates an OID and generates unnecessar=
y garbage in that case:

# Start fresh for clarity
py> db =3D ZODB.DB(ZODB.DemoStorage.DemoStorage())
py> conn =3D db.open()
# Create an object, make it reachable from a database connection
py> parent =3D conn.root.parent =3D O()
# Explicitly add it now
py> conn.add(parent)
# This has the effect of allocating an OID for it, meaning it *will* be sto=
red
py> parent._p_oid
b'\x13\x03\xd7\xee\xe8yu\xd7'

# Now go on and do some other work that results in "rolling back" this obje=
ct
py> conn.root.parent =3D None
py> transaction.commit()
py> conn.root.parent # No longer present

# Yet this unreachable object was still stored in the database,
# where it will have to be garbage collected away
py> conn.get(b'\x13\x03\xd7\xee\xe8yu\xd7')
<__main__.O object at 0x10ba80a78 oid 0x1303d7eee87975d7 in <Connection at =
10bc15aa8>>
# it's visible to new connections, even though it's not reachable
py> conn2 =3D db.open()
py> conn2.root.parent # None
py> conn2.get(b'\x13\x03\xd7\xee\xe8yu\xd7')
<__main__.O object at 0x10bda8ef8 oid 0x1303d7eee87975d7 in <Connection at =
10be3c058>>

# But if we pack the database...
py> db.pack()
# It is no longer visible to *new* connections
py> conn3 =3D db.open()
py> conn3.get(b'\x13\x03\xd7\xee\xe8yu\xd7')
Traceback (most recent call last)
...
POSKeyError: 0x1303d7eee87975d7

# Very confusingly, it is still visible from connections that had it cached
py> conn3.close()
py> conn2.close()
py> conn.close()
py> conn =3D db.open()
py> conn2 =3D db.open()
py> conn3 =3D db.open()
py> conn.get(b'\x13\x03\xd7\xee\xe8yu\xd7')
<__main__.O object at 0x10ba80a78 oid 0x1303d7eee87975d7 in <Connection at =
10bc15aa8>>
py> conn2.get(b'\x13\x03\xd7\xee\xe8yu\xd7')
<__main__.O object at 0x10bda8ef8 oid 0x1303d7eee87975d7 in <Connection at =
10be3c058>>
py> conn3.get(b'\x13\x03\xd7\xee\xe8yu\xd7')
Traceback (most recent call last)
...
POSKeyError: 0x1303d7eee87975d7

> Is there a reliable way to check if an object is added or not?

I suppose it's application specific. In an application using zope.container=
 and zope.location, you could check that there's a traversal path back to t=
he root; zope.keyreference's IConnection adapter can also be helpful.

I don't think ZODB offers a shortcut to find out "is this object reachable =
from the connection's root" before commit time.

~Jason

--=20
You received this message because you are subscribed to the Google Groups "=
zodb" group.
To unsubscribe from this group and stop receiving emails from it, send an e=
mail to [email protected].
For more options, visit https://groups.google.com/d/optout.