Skip to Content

Python : les bases de données géospatiales - 2) mapping objet-relationnel (ORM, SQLAlchemy, SQLObject, GeoAlchemy, Django-GeoDjango, TurboGears ou MapFish)


python

Le billet précédent 'Python les bases de données géospatiales, traitement classique, principes et problèmes" a introduit le terme "mapping objet-relationnel".

Selon Wikipedia, la définition est  :

"Un mapping objet-relationnel (en anglais object-relational mapping ou ORM) est une technique de programmation informatique qui crée l'illusion d'une base de données orientée objet à partir d'une base de données relationnelle en définissant des correspondances entre cette base de données et les objets du langage utilisé. On pourrait le désigner par « correspondance entre monde objet et monde relationnel".

Ceci veut dire, plus simplement ici, que toutes les tables SQL seront transformées en classes Python et que tous les traitements (query, jointures, héritages, etc.) ne concerneront plus que des relations entre objets Python. Les deux techniques possibles sont expliquées dans la figure suivante (liaisons tables SQL - classes Python créés par un "mapper" ou directes).

 

Parmi les modules qui utilisent cette technique les plus connus sont SQLAlchemy ou SQLObject mais aussi des frameworks web comme Django, Turbogears, web2py ou Pylons.  Leur principe est simple pour un programmeur, une fois connecté à une base de données, il ne s'occupe plus que de Python. 

Nous n'allons pas essayer ici d'expliquer toutes leurs spécificités ni d'en faire un tutoriel, car il y en a un très grand nombre sur Internet, très bien faits et  même des livres qui leur sont consacrés (personnellement, j'utilise le livre "Essential SQLAlchemy" de Rick Copeland chez O'Reilly, plus de 200 pages...). Nous nous contenterons ici de les "effleurer" dans le cadre des données spatiales pour montrer leurs avantages et leurs problèmes et nous nous contenterons des modules (soit les principes des frameworks web sont équivalents, soit ils utilisent SQLAlchemy).

Malgré sa relative complexité, SQLAlchemy sera choisi pour en développer les principes de base, car il permet de les exposer de la manière la plus claire. Les modules d'accès aux bases de données sont évidemment toujours nécessaires, mais leurs disparités sont uniformisées par le processus nommé "engine" (moteur ?)  et une fois connecté, plus de traces apparentes de SQL.

 Le mapper ou lien est fait entre la table "cartes" par l'entremise de "macarte" (création) ou "matable" (récupération) et la classe Carte. A partir de ce moment, tous les traitements (créations de tables, requêtes, mises à jour, suppressions, jointures, etc.) se feront en Python à partir de la classe Carte

a = Carte()

SQLAlchemy est très puissant et ses fondements, ébauchés ici, sont valables pour tous les autres modules. Dans un premier temps, un autre module, SQLObject, sera utilisé pour les exemples, car il est plus simple à appréhender.

Application (avec le module SQLObject)

L'opération sera effectuée avec la base Postgis abordée dans l'article précédent:

requête SELECT avec SQLObject:

  1. from sqlobject import *
  2.  
  3. #connexion
  4. db='postgres://localhost:5432/testpostgis'
  5. connection = connectionForURI(db)
  6.  
  7. #puisque la table existe déjà dans la base,on récupère directement
  8. #ses caractéristiques dans la classe, puis les types des champs peuvent être redéfinis
  9.  
  10. class testpoly(SQLObject):
  11. _connection = connection
  12. _fromDatabase = True
  13. nom = StringCol()
  14. the_geom = StringCol()# type geometry dans la base physique testpoly
  15.  
  16. poly = testpoly.get(1)
  17. print poly.nom
  18. 'test1'
  19. print poly.the_geom
  20. '01030000000100000007000000B64939E22648B73F210C2F0B83D7E93F0399366B2021C23F5B07A42CAEA5E93FA3A3B4CC6E14D73F3164C5F9C377E63FAB25B136C9D8CE3F179603F45D89E13FD2DA533F623EB63FB4B2452B5BB4E23FA1C950507AE0AB3FA12E64C5F9C3E73FB64939E22648B73F210C2F0B83D7E93F'

