"""Mapping a vertical table as a dictionary. This example illustrates accessing and modifying a "vertical" (or "properties", or pivoted) table via a dict-like interface. These are tables that store free-form object properties as rows instead of columns. For example, instead of:: # A regular ("horizontal") table has columns for 'species' and 'size' Table('animal', metadata, Column('id', Integer, primary_key=True), Column('species', Unicode), Column('size', Unicode)) A vertical table models this as two tables: one table for the base or parent entity, and another related table holding key/value pairs:: Table('animal', metadata, Column('id', Integer, primary_key=True)) # The properties table will have one row for a 'species' value, and # another row for the 'size' value. Table('properties', metadata Column('animal_id', Integer, ForeignKey('animal.id'), primary_key=True), Column('key', UnicodeText), Column('value', UnicodeText)) Because the key/value pairs in a vertical scheme are not fixed in advance, accessing them like a Python dict can be very convenient. The example below can be used with many common vertical schemas as-is or with minor adaptations. """ from __future__ import unicode_literals class ProxiedDictMixin(object): """Adds obj[key] access to a mapped class. This class basically proxies dictionary access to an attribute called ``_proxied``. The class which inherits this class should have an attribute called ``_proxied`` which points to a dictionary. """ def __len__(self): return len(self._proxied) def __iter__(self): return iter(self._proxied) def __getitem__(self, key): return self._proxied[key] def __contains__(self, key): return key in self._proxied def __setitem__(self, key, value): self._proxied[key] = value def __delitem__(self, key): del self._proxied[key] if __name__ == '__main__': from sqlalchemy import (Column, Integer, Unicode, ForeignKey, UnicodeText, and_, create_engine) from sqlalchemy.orm import relationship, Session from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy Base = declarative_base() class AnimalFact(Base): """A fact about an animal.""" __tablename__ = 'animal_fact' animal_id = Column(ForeignKey('animal.id'), primary_key=True) key = Column(Unicode(64), primary_key=True) value = Column(UnicodeText) class Animal(ProxiedDictMixin, Base): """an Animal""" __tablename__ = 'animal' id = Column(Integer, primary_key=True) name = Column(Unicode(100)) facts = relationship("AnimalFact", collection_class=attribute_mapped_collection('key')) _proxied = association_proxy("facts", "value", creator= lambda key, value: AnimalFact(key=key, value=value)) def __init__(self, name): self.name = name def __repr__(self): return "Animal(%r)" % self.name @classmethod def with_characteristic(self, key, value): return self.facts.any(key=key, value=value) engine = create_engine("sqlite://") Base.metadata.create_all(engine) session = Session(bind=engine) stoat = Animal('stoat') stoat['color'] = 'reddish' stoat['cuteness'] = 'somewhat' # dict-like assignment transparently creates entries in the # stoat.facts collection: print(stoat.facts['color']) session.add(stoat) session.commit() critter = session.query(Animal).filter(Animal.name == 'stoat').one() print(critter['color']) print(critter['cuteness']) critter['cuteness'] = 'very' print('changing cuteness:') marten = Animal('marten') marten['color'] = 'brown' marten['cuteness'] = 'somewhat' session.add(marten) shrew = Animal('shrew') shrew['cuteness'] = 'somewhat' shrew['poisonous-part'] = 'saliva' session.add(shrew) loris = Animal('slow loris') loris['cuteness'] = 'fairly' loris['poisonous-part'] = 'elbows' session.add(loris) q = (session.query(Animal). filter(Animal.facts.any( and_(AnimalFact.key == 'color', AnimalFact.value == 'reddish')))) print('reddish animals', q.all()) q = session.query(Animal).\ filter(Animal.with_characteristic("color", 'brown')) print('brown animals', q.all()) q = session.query(Animal).\ filter(~Animal.with_characteristic("poisonous-part", 'elbows')) print('animals without poisonous-part == elbows', q.all()) q = (session.query(Animal). filter(Animal.facts.any(value='somewhat'))) print('any animal with any .value of "somewhat"', q.all())