[TransWarp] First attempt to use the storage package in PEAK

Phillip J. Eby pje at telecommunity.com
Tue Dec 24 14:11:08 EST 2002

At 11:19 AM 12/24/02 +0200, Roché Compaan wrote:
>Hi Philip
>Here is my first quick attempt to use the storage package in PEAK.
>Creating new instances work fine but loading and saving doesn't work
>yet.  My use of the binding package is still a shot in the dark. Can you
>maybe comment on the way I used the various parts of PEAK below:
>from peak.storage.SQL import MySQLConnection
>from peak.api import storage
>from peak.api import binding, model
>from peak.model.datatypes import String

Sure.  These classes:

>class Address:
>     server = binding.requireBinding('server')
>     db = binding.requireBinding('db')
>     user = binding.requireBinding('user')
>     passwd = binding.requireBinding('passwd')
>class ContactsDB(MySQLConnection):
>     __implements__ = storage.ISQLConnection
>     address = Address()
>     address.server = 'localhost'
>     address.db = 'Contacts'
>     address.user = 'roche'
>     address.passwd = 'mypasswd'

are unnecessary and can be deleted.  I'll show you how, later on below.

>class Contact(model.Element):
>     Name = binding.New(String)
>     Surname = binding.New(String)
>     HomePhone = binding.New(String)
>     WorkPhone = binding.New(String)
>     Email = binding.New(String)

class Contact(model.Element):

      class Name(model.Field):
          referencedType = String

      class Surname(model.Field):
          referencedType = String


But it isn't actually necessary for you to do all that; if you don't care 
about any of the special capabilities provided by 'peak.model' bases and 
metaclassses, you can just subclass Persistence.Persistent and be done with it.

Also, 'peak.model' has a significant refactoring pending, and it's not well 
documented.  For the time being, you may not want to bother with it.  But 
if you want examples of its use, look at:

peak.metamodels.uml.MetaModel, and

More or less in that order, with the caveat that MOF131 is untested and 
might contain errors, or that it might have code that depends on 
refactorings in 'peak.model' that haven't actually been done yet.  :)

>class ContactDM(storage.EntityDM):
>     defaultClass = Contact
>     attrs = ['Name', 'Surname', 'HomePhone', 'WorkPhone', 'Email']
>     DBConn = binding.bindTo(storage.ISQLConnection)

Replace the line above with:

      DBConn = binding.bindTo("mysql://roche:mypasswd@localhost/Contacts")

This suffices to instantiate a MySQLConnection.  You also don't need to 
import MySQLConnection.  This is one of the things that the naming system 
is for.  :)

>     def defaultState(self, ob):
>         state = {}
>         for attr in self.attrs:
>             state[attr] = ''
>         return state
>     def load(self, oid, ob):
>         print "load"
>         result = self.DBConn('SELECT * FROM Contact WHERE oid=%s'%oid)
>         state = {}
>         for r in result:
>             for attr in self.attrs:
>                 value = getattr(r, attr)
>                 state[attr] = value
>         return state

Another way to specify the above is:

