mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-15 05:07:16 -04:00
9629838f7d
- fixed vertical example to just use a scoped session
175 lines
6.5 KiB
Python
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()
|