Files
sqlalchemy/examples/vertical/vertical.py
T
Mike Bayer 9629838f7d - fixed wrong varname in session exception throw
- fixed vertical example to just use a scoped session
2007-12-05 20:55:33 +00:00

175 lines
6.5 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 *
from sqlalchemy.orm import *
from sqlalchemy.orm.collections import mapped_collection
import datetime
e = MetaData('sqlite://')
e.bind.echo = True
Session = scoped_session(sessionmaker(transactional=True))
# this table represents Entity objects. each Entity gets a row in this table,
# with a primary key and a title.
entities = Table('entities', e,
Column('entity_id', Integer, primary_key=True),
Column('title', String(100), nullable=False),
)
# this table represents dynamic fields that can be associated
# with values attached to an Entity.
# a field has an ID, a name, and a datatype.
entity_fields = Table('entity_fields', e,
Column('field_id', Integer, primary_key=True),
Column('name', String(40), nullable=False),
Column('datatype', String(30), nullable=False))
# this table represents attributes that are attached to an
# Entity object. It combines a row from entity_fields with an actual value.
# the value is stored in one of four columns, corresponding to the datatype
# of the field value.
entity_values = Table('entity_values', e,
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))
e.create_all()
class Entity(object):
"""represents an Entity. The __getattr__ method is overridden to search the
object's _entities dictionary for the appropriate value, and the __setattribute__
method is overridden to set all non "_" attributes as EntityValues within the
_entities dictionary. """
def __getattr__(self, key):
"""getattr proxies requests for attributes which dont 'exist' on the object
to the underying _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):
"""setattr proxies certain requests to set attributes as EntityValues within
the _entities dictionary. This one is tricky as it has to allow underscore attributes,
as well as attributes managed by the Mapper, to be set by default mechanisms. Since the
mapper uses property-like objects on the Entity class to manage attributes, we check
for the key as an attribute of the class and if present, default to normal __setattr__
mechanisms, else we use the special logic which creates EntityValue objects in the
_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):
"""this class represents a row in the entity_fields table."""
def __init__(self, name=None):
self.name = name
self.datatype = None
class EntityValue(object):
"""the main job of EntityValue is to hold onto a value, corresponding the type of
the value to the underlying datatype of its EntityField."""
def __init__(self, key=None, value=None):
if key is not None:
self.field = Session.query(EntityField).filter(EntityField.name==key).first() or EntityField(key)
if self.field.datatype is None:
if isinstance(value, int):
self.field.datatype = 'int'
elif isinstance(value, str):
self.field.datatype = 'string'
elif isinstance(value, datetime.datetime):
self.field.datatype = 'datetime'
else:
self.field.datatype = 'binary'
setattr(self, self.field.datatype + "_value", value)
def _get_value(self):
return getattr(self, self.field.datatype + "_value")
def _set_value(self, value):
setattr(self, self.field.datatype + "_value", value)
name = property(lambda s: s.field.name)
value = property(_get_value, _set_value)
# 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))
})
# create two entities. the objects can be used about as regularly as
# any object can.
session = Session()
entity = Entity()
entity.title = 'this is the first entity'
entity.name = 'this is the name'
entity.price = 43
entity.data = ('hello', 'there')
entity2 = Entity()
entity2.title = 'this is the second entity'
entity2.name = 'this is another name'
entity2.price = 50
entity2.data = ('hoo', 'ha')
# commit
[session.save(x) for x in (entity, entity2)]
session.flush()
# we would like to illustate loading everything totally clean from
# the database, so we clear out the session
session.clear()
# select both objects and print
entities = session.query(Entity).select()
for entity in entities:
print entity.title, entity.name, entity.price, entity.data
# now change some data
entities[0].price=90
entities[0].title = 'another new title'
entities[1].data = {'oof':5,'lala':8}
entity3 = Entity()
entity3.title = 'third entity'
entity3.name = 'new name'
entity3.price = '$1.95'
entity3.data = 'some data'
session.save(entity3)
# commit changes. the correct rows are updated, nothing else.
session.flush()
# lets see if that one came through. clear the session, re-select
# and print
session.clear()
entities = session.query(Entity).select()
for entity in entities:
print entity.title, entity.name, entity.price, entity.data
for entity in entities:
session.delete(entity)
session.flush()