Skip to content

Commit

Permalink
Merged trunk to branch. I still ahve 4 ftests failing, but I suspect, it
Browse files Browse the repository at this point in the history
will be fixed once I merge the changes of the last three days (hopefully).
  • Loading branch information
strichter committed Feb 10, 2005
2 parents 527e1a3 + db89a2c commit 19a17ee
Show file tree
Hide file tree
Showing 6 changed files with 410 additions and 27 deletions.
193 changes: 193 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
A quick introduction to generations
===================================

Generations are a way of updating objects in the database when the application
schema changes. An application schema is essentially the structure of data,
the structure of classes in the case of ZODB or the table descriptions in the
case of a relational database.

When you change your application's data structures, for example,
you change the semantic meaning of an existing field in a class, you will
have a problem with databases that were created before your change. For a
more thorough discussion and possible solutions, see
http://dev.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/DatabaseGenerations

We will be using the component architecture, and we will need a database and a
connection:

>>> import cgi
>>> from pprint import pprint
>>> from zope.interface import implements
>>> from zope.app.tests import placelesssetup, ztapi
>>> placelesssetup.setUp()

>>> from ZODB.tests.util import DB
>>> db = DB()
>>> conn = db.open()
>>> root = conn.root()

Imagine that our application is an oracle: you can teach it to react to
phrases. Let's keep it simple and store the data in a dict:

>>> root['answers'] = {'Hello': 'Hi & how do you do?',
... 'Meaning of life?': '42',
... 'four < ?': 'four < five'}
>>> get_transaction().commit()


Initial setup
-------------

Here's some generations-specific code. We will create and register a
SchemaManager. SchemaManagers are responsible for the actual updates of the
database. This one will be just a dummy. The point here is to make the
generations module aware that our application supports generations.

The default implementation of SchemaManager is not suitable for this test
because it uses Python modules to manage generations. For now, it
will be just fine, since we don't want it to do anything just yet.

>>> from zope.app.generations.interfaces import ISchemaManager
>>> from zope.app.generations.generations import SchemaManager
>>> dummy_manager = SchemaManager(minimum_generation=0, generation=0)
>>> ztapi.provideUtility(ISchemaManager, dummy_manager, name='some.app')

'some.app' is a unique identifier. You should use a URI or the dotted name
of your package.

When you start Zope and a database is opened, an IDatabaseOpenedEvent is sent.
Zope registers evolveMinimumSubscriber by default as a handler for this event.
Let's simulate this:

>>> class DatabaseOpenedEventStub(object):
... def __init__(self, database):
... self.database = database
>>> event = DatabaseOpenedEventStub(db)

>>> from zope.app.generations.generations import evolveMinimumSubscriber
>>> evolveMinimumSubscriber(event)

The consequence of this action is that now the database contains the fact
that our current schema number is 0. When we update the schema, Zope3 will
have an idea of what the starting point was. Here, see?

>>> from zope.app.generations.generations import generations_key
>>> root[generations_key]['some.app']
0

In real life you should never have to bother with this key directly,
but you should be aware that it exists.


Upgrade scenario
----------------

Back to the story. Some time passes and one of our clients gets hacked because
we forgot to escape HTML special characters! The horror! We must fix this
problem ASAP without losing any data. We decide to use generations to impress
our peers.

Let's update the schema manager (drop the old one and install a new custom
one):

>>> ztapi.unprovideUtility(ISchemaManager, name='some.app')

>>> class MySchemaManager(object):
... implements(ISchemaManager)
...
... minimum_generation = 1
... generation = 2
...
... def evolve(self, context, generation):
... root = context.connection.root()
... answers = root['answers']
... if generation == 1:
... for question, answer in answers.items():
... answers[question] = cgi.escape(answer)
... elif generation == 2:
... for question, answer in answers.items():
... del answers[question]
... answers[cgi.escape(question)] = answer
... else:
... raise ValueError("Bummer")
... root['answers'] = answers # ping persistence
... get_transaction().commit()

