2004-10-19 07:17:12 +04:00
|
|
|
"""
|
|
|
|
This example/recipe has been contributed by Valentino Volonghi (dialtone)
|
|
|
|
|
|
|
|
Mapping arbitrary objects to a PostgreSQL database with psycopg2
|
|
|
|
|
|
|
|
- Problem
|
|
|
|
|
|
|
|
You need to store arbitrary objects in a PostgreSQL database without being
|
|
|
|
intrusive for your classes (don't want inheritance from an 'Item' or
|
|
|
|
'Persistent' object).
|
|
|
|
|
|
|
|
- Solution
|
|
|
|
"""
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
2005-06-13 10:25:06 +04:00
|
|
|
import psycopg2
|
|
|
|
from psycopg2.extensions import adapt, register_adapter
|
2004-10-19 07:17:12 +04:00
|
|
|
|
2005-07-17 08:26:27 +04:00
|
|
|
try:
|
|
|
|
sorted()
|
|
|
|
except:
|
2004-10-19 07:17:12 +04:00
|
|
|
def sorted(seq):
|
|
|
|
seq.sort()
|
|
|
|
return seq
|
|
|
|
|
|
|
|
# Here is the adapter for every object that we may ever need to
|
|
|
|
# insert in the database. It receives the original object and does
|
|
|
|
# its job on that instance
|
|
|
|
|
|
|
|
class ObjectMapper(object):
|
2005-03-02 17:51:24 +03:00
|
|
|
def __init__(self, orig, curs=None):
|
2004-10-19 07:17:12 +04:00
|
|
|
self.orig = orig
|
|
|
|
self.tmp = {}
|
|
|
|
self.items, self.fields = self._gatherState()
|
|
|
|
|
|
|
|
def _gatherState(self):
|
|
|
|
adaptee_name = self.orig.__class__.__name__
|
|
|
|
fields = sorted([(field, getattr(self.orig, field))
|
|
|
|
for field in persistent_fields[adaptee_name]])
|
|
|
|
items = []
|
|
|
|
for item, value in fields:
|
|
|
|
items.append(item)
|
|
|
|
return items, fields
|
|
|
|
|
|
|
|
def getTableName(self):
|
|
|
|
return self.orig.__class__.__name__
|
|
|
|
|
|
|
|
def getMappedValues(self):
|
|
|
|
tmp = []
|
|
|
|
for i in self.items:
|
|
|
|
tmp.append("%%(%s)s"%i)
|
|
|
|
return ", ".join(tmp)
|
|
|
|
|
|
|
|
def getValuesDict(self):
|
|
|
|
return dict(self.fields)
|
|
|
|
|
|
|
|
def getFields(self):
|
|
|
|
return self.items
|
|
|
|
|
|
|
|
def generateInsert(self):
|
|
|
|
qry = "INSERT INTO"
|
|
|
|
qry += " " + self.getTableName() + " ("
|
|
|
|
qry += ", ".join(self.getFields()) + ") VALUES ("
|
|
|
|
qry += self.getMappedValues() + ")"
|
|
|
|
return qry, self.getValuesDict()
|
|
|
|
|
|
|
|
# Here are the objects
|
|
|
|
class Album(object):
|
|
|
|
id = 0
|
|
|
|
def __init__(self):
|
|
|
|
self.creation_time = datetime.now()
|
|
|
|
self.album_id = self.id
|
|
|
|
Album.id = Album.id + 1
|
|
|
|
self.binary_data = buffer('12312312312121')
|
|
|
|
|
|
|
|
class Order(object):
|
|
|
|
id = 0
|
|
|
|
def __init__(self):
|
|
|
|
self.items = ['rice','chocolate']
|
|
|
|
self.price = 34
|
|
|
|
self.order_id = self.id
|
|
|
|
Order.id = Order.id + 1
|
2005-03-02 17:51:24 +03:00
|
|
|
|
|
|
|
register_adapter(Album, ObjectMapper)
|
|
|
|
register_adapter(Order, ObjectMapper)
|
2004-10-19 07:17:12 +04:00
|
|
|
|
|
|
|
# Describe what is needed to save on each object
|
|
|
|
# This is actually just configuration, you can use xml with a parser if you
|
|
|
|
# like to have plenty of wasted CPU cycles ;P.
|
|
|
|
|
|
|
|
persistent_fields = {'Album': ['album_id', 'creation_time', 'binary_data'],
|
|
|
|
'Order': ['order_id', 'items', 'price']
|
|
|
|
}
|
|
|
|
|
|
|
|
print adapt(Album()).generateInsert()
|
|
|
|
print adapt(Album()).generateInsert()
|
|
|
|
print adapt(Album()).generateInsert()
|
|
|
|
print adapt(Order()).generateInsert()
|
|
|
|
print adapt(Order()).generateInsert()
|
|
|
|
print adapt(Order()).generateInsert()
|
|
|
|
|
|
|
|
"""
|
|
|
|
- Discussion
|
|
|
|
|
|
|
|
Psycopg 2 has a great new feature: adaptation. The big thing about
|
|
|
|
adaptation is that it enable the programmer to glue most of the
|
|
|
|
code out there without many difficulties.
|
|
|
|
|
|
|
|
This recipe tries to focus the attention on a way to generate SQL queries to
|
|
|
|
insert completely new objects inside a database. As you can see objects do
|
|
|
|
not know anything about the code that is handling them. We specify all the
|
|
|
|
fields that we need for each object through the persistent_fields dict.
|
|
|
|
|
2005-11-26 10:47:48 +03:00
|
|
|
The most important lines of this recipe are:
|
|
|
|
register_adapter(Album, ObjectMapper)
|
|
|
|
register_adapter(Order, ObjectMapper)
|
2004-10-19 07:17:12 +04:00
|
|
|
|
2005-11-26 10:47:48 +03:00
|
|
|
In these line we notify the system that when we call adapt with an Album instance
|
2004-10-19 07:17:12 +04:00
|
|
|
as an argument we want it to istantiate ObjectMapper passing the Album instance
|
|
|
|
as argument (self.orig in the ObjectMapper class).
|
|
|
|
|
|
|
|
The output is something like this (for each call to generateInsert):
|
|
|
|
|
|
|
|
('INSERT INTO Album (album_id, binary_data, creation_time) VALUES
|
|
|
|
(%(album_id)s, %(binary_data)s, %(creation_time)s)',
|
|
|
|
|
|
|
|
{'binary_data': <read-only buffer for 0x402de070, ...>,
|
|
|
|
'creation_time': datetime.datetime(2004, 9, 10, 20, 48, 29, 633728),
|
|
|
|
'album_id': 1}
|
|
|
|
)
|
|
|
|
|
|
|
|
This is a tuple of {SQL_QUERY, FILLING_DICT}, and all the quoting/converting
|
|
|
|
stuff (from python's datetime to postgres s and from python's buffer to
|
|
|
|
postgres' blob) is handled with the same adaptation process hunder the hood
|
|
|
|
by psycopg2.
|
|
|
|
|
|
|
|
At last, just notice that ObjectMapper is working for both Album and Order
|
|
|
|
instances without any glitches at all, and both classes could have easily been
|
|
|
|
coming from closed source libraries or C coded ones (which are not easily
|
|
|
|
modified), whereas a common pattern in todays ORMs or OODBs is to provide
|
|
|
|
a basic 'Persistent' object that already knows how to store itself in the
|
|
|
|
database.
|
|
|
|
"""
|