Files
sqlalchemy/examples/vertical/vertical.py
T
Michael Trier 6010afb28f Lots of fixes to the code examples to specify imports explicitly.
Explicit imports make it easier for users to understand the examples.
Additionally a lot of the examples were fixed to work with the changes in the
0.5.x code base. One small correction to the Case expression.  Thanks a bunch
to Adam Lowry! Fixes #717.
2009-03-31 22:31:08 +00:00

202 lines
6.3 KiB
Python

"""this example illustrates a "vertical table". an object is stored with individual attributes
represented in distinct database rows. This allows objects to be created with dynamically changing
fields that are all persisted in a normalized fashion."""
from sqlalchemy import (create_engine, MetaData, Table, Column, Integer, String,
ForeignKey, PickleType, DateTime, and_)
from sqlalchemy.orm import mapper, relation, sessionmaker, scoped_session
from sqlalchemy.orm.collections import mapped_collection
import datetime
engine = create_engine('sqlite://', echo=False)
meta = MetaData(engine)
Session = scoped_session(sessionmaker())
# represent Entity objects
entities = Table('entities', meta,
Column('entity_id', Integer, primary_key=True),
Column('title', String(100), nullable=False),
)
# represent named, typed fields
entity_fields = Table('entity_fields', meta,
Column('field_id', Integer, primary_key=True),
Column('name', String(40), nullable=False),
Column('datatype', String(30), nullable=False))
# associate a field row with an entity row, including a typed value
entity_values = Table('entity_values', meta,
Column('value_id', Integer, primary_key=True),
Column('field_id', Integer, ForeignKey('entity_fields.field_id'), nullable=False),
Column('entity_id', Integer, ForeignKey('entities.entity_id'), nullable=False),
Column('int_value', Integer),
Column('string_value', String(500)),
Column('binary_value', PickleType),
Column('datetime_value', DateTime))
meta.create_all()
class Entity(object):
"""a persistable dynamic object.
Marshalls attributes into a dictionary which is
mapped to the database.
"""
def __init__(self, **kwargs):
for k in kwargs:
setattr(self, k, kwargs[k])
def __getattr__(self, key):
"""Proxy requests for attributes to the underlying _entities dictionary."""
if key[0] == '_':
return super(Entity, self).__getattr__(key)
try:
return self._entities[key].value
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
"""Proxy requests for attribute set operations to the underlying _entities dictionary."""
if key[0] == "_" or hasattr(Entity, key):
object.__setattr__(self, key, value)
return
try:
ev = self._entities[key]
ev.value = value
except KeyError:
ev = _EntityValue(key, value)
self._entities[key] = ev
class _EntityField(object):
"""Represents a field of a particular name and datatype."""
def __init__(self, name, datatype):
self.name = name
self.datatype = datatype
class _EntityValue(object):
"""Represents an individual value."""
def __init__(self, key, value):
datatype = self._figure_datatype(value)
field = \
Session.query(_EntityField).filter(
and_(_EntityField.name==key, _EntityField.datatype==datatype)
).first()
if not field:
field = _EntityField(key, datatype)
Session.add(field)
self.field = field
setattr(self, self.field.datatype + "_value", value)
def _figure_datatype(self, value):
typemap = {
int:'int',
str:'string',
datetime.datetime:'datetime',
}
for k in typemap:
if isinstance(value, k):
return typemap[k]
else:
return 'binary'
def _get_value(self):
return getattr(self, self.field.datatype + "_value")
def _set_value(self, value):
setattr(self, self.field.datatype + "_value", value)
value = property(_get_value, _set_value)
def name(self):
return self.field.name
name = property(name)
# the mappers are a straightforward eager chain of
# Entity--(1->many)->EntityValue-(many->1)->EntityField
# notice that we are identifying each mapper to its connecting
# relation by just the class itself.
mapper(_EntityField, entity_fields)
mapper(
_EntityValue, entity_values,
properties = {
'field' : relation(_EntityField, lazy=False, cascade='all')
}
)
mapper(Entity, entities, properties = {
'_entities' : relation(
_EntityValue,
lazy=False,
cascade='all',
collection_class=mapped_collection(lambda entityvalue: entityvalue.field.name)
)
})
session = Session()
entity1 = Entity(
title = 'this is the first entity',
name = 'this is the name',
price = 43,
data = ('hello', 'there')
)
entity2 = Entity(
title = 'this is the second entity',
name = 'this is another name',
price = 50,
data = ('hoo', 'ha')
)
session.add_all([entity1, entity2])
session.commit()
for entity in session.query(Entity):
print "Entity id %d:" % entity.entity_id, entity.title, entity.name, entity.price, entity.data
# perform some changes, add a new Entity
entity1.price = 90
entity1.title = 'another new title'
entity2.data = {'oof':5,'lala':8}
entity3 = Entity(
title = 'third entity',
name = 'new name',
price = '$1.95', # note we change 'price' to be a string.
# this creates a new _EntityField separate from the
# one used by integer 'price'.
data = 'some data'
)
session.add(entity3)
session.commit()
print "----------------"
for entity in session.query(Entity):
print "Entity id %d:" % entity.entity_id, entity.title, entity.name, entity.price, entity.data
print "----------------"
# illustrate each _EntityField that's been created and list each Entity which uses it
for ent_id, name, datatype in session.query(_EntityField.field_id, _EntityField.name, _EntityField.datatype):
print name, datatype, "(Enitites:", ",".join([
str(entid) for entid in session.query(Entity.entity_id).\
join(
(_EntityValue, _EntityValue.entity_id==Entity.entity_id),
(_EntityField, _EntityField.field_id==_EntityValue.field_id)
).filter(_EntityField.field_id==ent_id)
]), ")"
# delete all the Entity objects
for entity in session.query(Entity):
session.delete(entity)
session.commit()