>>> manager = MySchemaManager()
>>> ztapi.provideUtility(ISchemaManager, manager, name='some.app')

We have set `minimum_generation` to 1. That means that our application
will refuse to run with a database older than generation 1. The `generation`
attribute is set to 2, which means that the latest generation that this
SchemaManager knows about is 2.

evolve() is the workhorse here. Its job is to get the database from
`generation`-1 to `generation`. It gets a context which has the attribute
'connection', which is a connection to the ZODB. You can use that to change
objects like in this example.

In this particular implementation generation 1 escapes the answers (say,
critical, because they can be entered by anyone!), generation 2 escapes the
questions (say, less important, because these can be entered by authorized
personell only).

In fact, you don't really need a custom implementation of ISchemaManager. One
is available, we have used it for a dummy previously. It uses Python modules
for organization of evolver functions. See its docstring for more information.

In real life you will have much more complex object structures than the one
here. To make your life easier, there are two very useful functions available
in zope.app.generations.utility: findObjectsMatching() and
findObjectsProviding(). They will dig through containers recursively to help
you seek out old objects that you wish to update, by interface or by some other
criteria. They are easy to understand, check their docstrings.


Generations in action
---------------------

So, our furious client downloads our latest code and restarts Zope. The event
is automatically sent again:

>>> event = DatabaseOpenedEventStub(db)
>>> evolveMinimumSubscriber(event)

Shazam! The client is happy again!

>>> pprint(root['answers'])
{'Hello': 'Hi &amp; how do you do?',
'Meaning of life?': '42',
'four < ?': 'four &lt; five'}

Because evolveMinimumSubscriber is very lazy, it only updates the database just
enough so that your application can use it (to the `minimum_generation`, that
is). Indeed, the marker indicates that the database generation has been bumped
to 1:

>>> root[generations_key]['some.app']
1

We see that generations are working, so we decide to take the next step
and evolve to generation 2. Let's see how this can be done manually:

>>> from zope.app.generations.generations import evolve
>>> evolve(db)

>>> pprint(root['answers'])
{'Hello': 'Hi &amp; how do you do?',
'Meaning of life?': '42',
'four &lt; ?': 'four &lt; five'}
>>> root[generations_key]['some.app']
2

Default behaviour of `evolve` upgrades to the latest generation provided by
the SchemaManager. You can use the `how` argument to evolve() when you want
just to check if you need to update or if you want to be lazy like the
subscriber which we have called previously.


Let's clean up after ourselves:

>>> conn.close()
>>> db.close()
>>> placelesssetup.tearDown()
25 changes: 13 additions & 12 deletions browser/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def _getdb(self):
return self.request.publication.db

def evolve(self):
"""Perform a requested evolution
"""Perform a requested evolution
This method needs to use the component architecture, so
we'll set it up:
Expand Down Expand Up @@ -90,12 +90,12 @@ def evolve(self):
We'll also increase the generation of app1:
>>> app1.generation = 2
Now we can create our view:
>>> view = Managers(None, request)
Now, if we call it's `evolve` method, it should see that the
Now, if we call its `evolve` method, it should see that the
app1 evolve button was pressed and evolve app1 to the next
generation.
Expand All @@ -105,7 +105,7 @@ def evolve(self):
2
The demo evolver just writes the generation to a database key:
>>> from zope.app.generations.demo import key
>>> conn.root()[key]
(2,)
Expand Down Expand Up @@ -148,7 +148,7 @@ def evolve(self):
2
>>> conn.root()[key]
(2,)
We'd better clean upp:
>>> db.close()
Expand Down Expand Up @@ -234,12 +234,12 @@ def applications(self):
>>> app1.generation += 1
so we can evolve it.
Now we can create our view:
>>> view = Managers(None, request)
We call it's applications method to get data about
We call its applications method to get data about
application generations. We are required to call evolve
first:
Expand All @@ -250,19 +250,20 @@ def applications(self):
>>> for info in data:
... print info['id']
... print info['min'], info['max'], info['generation']
... print 'evolve?', info['evolve']
... print 'evolve?', info['evolve'] or None
foo.app1
0 2 1
evolve? evolve-app-foo.app1
foo.app2
0 0 0
evolve?
evolve? None
We'd better clean upp:
We'd better clean up:
>>> db.close()
>>> tearDown()
"""
"""
result = []

