.. -*- coding: utf-8 -*-
.. % Programmation ZODB
.. % Installation de la ZODB
.. % Comment fonctionne la ZODB
.. % Règles d'écriture de classes persistantes
Programmation ZODB
==================
Installation de la ZODB
-----------------------
La ZODB est empaquetée à l'aide des outils standards 'distutils'.
Vous avez besoin au minimum d'une version de Python supérieure ou égale à 2.3.5.
Installez la ZODB avec la commande :command:`easy_install ZODB3` sous Unix
sinon installer les paquets pré-construits pour Windows.
En effet, sous Unix vous aurez besoin d'un compilateur C pour construire le
paquet car la ZODB possède de nombreux modules écrits en C pour des raisons de
performance.
Comment fonctionne la ZODB
--------------------------
La ZODB est conceptuellement simple. Les classes Python dérivent une classe de
base :class:`persistent.Persistent` pour être compatibles avec la ZODB. Les
instances des objets persistants sont construits, lorsque le programme
en a besoin, à partir du média de stockage,
qui peut être par exemple un fichier sur le disque, et sont mis en cache dans la mémoire.
La ZODB est prévenue de la
modification des objets, si bien que lorsque une instruction réalise une
modification comme ``obj.size = 1``, l'objet modifié est marqué 'dirty'. En une
requête tous les objets marqués 'dirty' sont écrits sur le stockage permanent,
cette opération s'appelle valider une transaction ("committing a transaction"
ou "commiter" en langue de développeur). Une transaction peut aussi être
abandonnée ou annulée, ce qui à pour conséquence d'annuler toutes les
modifications, les objets marqués 'dirty' sont remis à leur état initial
d'avant le début de transaction.
Le terme "transaction" a une signification spécifique en informatique.
Il est extrêmement important que le contenu d'une base de données ne soit pas
corrompue par les crashs logiciels ou matériels, et la plupart des logiciels
de base de données offrent cette protection en ayant quatre caractéristiques :
atomique, consistante, isolée, et durable (ACID) :
* atomique : les opérations sont indivisibles, en cas d'échec la suite des
opérations est complètement annulée quelles que soientt le nombre d'opérations
effectuées (rollback), inversement en cas de succès elles sont toutes
appliquées ; en cas de crash cela garantit que la base ne sera pas dans un
état partiellement modifié.
* cohérente : en fin de transaction, la base est de nouveau dans un état
cohérent - ce qui n'est pas forcément le cas pendant la transaction. Un contenu final
incohérent provoque l'annulation de la transaction.
* isolée : deux transactions simultanées n'ont pas connaissance de modifications
apportées à la base par l'autre tant qu'elle n'a pas été validée (commitée).
* durable : une transaction validée ne peut être annulée ou écrasée par une
transaction ayant démarré simultanément. Lorsque la seconde voudra écraser
les données de la première elle se verra annulée. Ce qui provoque l'émission
d'un message d'erreur prévenant l'auteur de la seconde transaction que sa
requête n'a pu aboutir en raison d'un conflit de transaction.
La ZODB fournit toutes les caractéristiques ACID.
Créer une ZODB
--------------
Il y a trois interfaces principales fournies par la ZODB : les classes
:class:`Storage`, :class:`DB`, et :class:`Connection`. Les interfaces
:class:`DB` et :class:`Connection` ont toutes deux une implémentation unique,
mais il y a plein de classes différentes qui implémentent l'interface
:class:`Storage`.
* Les classes de type :class:`Storage` sont les couches les plus basses,
manipulant, stockant et restituant les objets depuis les différents types de
stockage. Quelques types de stockage différents ont été écrits, telles les classes
:class:`FileStorage`, qui utilise un fichier de stockage classique, et
:class:`BDBFullStorage`, qui utilise le logiciel de la base de données
BerkeleyDB Sleepycat. Vous pouvez écrire votre propre classes Storage qui
stocke les objets dans une base de données relationnelle, par exemple, si
cela convient mieux à votre application. D'autres exemples de stockage,
:class:`DemoStorage`, :class:`MappingStorage` et :class:`RelStorage` (storage MySQL),
sont disponibles et peuvent
servir de modèle si vous voulez écrire un nouveau système de stockage.
* La classe :class:`DB` chapeaute le stockage, et réalise la médiation entre
les différentes connexions. Une seule instance de :class:`DB` est créée par
processus.
* Enfin, la classe :class:`Connection` réalise la mise en cache des objets, et
les déplace depuis ou vers la solution de stockage. Un programme muti-threadé
doit ouvrir une instance de :class:`Connection` pour chaque flux d'exécution.
Les différents flux d'exécution peuvent alors modifier les objets et valider
leur modifications indépendamment.
Projeter d'utiliser une ZODB demande trois étapes : vous avez à instancier la
classe :class:`Storage`, et obtenir une connexion sous forme d'une instance de
:class:`Connection` à partir de l'instance de :class:'DB'. Tout cela ne
représente que très peu de ligne de code :::
>>> from ZODB import FileStorage, DB
>>> storage = FileStorage.FileStorage('/tmp/test-filestorage.fs')
>>> db = DB(storage)
>>> conn = db.open()
Remarquez que vous pouvez utiliser un système de stockage complètement
différent simplement en changeant la ligne qui instancie la classe du type
:class:`Storage`, l'exemple précédent utilise :class:`FileStorage`. Dans la
section :ref:`zeo`, "Comment fonctionne la ZEO", vous verrez comment ZEO
utilise cette possibilité.
Utiliser un fichier de configuration pour la ZODB
-------------------------------------------------
La ZODB supporte également les fichiers de configuration écrits dans le format
ZConfig. Un fichier de configuration peut être utilisé pour séparer la
configuration de l'applicatif. Les classes de stockage et la classe :class:`DB`
supportent divers arguments. Toutes ces options peuvent être spécifiées par
le fichier de configuration.
Le format du fichier est simple, L'exemple du chapitre précédent peut être
réalisé comme suit :::
path /tmp/test-filestorage.fs
Le module :mod:`ZODB.config` inclut plusieurs fonctions pour ouvrir une base de
données et un stockage depuis un fichier de configuration. :python::
>>> import ZODB.config
>>> db = ZODB.config.databaseFromURL('/tmp/test.conf')
>>> conn = db.open()
La documentation sur ZConfig est inclue dans la livraison de ZODB3, elle
explique le format en détail. Chaque fichier de configuration est décrit par un
schéma, qui par convention est stocké dans un fichier :file:`component.xml`.
ZODB, ZEO, zLOG, et zdaemon ont tous un schéma.
Écriture d'une classe persistante
---------------------------------
Faire une classe persistante est assez simple; il suffit de dériver de la
classe :class:`Persistent`, comme montré dans l'exemple suivant :python::
>>> from persistent import Persistent
>>> class User(Persistent):
... pass
La classe :class:`Persistent` est une classe de type 'new-style' c'est à dire
qu'elle dérive de `object`. Elle est implémentée en C.
Pour des raisons de simplicité, dans l'exemple la classe :class:`User` sera
simplement utilisée comme un support à un ensemble d'attributs. Habituellement
la classe devrait définir plusieurs méthodes qui ajoutent des fonctionnalités,
mais cela n'a aucun impact sur le traitement qu'en fait la ZODB.
La ZODB utilise la persistance par accessibilité : à partir d'un ensemble
d'objets racines tous les attributs de ces objets sont rendus persistants, qu'il
s'agisse de type de données Python ou d'instances de classe. Il n'y a pas de
méthode explicite pour stocker les objets dans la base ZODB : ajoutez les
simplement comme attribut à un objet ou dans un dictionnaire qui soit déjà dans
la base. Cette chaîne de contenance doit finir par rejoindre l'objet racine de
la base de données.
Comme exemple, nous allons créer une base de données d'utilisateurs simple qui
permette de récupérer des instances de la classe :class:`User` pour un ID
d'utilisateur donné. Premièrement, nous récupérons l'objet à la racine primaire
de la ZODB en utilisant la méthode :meth:`root` de l'instance
:class:`Connection`. L'objet racine se comporte comme un dictionnaire, en
conséquence vous pouvez ajouter une nouvelle entrée clé/valeur pour la racine
de votre application. Nous allons insérer un objet :class:`OOBTree` qui va
contenir toute les objets :class:`User`. (Le module :class:`BTree` est
également inclus comme faisant partie des éléments de Zope.) :python::
>>> dbroot = conn.root()
>>> # Ensure that a 'userdb' key is present
...
>>> # in the root
...
>>> if not dbroot.has_key('userdb'):
... from BTrees.OOBTree import OOBTree
... dbroot['userdb'] = OOBTree()
...
>>> userdb = dbroot['userdb']
Insérer un nouvel utilisateur est simple : créez un objet de la classe
:class:`User`, remplissez le avec les données, insérer le dans l'instance du
:class:`BTree`, et validez (commitez) la transaction.
:python::
>>> newuser.id = 'amk'
>>> newuser.first_name = 'Andrew' ; newuser.last_name = 'Kuchling'
>>>
>>> # Add object to the BTree, keyed on the ID
...
>>> userdb[newuser.id] = newuser
>>>
>>> # Commit the change
...
>>> transaction.commit()
Le module :mod:`transaction` définit quelque fonction de haut niveau pour
travailler avec les transactions. La fonction :func:`commit` écrit tous les
objets modifiés sur le disque, ce qui rend les modifications permanentes. La
fonction :func:`abort` annule toutes les modifications qui ont été réalisées
depuis le dernier appel à :func:`commit`, restaurant l'état initial des objets.
Si vous êtes familier avec la sémantique des bases de données relationnelles,
vous n'êtes pas dépaysé. La fonction :func:`get` retourne une instance de la
classe :class:`Transaction` qui ont des méthodes additionnelles comme la
fonction :meth:`note` qui ajoute une note au métadata de la transaction.
Plus précisément, le module :mod:`transaction` expose une instance de la classe
de gestion des transactions :class:`ThreadTransactionManager` comme
``transaction.manager``, et les fonctions du module :mod:`transaction` comme
:func:`get` et :func:`begin` qui redirige vers des méthodes du même nom du
``transaction.manager``. La fonction :func:`commit` et :func:`abort` appliquent
les méthodes de même nom de l'instance de la classe :class:`Transaction`
retourné par ``transaction.manager.get()``. Tout ceci pour des raisons de
commodité. Il est également possible de créer votre propre gestionnaire de
transaction, et de dire à ``DB.open()`` de l'utiliser à la place.
Par ce que l'intégration avec Python est complète, c'est presque comme avoir
une sémantique transactionnelle pour les variables de vos programme, vous
pouvez expérimenter les transactions dans un interpréteur Python : :python::
>>> newuser
>>> newuser.first_name # Print initial value
'Andrew'
>>> newuser.first_name = 'Bob' # Change first name
>>> newuser.first_name # Verify the change
'Bob'
>>> transaction.abort() # Abort transaction
>>> newuser.first_name # The value has changed back
'Andrew'
Règles d'écriture de classes persistantes
-----------------------------------------
Pratiquement tous les langages persistants imposent des restrictions sur le
style des programmes, avertissant des constructions qu'ils ne peuvent gérer ou
y ajoutent de subtiles modifications sémantiques, et la ZODB ne fait pas
exception. Heureusement, les restrictions de la ZODB sont assez simples à
comprendre, et dans la pratique il n'est pas douloureux de les contourner.
Le résumé des règles est le suivant :
* Si vous modifiez un objet mutable qui est la valeur d'un attribut d'un autre
objet la ZODB ne peut le savoir, et ne marquera pas l'objet comme 'dirty'.
La solution consiste soit à positionner le drapeau 'dirty' vous même quand
vous modifiez l'objet, soit à utiliser un 'wrapper' (un objet enveloppe qui
fournit les services manquants) pour les listes et les dictionnaires Python
(:class:`PersistentList`, :class:`PersistentMapping`) qui positionne le
drapeau 'dirty' proprement.
* Les versions récentes de la ZODB autorisent l'écriture de classe qui ont des
méthodes :meth:`__setattr__`, :meth:`__getattr__`, ou :meth:`__delattr__`. Ce
que ne permettaient pas du tout les anciennes versions. Si vous écrivez des
méthodes :meth:`__setattr__` ou :meth:`__delattr__`, leur code doit
positionner le drapeau 'dirty' manuellement.
* Une classe persistante ne doit pas avoir de méthode :meth:`__del__`. La base
de données doit pouvoir déplacer librement les objets entre le système de
stockage et la mémoire. Si un objet n'est pas utilisé depuis un moment, il
peut être relâché et son contenu chargé depuis le système de stockage à la
prochaine utilisation. Parce que l'interpréteur Python n'est pas conscient
des mécanismes de persistance, il pourrait appeler la méthode :meth:`__del__`
chaque fois que l'objet a été libéré.
Nous allons regarder chaque règle en détail.
Modification des objets modifiables
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
La ZODB utilise différents hameçons Python pour attraper les accès aux
attributs, et peut détourner la majorité des façons de modifier un objet, mais
pas tous. Si vous modifiez un objet de la classe :class:`User` par affectation
d'un de ses attributs, comme dans ``userobj.first_name = 'Andrew'``, la ZODB va
marquer l'objet comme ayant changé, et il sera écrit dans le système de
stockage lors du prochain :meth:`commit` (validation).
Le cas le plus typique qui n'est pas pris en charge par la ZODB est la liste ou
le dictionnaire. Si les objets de type :class:`User` ont un attribut nommé
``friends`` contenant une liste, appelant ``userobj.friends.append(otherUser)``
qui ne marque pas ``userobj`` comme étant modifié. Du point de vue de la ZODB,
``userobj.friends`` n'a été que lu, et sa valeur, ce qui arrive à une liste
Python ordinaire, a été retournée. La ZODB n'est pas consciente que l'objet
retourné a été modifié après.
C'est l'une des quelque bizarreries dont vous devez vous rappeler quand vous
utilisez la ZODB : si vous modifiez un objet modifiable attribut d'un objet en
place, vous devez marquer manuellement l'objet qui a été modifié pour que son
drapeau 'dirty' soit à vrai. Ceci est fait en positionnant l'attribut
:attr:`_p_changed` de l'objet à vrai :python::
>>> userobj.friends.append(otherUser)
>>> userobj._p_changed = True
Vous pouvez cacher le détails d'implémentation du marquage de l'objet
comme 'dirty' en concevant l'API de vos classes pour qu'elles
n'utilisent pas directement l'accès aux attributs. En lieu et place, vous
pouvez utiliser l'approche Java des accesseurs pour tout, et positionner le
drapeau de modification à l'intérieur des méthodes. Par exemple, vous pouvez
interdire l'accès direct à l'attribut ``friends``, et ajouter une méthode
:meth:`get_friend_list` et une méthode :meth:`add_friend` de modification.
La méthode :meth:`add_friend` devrait ressembler à :python::
>>> def add_friend(self, friend):
>>> self.friends.append(otherUser)
>>> self._p_changed = True
Vous pouvez aussi utiliser le mécanisme des 'properties' pour cacher les
accesseurs à l'usage (@property).
Vous pouvez également utiliser une liste ou un dictionnaire compatible avec la
ZODB qui gère pour vous le drapeau de modification. La ZODB est fournie avec la
classe :class:`PersistentMapping` et :class:`PersistentList`
Vous pouvez rendre silencieuses les modifications d'un objet en changeant la
valeur du drapeau de modification (_p_changed ) à False.
:meth:`__getattr__`, :meth:`__delattr__`, and :meth:`__setattr__`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
La ZODB autorise la persistance des classes qui ont des méthodes crochets comme
:meth:`__getattr__` et :meth:`__setattr__`. Il y a quatre méthodes spéciales
qui contrôlent l'accès aux attributs : les règles de chacune diffèrent.
La méthode :meth:`__getattr__` fonctionne presque de la même façon pour les
classes persistantes que pour les autres classes. Pas besoin de manipuler quoi
que ce soit. Si un objet est rendu silencieux, il devra être manipulé avant
l'appel à :meth:`__getattr__`.
Les autres méthodes sont plus délicates. Elles vont surcharger les crochets
fournis par la :class:`Persistent`, si bien que l'utilisateur doit appeler des
méthodes spéciales pour invoquer ces crochets.
La méthode :meth:`__getattribute__` sera appelée pour les accès aux attributs :
Elle surcharge l'accès au code fourni lors de la dérivation de la classe
:class:`Persistent`. Une méthode :meth:`__getattribute__` surchargée par
l'utilisateur doit toujours faire en sorte que la classe de base
:class:`Persistent` ait une chance de manipuler les attributs spéciaux comme
:attr:`__dict__` ou :attr:`__class__`. La surcharge doit appeler la méthode
:meth:`_p_getattr`, et doit lui passer comme seul argument le nom de
l'attribut. Si elle retourne True, le code de la fonction surchargée par
l'utilisateur doit appeler la méthode :meth:`__getattribute__` de la classe
:class:`Persistent` pour obtenir la valeur. Sinon le code peut continuer sont
exécution.
Un crochet de la méthode :meth:`__setattr__` va également surcharger la méthode
:meth:`__setattr__` de la classe :class:`Persistent` et l'utilisateur doit la
traiter un peu comme la précédente. Le code réalisé par l'utilisateur doit
appeler la méthode :meth:`_p_setattr` de la classe :class:`Persistent` en lui
passant le nom et la valeur de l'attribut. Si la méthode retourne True, la
classe :class:`Persistent` gère l'attribut, sinon le code peut continuer sont
exécution. Si le code de l'utilisateur modifie l'état de l'objet, le code doit
positionner l'attribut :attr:`_p_changed`.
Le crochet de la méthode meth:`__delattr__` doit être implémenté de la même
façon. Le code de l'utilisateur doit appeler :meth:`_p_delattr`, en passant le
nom de l'attribut comme argument. Si l'appel renvoit True alors la classe
:class:`Persistent` gère l'attribut sinon c'est au code de l'utilisateur de le
faire.
Méthode :meth:`__del__`
^^^^^^^^^^^^^^^^^^^^^^^
La méthode :meth:`__del__` est invoquée juste avant que la mémoire occupée par
un objet Python non référencé soit libérée. Parce que la ZODB peut
matérialiser ou dématérialiser un objet persistant en mémoire un nombre
quelconque de fois, il y a une relation très forte entre la persistance d'un
objet et la méthode :meth:`__del__` qui est normalement invoquée durant le
cycle de vie de l'objet. Par exemple, la méthode :meth:`__del__` d'un objet
persistant n'est pas invoqué uniquement dans le cas d'un objet qui n'est plus
référencé par d'autres objets de la base de donnée car la méthode
:meth:`__del__` est aussi mise en jeu dans le cas de l'accessibilité des objets
en mémoire.
Pire, une méthode :meth:`__del__` peut interférer avec l'objectif de la
machinerie de persistance. Par exemple, de nombreux objets restent dans le cache
d'une class:`Connection`. À plusieurs reprises, pour réduire la charge du
cache, les objets qui n'ont pas été référencés récemment sont enlevés du cache.
Si un objet persistant est enlevé du cache et que le cache contenait la
dernière référence en mémoire de cet objet la méthode :meth:`__del__` de
l'objet sera appelée. Si la méthode :meth:`__del__` référence n'importe quel
attribut de l'objet, la ZODB devra recharger l'objet à partir de la base de
données à nouveau, avant de pouvoir satisfaire la référence. Ce qui a pour
conséquence de remettre l'objet dans le cache. Un tel objet est virtuellement
immortel, occupant de l'espace en mémoire pour toujours, puisque chaque essai
pour l'enlever du cache abouti à l'y remettre. Avec les ZODB antérieures à la
version 3.2.2 cela causait le bouclage infini du code de réduction de la taille
du cache. Cette boucle infinie ne se produit plus mais les objets continuent à
vivre en cache pour toujours.
Parce que la méthode :meth:`__del__` n'a pas beaucoup de sens dans le cas
d'objets persistants et peut créer des problèmes, les méthodes persistantes ne
devraient pas surcharger la méthode :meth:`__del__`.
Écrire des classes persistantes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Maintenant que nous connaissons les bases de la programmation de la ZODB, nous
allons regarder quelques tâches plus délicates qui sont nécessaires à tous les
utilisateurs de la ZODB dans un système en production.
Modifier les attributs d'une instance
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Idéalement, avant de rendre des classes persistantes, vous voulez définir leur
interface correctement du premier coup, en conséquence de quoi il n'y aurait
pas besoin d'ajouter de nouveaux attributs au cours du temps. C'est un objectif
difficile et pratiquement impossible à atteindre à moins de connaître
exactement vos besoins futurs. De telles demandes peuvent être réclamées par
d'autres personnes, si bien que vous devez vous préparer à recevoir des
demandes impliquant des changement structurels. En terminologie de base de
données orientées objets, cela s'appelle une mise à jour du schéma. La ZODB n'a
pas besoin de spécification du schéma, si vous changez ce que le logiciel
attend comme données de la base pour un objet, vous changez implicitement le
schéma.
Une façon de gérer de tels changements est de réaliser des programmes qui vont
chercher tous les objets de la base pour les mettre à jour selon le nouveau
schéma. C'est facile si votre réseau d'objet est bien structuré, par exemple si
toutes les instances de la classe :class:`User` se trouvent dans un unique
dictionnaire ou BTree, il suffit alors de boucler sur chaque instance
:class:`User`. C'est plus difficile si le graphe est moins structuré. Si vos
objets :class:`User` ne peuvent être trouvés comme attributs d'un faible nombre
d'objets, il vous faudra écrire un traverseur qui parcourera la base et qui
vérifiera que chaque objet de la ZODB est du type :class:`User` ou non.
Certaines OODBs supportent une fonctionnalité appelée "extends", qui peut
rapidement trouver les objets d'un type données, quelque soit le graphe des
objets, malheureusement ce n'est pas le cas de la ZODB.