[Zope-dev] ZPatterns AttributeProvider question

Phillip J. Eby pje@telecommunity.com
Sun, 22 Apr 2001 18:00:15 -0500


At 08:29 PM 4/22/01 +0200, Christian Scholz wrote:
>Hi!
>
>A little update..
>
>Actually I was mistaken and SetAttributeFor() is called. Just
_objectChanged()
>is not called in that case.. and thus my attributes are not stored to the
database.

When an object is newly added, only _objectAdded() is called.
_objectAdded(), _objectChanged(), and _objectDeleted() are mutually
exclusive within a given transaction.  That is, only one of them occurs in
a given transaction.  See more below.


>Maybe some more code will help (just the important pieces):
>
>    def _objectAdded(self,client):
>        """ store cached data in database """
>
>	... here we do an sql query to insert things in database ...
>
>    def _objectChanged(self,client):
>        """ store cached data in database """
>
>	... here we do an sql query to update things in database ...
>
>    def _objectAdding(self,client,
>        _tmap={ None:AddedStatus,
>                DeletedStatus:ChangedStatus,
>                ChangedStatus:AddedStatus
>            }
>        ):
>        t = client._getTokenFor(self)
>        s = t.status
>        t.status = _tmap.get(s,s)
>
>        # we need to do a commit here in order to store the record into
the database!
>	# and we need to have it stored as an AttributeFor() of the primary key will
>	# return None otherwise (as this also just makes a query).
>        client.commitSubtransaction()

This is broken.  Don't do this!  commitSubtransaction() is an
application-level operation.  Only.  If you have to use it inside of
provider-level code or code called from SkinScript, chances are that you
are doing something horribly wrong.  You run the risk of causing a
recursive commit operation in the Zope transaction machinery.  Don't do this!



>    def _SetAttributeFor(self,client,name,value):
>        """Set the attribute and return true if successful"""
>	# attribs stores the attributes we care about
>        if name in self._attribs.keys():
>            client._getCache()[name]=value
>            client.__dict__[name]=value; 
>	    client._p_changed = 1
>            return 1
>        return None

This also looks broken to me.  Why are you changing both the cache and the
persistent state?  There's no reason to do both.


    
>So _objectChanging() is not overridden, the rest I have ommitted..
>
>So as you also can see I already do a subtransaction commit but it does
>not seem to help for this problem.. 
>

I think you're seriously over-engineering this.  I don't see anything that
you're doing here that couldn't be done more easily with SkinScript
statements.  If all you want to do is store the data in an SQL database, a
few SkinScript triggers would suffice.

Now, if you're trying to get a row to be inserted in an SQL database at the
actual time of the newItem() call, there is a trick you can use to do that
from SkinScript also.  Do this:

INITIALIZE OBJECT WITH someAttr1=default1,
someAttr2=default2,...,dummyAttributeName=myInsertQuery(primaryKey=self.id)

Where myInsertQuery() is an SQL insert query that takes primaryKey as a
parameter, and dummyAttributeName is some attribute name you don't care
about.  You should also set any default values for fields that will be
inserted.  Your insert query will be called, initializing the row in the
database, and then use something like this:

WHEN OBJECT ADDED,CHANGED STORE foo,bar,baz USING
UpdateMethod(widget_id=self.id, foo_field=self.foo, wbar=self.bar)


To update the data at transaction commit time.  Voila.  No need to make any
custom attribute providers just to do this.

Just to clarify the event sequence that should take place:

1. newItem() is called, which fires an _objectCreating() event

1.1. The SkinScript INITIALIZE OBJECT statement is triggered by the
_objectCreating() event, and calls myInsertQuery() to insert a row in the
database, with default values for everything but the primary key.

1.2. newItem() fires an _objectAdding() event, which does nothing except
flag the object as "AddedStatus" and pass the event back to the DataManager
(Rack).

1.3. The Rack passes the _objectAdding() event to all appropriately
registered providers.  The WHEN OBJECT ADDED,CHANGED statement is such a
provider, so it receives the message and notes that the DataSkin was added,
but does not act on it yet.  Instead, it registers a proxy for itself with
the Zope transaction object so that it will be notified of transaction commit.

2. newItem() returns a new DataSkin to your application, bearing the
contents set up by the INITIALIZE OBJECT WITH statement.

3. Your application calls manage_changeProperties, to change an attribute.

3.1. The DataSkin fires an _objectChanging() event, which flags the object
as "ChangedStatus", and the event is propagated to all registered providers
in the Rack.  (Such as the WHEN OBJECT statement)

3.2. The WHEN OBJECT statement ignores the _objectChanging() event, because
from its point of view the object is in AddedStatus.

3.3. The DataSkin calls _SetAttributeFor(), which the Rack passes on to the
WHEN OBJECT statement.  The statement ignores this except to note which
attribute was changed for future reference, and to save the new value in
the object's attribute cache.

3.4. The DataSkin returns control to the application level code after a
successful manage_changeProperties.

4. The application code ends, returning control to Zope and causing a
transaction commit.

4.1. Zope notifies registered objects that the transaction is commiting.
One of those objects is the WHEN OBJECT statement, which notes that it
should act because the object was indeed added during this transaction,
*and* one of the specified attributes was changed.  So it calls the update
query, which replaces the default data in the row in the database with the
changed attribute value(s).

4.2. Zope begins the second phase of the commit operation, which causes the
SQL database adapter to send a COMMIT TRANSACTION operation to the SQL
database, commiting all the work done.


Whew.  The above is a vastly simplified picture of the whole sequence,
leaving out a lot of things such as how a statement can actually be
responsible for many dataskins at once, the state machine that agents use
to decide which event is considered to have occurred, etc.

Anyway...  the point of all that was...  SkinScript is your friend.  :)
You can do virtually anything by calling something from SkinScript, so
there's almost never a need to go to Python attribute providers.  About the
only times it makes sense to write a specialized provider or agent is when
what you're doing involves access to secured object innards, and you need
it to be cleaner than calling an external method (because for example you
want to distribute the code as part of a Product).

In the early days of ZPatterns, I assumed that I would create SQL
providers, LDAP providers, and suchlike gizmos.  Later, it became clear
that it was more useful to have a simple "glue" language to allow
harnessing the full power of Zope in the context of events happening to
objects.  I still toy with the idea of making an "SQL attribute provider",
however, that would be based on the ZSQLMethod object and add some
ZPatterns hooks to it, but it's not a big priority.  Its main value would
be to cut down on some repetitive typing between one's SQL statements and
SkinScript statements.