def load(self, oid, ob):
     return dict(
         (~self.DBConn('SELECT * FROM Contact WHERE oid=%s' % oid)).items()

Two things are happening here...  first the '~' means you want exactly one 
row from the result.  If there are more or fewer, an exception will be 
raised.  Second, database result rows support a mapping interface, 
including items(), keys(), etc.  So you could also write:

def load(self, oid, ob):
     state = {}
     for r in self.DBConn('SELECT * FROM Contact WHERE oid=%s'%oid):
         for attr in self.attrs:
             state[attr] = r[attr]
     return state

Anyway, there's several ways to skin this cat.  If you knew that the 
columns were in the same order as the 'attrs' list, you could do:

def load(self, oid, ob):
     row = ~ self.DBConn('SELECT * FROM Contact WHERE oid=%s'%oid)
     return dict(zip(self.attrs, row))

Which would pair each self.attrs value with its corresponding column value.

>     def new(self, ob):
>         print "new"
>         sql = "INSERT INTO Contact (%s) VALUES (" % ','.join(self.attrs)
>         values = []
>         for attr in self.attrs:
>             values.append('"%s"' % getattr(ob, attr))

I'd recommend '%r' instead of '%s', or that you otherwise do the necessary 
SQL quoting on the attribute value, in case it contains quotes.

>         sql += ','.join(values) + ")"
>         self.DBConn(sql)
>         # get new oid
>         sql = """SELECT MAX(oid) FROM Contact"""
>         for r in self.DBConn(sql):
>             return r[0]

I should remind you that doing a SELECT MAX() is subject to race conditions 
in a multi-process environment.

>     def save(self, ob):
>         sql = "UPDATE Contact SET "
>         sql_where = "oid = %s" % ob._p_oid
>         values = []
>         for attr in self.attrs:
>             value = getattr(ob, attr)
>             values.append('%s = "%s"' % (attr, value))
>         sql += ','.join(values)
>         sql += sql_where
>         self.DBConn(sql)

Looks fine.

Replace these next few lines...

>db = ContactsDB()
>myDM = ContactDM()
>myDM.DBConn = db

with just:

myDM = ContactDM()

The rest will happen automatically.

># Create a new instance
>ni = myDM.newItem()
>ni.Name = 'John'
>ni.Surname = 'Smith'
>ni.HomePhone = '888 0000'
>ni.WorkPhone = '999 0000'
>ni.Email = 'john at smith.com'

Looks good, but that should be 'storage.begin(myDM)', not 'ContactDM'.  You 
want the *instance* to join the transaction, not the class.

Since your code is using only a single global transaction, you can also 
simplify to just 'storage.begin()' and 'storage.commit()', since passing 
the DM in isn't going to cause it to find a different transaction service.

># Retrieve and modify existing instance with oid=1
>ni = myDM.preloadState(1)

ni = myDM[1]

The "preloadState()" method is not for this purpose.  Use __getitem__ to 
retrieve objects.  'preloadState()' is for collaboration with query 
DMs.  Consider a report that is a contact list.  You want to do a select to 
get a lot of contacts, but if you work with the individual contact objects, 
you have to select again.  'preloadState()' allows you to bypass this, by 
having the query DM pass preloaded rows to the entity DM.  So if you had a 
ContactList query DM, when it selects multiple rows, it can call 
ContactDM.preloadState() with an 'oid' and a usable 'state'.  If the object 
is in cache, its state is updated, or if not, a fresh ghost is created and 
loaded with the state.  In this way, you can mass load objects into the 
cache, or load them one at a time as you iterate through the rows of a 
reporting query.

># I would have thought that the object will get loaded here but it
># doesn't.  If I call print ni.HomePhone before assigning to it the load
># method on the DM does get called.

Right.  That's because you didn't pass a state to 'preloadState()'.  In any 
case, __getitem__ doesn't preload the state, and that's what you should use.

Lazy loading in this fashion is a big advantage for relational 
models.  Let's say that Contacts were associated with a Department.  When I 
load the state for the Contact, I could say something like:

state['Department'] = self.DepartmentDM[row.departmentID]

The department object will not actually be loaded yet, until you need to 
touch it.  This is good because a department might have a parent department 
and so on, and maybe a document repository, or various other things.  You 
don't want your entire database to get loaded in order to look at just one 
object.  :)

Anyway, this lazy-load model is not restricted to PEAK.  It's standard for 
ZODB persistence and has been in Zope since the beginning.  And ZPatterns 
implements it too, but on a per-attribute basis.  PEAK mostly follows the 
ZODB pattern of loading the object's state all at once, but it also 
supports LazyLoader objects which can be used to load selected attributes 
even later than "object state load" time.

>ni.HomePhone = '777 0000'


More information about the PEAK mailing list