Cela à l'air magique la première fois que l'on utilise un de ces modules, mais pour réellement comprendre ce qu'ils font, il suffit d'ajouter une ligne (de débogage):

testpoly._connection.debug = True

détail du processus

  1. testpoly._connection.debug = True
  2. a = testpoly.get(1)
  3. 1/QueryOne: SELECT nom, the_geom FROM testpoly WHERE ((testpoly.id) = (1))
  4. 1/QueryR : SELECT nom, the_geom FROM testpoly WHERE ((testpoly.id) = (1))
  5. 1/COMMIT : auto

Et tout s'éclaire, car "ils ne font que" masquer les requêtes SQL, mais, somme toute, c'est le principe du mapping relationel objet. La procédure est néanmoins totalement différente. Il est tout à fait possible de travailler virtuellement en Python seul (base de données en mémoire) d'effectuer tous les traitements (création de tables, insertion de valeurs, requêtes, etc.), et de n'envoyer les résultats en SQL (sous forme de curseur) à la base de données physique que selon notre bon vouloir. Le rôle du curseur est donc fortement minimisé.

Le résultat obtenu ici est la géométrie brute au format WKB (Well Known Binary, de l' OGC). Comment transformer cette valeur en format compréhensible par Python ? Dans la démarche traditionnelle, la requête utilisée faisait appel à des fonctions propres à Postgis, AsText(the_geom) et Srid(the_geom).

Ces modules offrent toujours une possibilité d'effectuer des requêtes brutes en SQL

requête brute avec SQLObject

  1. b = testpoly._connection.queryOne("""SELECT nom, AsText(the_geom), Srid(the_geom) FROM testpoly""")
  2.  
  3. print b
  4. ('test1', 'POLYGON((0.09094470046083 0.807557603686636,0.141635944700461 0.80147465437788,0.360622119815668 0.702119815668203,0.240990783410138 0.548018433179723,0.086889400921659 0.584516129032258,0.054447004608295 0.742672811059908,0.09094470046083 0.807557603686636))', 4326)

Mais on retombe évidemment dans les travers de la première démarche.

Il serait difficile de trouver un artifice en Python pour "masquer" cette requête SQL propre à Postgis. La solution réside dans la possibilité offerte par une classe de modifier les résultats obtenus. Ainsi, le module Shapely, déjà vu, permet de transformer le format WKB en WKT:

 

décodage du format WKB à l'aide du module Shapely

  1. from shapely.wkb import loads
  2. class testpoly(SQLObject):
  3. _connection = connection
  4. _fromDatabase = True
  5. nom = StringCol()
  6. the_geom = StringCol()
  7. # modification du résulat à l'aide du module shapely qui sait décoder le format wkb
  8. def _get_the_geom(self):
  9. valeur=self._SO_get_the_geom()
  10. res = loads(valeur.decode('hex')).wkt
  11. return res
  12. a = testpoly.get(1)
  13. print a.the_geom
  14. 'POLYGON ((0.0909447004608300 0.8075576036866360, 0.1416359447004610 0.8014746543778800, 0.3606221198156680 0.7021198156682030, 0.2409907834101380 0.5480184331797230, 0.0868894009216590 0.5845161290322580, 0.0544470046082950 0.7426728110599080, 0.0909447004608300 0.8075576036866360))'

La structure objet de ces modules permet donc d'ajouter facilement de nouvelles classes Python pour traiter ces problèmes. Les premières ont été proposées par info.bycycle.org/2007/01/29/using-postgis-with-sqlalchemy/ et sgillies.net/blog/531/the-shapely-alchemist/ sur base de modules externes, mais ceux-ci ne disposent pas de toutes les possibilités de Postgis. D'autres se sont donc mis à traiter directement les fonctions de Postgis, sans passer par un module externe. Parmi ceux-ci, SQLAlchemy lui-même (svn.sqlalchemy.org/sqlalchemy/trunk/examples/postgis/postgis.py) ou  une "surcouche" nommée GeoAlchemy, entièrement dédiée aux bases de données géospatiales (le module ajoute une classe supplémentaire nommée geoalchemy). Le module n'en est qu'à sa première version, mais il est déjà prometteur (avec encore quelques bugs...)

requête avec SQLAlchemy - GeoAlchemy

  1. from sqlalchemy import *
  2. from sqlalchemy.orm import *
  3. from sqlalchemy.ext.declarative import declarative_base
  4. from geoalchemy import *
  5.  
  6. #création de l'"engine"
  7. engine = create_engine('postgres://moi@localhost/testpostgis') # l'instruction ",echo=True"
  8. #placée ici, aurait le même effet que la commande de débogage de SQLObject
  9.  
  10. # ouverture d'une session de travail
  11. session = sessionmaker(bind=engine)()
  12.  
  13. # métadonnées
  14. metadata = MetaData(engine)
  15. Base = declarative_base(metadata=metadata)
  16.  
  17.  
  18. #récupération de la table testpoly dans la classe Testpoly
  19. class Testpoly(Base):
  20. __tablename__= 'testpoly'
  21. __table_args = {'autoload': True}
  22. the_geom = GeometryColumn(Polygon(2))
  23.  
  24. #requête
  25. s = session.query(Testpoly).get(1)
  26. #ou
  27. s = session.query(Testpoly).first()
  28. #géométrie
  29. print session.scalar(s.geom.geometry_type)
  30. 'ST_Polygon'
  31. # géométrie en texte wkt
  32. print session.scalar(s.geom.wkt)
  33. 'POLYGON((0.09094470046083 0.807557603686636,0.141635944700461 0.80147465437788,0.360622119815668 0.702119815668203,0.240990783410138 0.548018433179723,0.086889400921659 0.584516129032258,0.054447004608295 0.742672811059908,0.09094470046083 0.807557603686636))'
  34.  
  35. #géométrie en gml
  36. print session.scalar(s.geom.gml)
  37. '<gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>0.09094470046083,0.807557603686636 0.141635944700461,0.80147465437788 0.360622119815668,0.702119815668203 0.240990783410138,0.548018433179723 0.086889400921659,0.584516129032258 0.054447004608295,0.742672811059908 0.09094470046083,0.807557603686636</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon>'
  38.  
  39. #géométrie en kml
  40. print session.scalar(s.geom.kml)
  41. '<Polygon><outerBoundaryIs><LinearRing><coordinates>0.09094470046083,0.807557603686636 0.141635944700461,0.80147465437788 0.360622119815668,0.702119815668203 0.240990783410138,0.548018433179723 0.086889400921659,0.584516129032258 0.054447004608295,0.742672811059908 0.09094470046083,0.807557603686636</coordinates></LinearRing></outerBoundaryIs></Polygon>'

GeoAlchemy permet aussi directement d'utiliser des fonctions similaires à celles présentes dans Postgis comme:

possibilités supplémentaires

  1. #Création de géométries:
  2. wkt = "POLYGON((....))"
  3. essai = Testpoly(nom="test3", geom=WKTSpatialElement(wkt))
  4. wkt2 = "POLYGON((....))"
  5. essai2 = Testpoly(nom="test4", geom=WKTSpatialElement(wkt))
  6. session.add_all([essai1,essai2])
  7. session.commit()
  8. # et les analyses spatiales:
  9. session.query(Testpoly).filter(Testpoly.geom.touches(uneautregéométrie)).count()
  10. # + buffer, intersection, convex hull..;

Lors du dernier PyCon 2010 (conférence des utilisateurs Python), Sanjiv Singh, l'auteur de Geoalchemy a montré comment l'intégrer à TurboGears pour en faire un framework web spatial (us.pycon.org/media/2010/talkdata/PyCon2010/016/tg2-geospatial-pycon2010.pdf). Il offre une demonstration de ce framework avec des manipulations Geoalchemy à geo.turbogears.org/geoalchemydemo

D'autres solutions de webmapping geospatial sont aussi basées sur ces principes comme GeoDjango, maintenant intégré à Django ou MapFish (tutoriels dans les liens)

Conclusions

Dans le domaine géospatial, les solutions de mapping objet-relationnel prennent de plus en plus d'importance et il serait dommage de passer à côté lorsque l'on développe en Python, que soit pour des scripts ou pour du webmapping. Dans leur souci de neutralité vis à vis-à-vis du SQL et des bases de données (un même script peut s'appliquer à tous les cas, en changeant une seule ligne, l'engine), il est cependant nécessaire de suivre les évolutions (mises à jour et classes supplémentaires en fonction des évolutions d'une base de données). 

Mais une fois bien maitrisés, cela devient un réel plaisir de ne plus devoir s'arracher les cheveux avec du SQL et de se consacrer uniquement à Python.

Voilà pourquoi michalisavraam.org/2010/02/7-wishes-for-the-new-geoprocessor/, entre autres, souhaiterait un même traitement pour le module Argisscripting d'ESRI.

Tous les traitements ont été effectués sur Mac OS X avec Python 2.5.4 et Inkscape pour les figures.


Site officiel : SQLAlchemy
Site officiel : SQLobject
Site officiel : Shapely
Site officiel : GeoAlchemy
Site officiel : Django
Site officiel : GeoDjango
Site officiel : TurboGears
Site officiel : web2py
Site officiel : Pylons
Site officiel : MapFish
Autres Liens : Python: les bases de données géospatiales, traitement classique, principes et problèmes
Autres Liens : Mapping objet-relationnel
Autres Liens : Pycon
Autres Liens : des tutoriels Django/Geodjango
Autres Liens : un tutoriel MapFish
Autres Liens : MapFish et TurboGears

Commentaires

Salut, j'arrive un peu tard

Salut, j'arrive un peu tard mais dire qu'on aura plus à s'arracher les cheveux me laisse un peu perplexe !

Disons que je suis pour les ORM mais à condition de savoir manier les bases de données et le langage SQL avant de passer à la magie de l'ORM :)

Merci de l'intérêt, mais tout

Merci beaucoup pour l'intérêt.

En pratique,tout dépend des versions de SQLAlchemy - GeoAlchemy.
Je me suis basé sur sur la réponse de Sanjiv Singh, créateur de GeoAlchemy, dans:
http://groups.google.com/group/geoalchemy/browse_thread/thread/45a59604f...

et le script exemple qu'il donne dans
http://bitbucket.org/sanjiv/geoalchemy/src/tip/examples/reflection.py

L'ensemble fonctionnant très bien chez moi, sans remplacement

petit oubli dans le code "requête avec SQLAlchemy - GeoAlchemy"

toutefois selon le script
http://bitbucket.org/sanjiv/geoalchemy/src/tip/examples/reflection.py

il manque dans ton script

# Setup the metadata and declarative extension
metadata = MetaData(engine)
Base = declarative_base(metadata=metadata)

je travaille sous Linux avec une version de GeoAlchemy 0.4.1
ceci explique peut-être cela.

de fait, tu as raison, je

de fait, tu as raison, je viens de le voir sur mon script chez moi, je l'avais oublié. La correction est effectuée. Merci encore

ps: il faut bien choisir son couple SQLAlchemy - geoalchemy au niveau des versions, sinon il y aura des problèmes

petit oubli dans le code "requête avec SQLAlchemy - GeoAlchemy"

Très bien ton tuto mais il manque quelques lignes dans
"requête avec SQLAlchemy - GeoAlchemy"

remplacer
# ouverture d'une session de travail
session = sessionmaker(bind=engine)()

par

metadata = Metadata(engine)
Base = declarative_base(metadata = metadata)
# ouverture d'une session de travail
Session = sessionmaker(bind=engine)()
session = Session()

Poster un nouveau commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.