from sqlalchemy.orm.collections import mapped_collection
import datetime
-e = MetaData('sqlite://')
-e.bind.echo = True
+engine = create_engine('sqlite://', echo=False)
+meta = MetaData(engine)
-Session = scoped_session(sessionmaker(transactional=True))
+Session = scoped_session(sessionmaker())
-# this table represents Entity objects. each Entity gets a row in this table,
-# with a primary key and a title.
-entities = Table('entities', e,
+# represent Entity objects
+entities = Table('entities', meta,
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,
+# 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))
-# 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,
+# 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('binary_value', PickleType),
Column('datetime_value', DateTime))
-e.create_all()
+meta.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. """
-
+ """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):
- """getattr proxies requests for attributes which dont 'exist' on the object
- to the underying _entities dictionary."""
+ """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):
- """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."""
+ """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)
+ 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):
+class _EntityField(object):
+ """Represents a field of a particular name and datatype."""
+
+ def __init__(self, name, datatype):
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)
+ 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)
- name = property(lambda s: s.field.name)
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(_EntityField, entity_fields)
mapper(
- EntityValue, entity_values,
+ _EntityValue, entity_values,
properties = {
- 'field' : relation(EntityField, lazy=False, cascade='all')
+ '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))
+ '_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:
+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.flush()
+session.commit()