db = self._getdb()
Expand All @@ -275,7 +276,7 @@ def applications(self):
manager = managers.get(key)
if manager is None:
continue

result.append({
'id': key,
'min': manager.minimum_generation,
Expand Down
30 changes: 15 additions & 15 deletions generations.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class SchemaManager(object):
>>> context.connection.close()
>>> conn.close()
>>> db.close()
"""

zope.interface.implements(ISchemaManager)
Expand All @@ -94,8 +94,8 @@ def __init__(self, minimum_generation=0, generation=0, package_name=None):
minimum_generation)

if generation and not package_name:
raise ValueError("A package name must be supplied if the"
"generation is non-zero")
raise ValueError("A package name must be supplied if the"
" generation is non-zero")

self.minimum_generation = minimum_generation
self.generation = generation
Expand All @@ -117,8 +117,8 @@ def getInfo(self, generation):
"%s.evolve%d" % (self.package_name, generation),
{}, {}, ['*'])
return evolver.evolve.__doc__



class Context(object):
pass
Expand Down Expand Up @@ -146,15 +146,15 @@ def evolve(db, how=EVOLVE):
... zope.interface.implements(ISchemaManager)
...
... erron = None # Raise an error is asked to evolve to this
...
...
... def __init__(self, name, minimum_generation, generation):
... self.name, self.generation = name, generation
... self.minimum_generation = minimum_generation
...
...
... def evolve(self, context, generation):
... if generation == self.erron:
... raise ValueError(generation)
...
...
... context.connection.root()[self.name] = generation
We also need to set up the component system, since we'll be
Expand All @@ -171,9 +171,9 @@ def evolve(db, how=EVOLVE):
>>> app2 = FauxApp('app2', 5, 11)
>>> ztapi.provideUtility(ISchemaManager, app2, name='app2')
If we great a new database, and evolve it, we'll simply update
If we create a new database, and evolve it, we'll simply update
the generation data:
>>> from ZODB.tests.util import DB
>>> db = DB()
>>> conn = db.open()
Expand Down Expand Up @@ -202,13 +202,13 @@ def evolve(db, how=EVOLVE):
2
>>> root[generations_key]['app2']
11
And that the database was updated for that application:
>>> root.get('app1')
2
>>> root.get('app2')
If there is an error updating a particular generation, but the
generation is greater than the minimum generation, then we won't
get an error from evolve, but we will get a log message.
Expand Down Expand Up @@ -238,7 +238,7 @@ def evolve(db, how=EVOLVE):
Then we'll get an error if we try to evolve, since we can't get
past 3 and 3 is less than 5:
>>> evolve(db)
Traceback (most recent call last):
...
Expand Down Expand Up @@ -286,7 +286,6 @@ def evolve(db, how=EVOLVE):
Now, if we use EVOLVEMINIMUM instead, we'll evolve to the minimum
generation:
>>> evolve(db, EVOLVEMINIMUM)
>>> conn.sync()
>>> root[generations_key]['app1']
Expand All @@ -307,11 +306,12 @@ def evolve(db, how=EVOLVE):
GenerationTooHigh: (5, u'app1', 2)
We'd better clean up:
>>> handler.uninstall()
>>> conn.close()
>>> db.close()
>>> tearDown()
"""
conn = db.open()
try:
Expand Down
Loading

0 comments on commit 19a17ee

Please sign in to comment.