]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added two new vertical dict mapping examples.
authorJason Kirtland <jek@discorporate.us>
Tue, 12 Feb 2008 01:44:20 +0000 (01:44 +0000)
committerJason Kirtland <jek@discorporate.us>
Tue, 12 Feb 2008 01:44:20 +0000 (01:44 +0000)
examples/vertical/dictlike-polymorphic.py [new file with mode: 0644]
examples/vertical/dictlike.py [new file with mode: 0644]

diff --git a/examples/vertical/dictlike-polymorphic.py b/examples/vertical/dictlike-polymorphic.py
new file mode 100644 (file)
index 0000000..66dcd48
--- /dev/null
@@ -0,0 +1,286 @@
+"""Mapping a polymorphic-valued vertical table as a dictionary.
+
+This example illustrates accessing and modifying a "vertical" (or
+"properties", or pivoted) table via a dict-like interface.  The 'dictlike.py'
+example explains the basics of vertical tables and the general approach.  This
+example adds a twist- the vertical table holds several "value" columns, one
+for each type of data that can be stored.  For example::
+
+  Table('properties', metadata
+        Column('owner_id', Integer, ForeignKey('owner.id'),
+               primary_key=True),
+        Column('key', UnicodeText),
+        Column('type', Unicode(16)),
+        Column('int_value', Integer),
+        Column('char_value', UnicodeText),
+        Column('bool_value', Boolean),
+        Column('decimal_value', Numeric(10,2)))
+
+For any given properties row, the value of the 'type' column will point to the
+'_value' column active for that row.
+
+This example approach uses exactly the same dict mapping approach as the
+'dictlike' example.  It only differs in the mapping for vertical rows.  Here,
+we'll use a Python @property to build a smart '.value' attribute that wraps up
+reading and writing those various '_value' columns and keeps the '.type' up to
+date.
+
+Note: Something much like 'comparable_property' is slated for inclusion in a
+      future version of SQLAlchemy.
+"""
+
+from sqlalchemy.orm.interfaces import PropComparator, MapperProperty
+from sqlalchemy.orm import session as sessionlib
+
+# Using the VerticalPropertyDictMixin from the base example
+from dictlike import VerticalPropertyDictMixin
+
+class PolymorphicVerticalProperty(object):
+    """A key/value pair with polymorphic value storage.
+
+    Supplies a smart 'value' attribute that provides convenient read/write
+    access to the row's current value without the caller needing to worry
+    about the 'type' attribute or multiple columns.
+
+    The 'value' attribute can also be used for basic comparisons in queries,
+    allowing the row's logical value to be compared without foreknowledge of
+    which column it might be in.  This is not going to be a very efficient
+    operation on the database side, but it is possible.  If you're mapping to
+    an existing database and you have some rows with a value of str('1') and
+    others of int(1), then this could be useful.
+
+    Subclasses must provide a 'type_map' class attribute with the following
+    form::
+
+      type_map = {
+         <python type> : ('type column value', 'column name'),
+         # ...
+      }
+
+    For example,::
+
+      type_map = {
+        int: ('integer', 'integer_value'),
+        str: ('varchar', 'varchar_value'),
+      }
+
+    Would indicate that a Python int value should be stored in the
+    'integer_value' column and the .type set to 'integer'.  Conversely, if the
+    value of '.type' is 'integer, then the 'integer_value' column is consulted
+    for the current value.
+    """
+
+    type_map = {
+        type(None): (None, None),
+        }
+
+    class Comparator(PropComparator):
+        """A comparator for .value, builds a polymorphic comparison via CASE.
+
+        Optional.  If desired, install it as a comparator in the mapping::
+
+          mapper(..., properties={
+            'value': comparable_property(PolymorphicVerticalProperty.Comparator,
+                                         PolymorphicVerticalProperty.value)
+          })
+        """
+
+        def _case(self):
+            cls = self.prop.parent.class_
+            whens = [(text("'%s'" % p[0]), getattr(cls, p[1]))
+                     for p in cls.type_map.values()
+                     if p[1] is not None]
+            return case(whens, cls.type, null())
+        def __eq__(self, other):
+            return cast(self._case(), String) == cast(other, String)
+        def __ne__(self, other):
+            return cast(self._case(), String) != cast(other, String)
+
+    def __init__(self, key, value=None):
+        self.key = key
+        self.value = value
+
+    def _get_value(self):
+        for discriminator, field in self.type_map.values():
+            if self.type == discriminator:
+                return getattr(self, field)
+        return None
+
+    def _set_value(self, value):
+        py_type = type(value)
+        if py_type not in self.type_map:
+            raise TypeError(py_type)
+
+        for field_type in self.type_map:
+            discriminator, field = self.type_map[field_type]
+            field_value = None
+            if py_type == field_type:
+                self.type = discriminator
+                field_value = value
+            if field is not None:
+                setattr(self, field, field_value)
+
+    def _del_value(self):
+        self._set_value(None)
+
+    value = property(_get_value, _set_value, _del_value, doc=
+                     """The logical value of this property.""")
+
+    def __repr__(self):
+        return '<%s %r=%r>' % (self.__class__.__name__, self.key, self.value)
+
+
+class comparable_property(MapperProperty):
+    """Instruments a Python property for use in query expressions."""
+
+    def __init__(self, comparator, property):
+        self.property = property
+        self.comparator = comparator(self)
+
+    def do_init(self):
+        class_ = self.parent.class_
+        sessionlib.register_attribute(class_, self.key, uselist=False,
+                                      proxy_property=self.property,
+                                      useobject=False,
+                                      comparator=self.comparator)
+
+    def setup(self, querycontext, **kwargs):
+        pass
+
+    def create_row_processor(self, selectcontext, mapper, row):
+        return (None, None, None)
+
+
+if __name__ == '__main__':
+    from sqlalchemy import *
+    from sqlalchemy.orm import mapper, relation, create_session
+    from sqlalchemy.orm.collections import attribute_mapped_collection
+
+    metadata = MetaData()
+
+    animals = Table('animal', metadata,
+                    Column('id', Integer, primary_key=True),
+                    Column('name', Unicode(100)))
+
+    chars = Table('facts', metadata,
+                  Column('animal_id', Integer, ForeignKey('animal.id'),
+                         primary_key=True),
+                  Column('key', Unicode(64), primary_key=True),
+                  Column('type', Unicode(16), default=None),
+                  Column('int_value', Integer, default=None),
+                  Column('char_value', UnicodeText, default=None),
+                  Column('boolean_value', Boolean, default=None))
+
+    class AnimalFact(PolymorphicVerticalProperty):
+        type_map = {
+            int: (u'integer', 'int_value'),
+            unicode: (u'char', 'char_value'),
+            bool: (u'boolean', 'boolean_value'),
+            type(None): (None, None),
+            }
+
+    class Animal(VerticalPropertyDictMixin):
+        """An animal.
+
+        Animal facts are available via the 'facts' property or by using
+        dict-like accessors on an Animal instance::
+
+          cat['color'] = 'calico'
+          # or, equivalently:
+          cat.facts['color'] = AnimalFact('color', 'calico')
+        """
+
+        _property_type = AnimalFact
+        _property_mapping = 'facts'
+
+        def __init__(self, name):
+            self.name = name
+
+        def __repr__(self):
+            return '<%s %r>' % (self.__class__.__name__, self.name)
+
+
+    mapper(Animal, animals, properties={
+        'facts': relation(
+            AnimalFact, backref='animal',
+            collection_class=attribute_mapped_collection('key')),
+        })
+
+    mapper(AnimalFact, chars, properties={
+        'value': comparable_property(AnimalFact.Comparator, AnimalFact.value)
+        })
+
+    metadata.bind = 'sqlite:///'
+    metadata.create_all()
+    session = create_session()
+
+    stoat = Animal(u'stoat')
+    stoat[u'color'] = u'red'
+    stoat[u'cuteness'] = 7
+    stoat[u'weasel-like'] = True
+
+    session.save(stoat)
+    session.flush()
+    session.clear()
+
+    critter = session.query(Animal).filter(Animal.name == u'stoat').one()
+    print critter[u'color']
+    print critter[u'cuteness']
+
+    print "changing cuteness value and type:"
+    critter[u'cuteness'] = u'very cute'
+
+    metadata.bind.echo = True
+    session.flush()
+    metadata.bind.echo = False
+
+    marten = Animal(u'marten')
+    marten[u'cuteness'] = 5
+    marten[u'weasel-like'] = True
+    marten[u'poisonous'] = False
+    session.save(marten)
+
+    shrew = Animal(u'shrew')
+    shrew[u'cuteness'] = 5
+    shrew[u'weasel-like'] = False
+    shrew[u'poisonous'] = True
+
+    session.save(shrew)
+    session.flush()
+
+    q = (session.query(Animal).
+         filter(Animal.facts.any(
+           and_(AnimalFact.key == u'weasel-like',
+                AnimalFact.value == True))))
+    print 'weasel-like animals', q.all()
+
+    # Save some typing by wrapping that up in a function:
+    with_characteristic = lambda key, value: and_(AnimalFact.key == key,
+                                                  AnimalFact.value == value)
+
+    q = (session.query(Animal).
+         filter(Animal.facts.any(
+           with_characteristic(u'weasel-like', True))))
+    print 'weasel-like animals again', q.all()
+
+    q = (session.query(Animal).
+           filter(Animal.facts.any(with_characteristic(u'poisonous', False))))
+    print 'animals with poisonous=False', q.all()
+
+    q = (session.query(Animal).
+         filter(or_(Animal.facts.any(
+                      with_characteristic(u'poisonous', False)),
+                    not_(Animal.facts.any(AnimalFact.key == u'poisonous')))))
+    print 'non-poisonous animals', q.all()
+
+    q = (session.query(Animal).
+         filter(Animal.facts.any(AnimalFact.value == 5)))
+    print 'any animal with a .value of 5', q.all()
+
+    # Facts can be queried as well.
+    q = (session.query(AnimalFact).
+         filter(with_characteristic(u'cuteness', u'very cute')))
+    print q.all()
+
+
+    metadata.drop_all()
diff --git a/examples/vertical/dictlike.py b/examples/vertical/dictlike.py
new file mode 100644 (file)
index 0000000..5f478d7
--- /dev/null
@@ -0,0 +1,247 @@
+"""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.
+"""
+
+class VerticalProperty(object):
+    """A key/value pair.
+
+    This class models rows in the vertical table.
+    """
+
+    def __init__(self, key, value):
+        self.key = key
+        self.value = value
+
+    def __repr__(self):
+        return '<%s %r=%r>' % (self.__class__.__name__, self.key, self.value)
+
+
+class VerticalPropertyDictMixin(object):
+    """Adds obj[key] access to a mapped class.
+
+    This is a mixin class.  It can be inherited from directly, or included
+    with multiple inheritence.
+
+    Classes using this mixin must define two class properties::
+
+    _property_type:
+      The mapped type of the vertical key/value pair instances.  Will be
+      invoked with two positional arugments: key, value
+
+    _property_mapping:
+      A string, the name of the Python attribute holding a dict-based
+      relation of _property_type instances.
+
+    Using the VerticalProperty class above as an example,::
+
+      class MyObj(VerticalPropertyDictMixin):
+          _property_type = VerticalProperty
+          _property_mapping = 'props'
+
+      mapper(MyObj, sometable, properties={
+        'props': relation(VerticalProperty,
+                          collection_class=attribute_mapped_collection('key'))})
+
+    Dict-like access to MyObj is proxied through to the 'props' relation::
+
+      myobj['key'] = 'value'
+      # ...is shorthand for:
+      myobj.props['key'] = VerticalProperty('key', 'value')
+
+      myobj['key'] = 'updated value']
+      # ...is shorthand for:
+      myobj.props['key'].value = 'updated value'
+
+      print myobj['key']
+      # ...is shorthand for:
+      print myobj.props['key'].value
+
+    """
+
+    _property_type = VerticalProperty
+    _property_mapping = None
+
+    __map = property(lambda self: getattr(self, self._property_mapping))
+
+    def __getitem__(self, key):
+        return self.__map[key].value
+
+    def __setitem__(self, key, value):
+        property = self.__map.get(key, None)
+        if property is None:
+            self.__map[key] = self._property_type(key, value)
+        else:
+            property.value = value
+
+    def __delitem__(self, key):
+        del self.__map[key]
+
+    def __contains__(self, key):
+        return key in self.__map
+
+    # Implement other dict methods to taste.  Here are some examples:
+    def keys(self):
+        return self.__map.keys()
+
+    def values(self):
+        return [prop.value for prop in self.__map.values()]
+
+    def items(self):
+        return [(key, prop.value) for key, prop in self.__map.items()]
+
+    def __iter__(self):
+        return iter(self.keys())
+
+
+if __name__ == '__main__':
+    from sqlalchemy import *
+    from sqlalchemy.orm import mapper, relation, create_session
+    from sqlalchemy.orm.collections import attribute_mapped_collection
+
+    metadata = MetaData()
+
+    # Here we have named animals, and a collection of facts about them.
+    animals = Table('animal', metadata,
+                    Column('id', Integer, primary_key=True),
+                    Column('name', Unicode(100)))
+
+    facts = Table('facts', metadata,
+                  Column('animal_id', Integer, ForeignKey('animal.id'),
+                         primary_key=True),
+                  Column('key', Unicode(64), primary_key=True),
+                  Column('value', UnicodeText, default=None),)
+
+    class AnimalFact(VerticalProperty):
+        """A fact about an animal."""
+
+    class Animal(VerticalPropertyDictMixin):
+        """An animal.
+
+        Animal facts are available via the 'facts' property or by using
+        dict-like accessors on an Animal instance::
+
+          cat['color'] = 'calico'
+          # or, equivalently:
+          cat.facts['color'] = AnimalFact('color', 'calico')
+        """
+
+        _property_type = AnimalFact
+        _property_mapping = 'facts'
+
+        def __init__(self, name):
+            self.name = name
+
+        def __repr__(self):
+            return '<%s %r>' % (self.__class__.__name__, self.name)
+
+
+    mapper(Animal, animals, properties={
+        'facts': relation(
+            AnimalFact, backref='animal',
+            collection_class=attribute_mapped_collection('key')),
+        })
+    mapper(AnimalFact, facts)
+
+
+    metadata.bind = 'sqlite:///'
+    metadata.create_all()
+    session = create_session()
+
+    stoat = Animal(u'stoat')
+    stoat[u'color'] = u'reddish'
+    stoat[u'cuteness'] = u'somewhat'
+
+    # dict-like assignment transparently creates entries in the
+    # stoat.facts collection:
+    print stoat.facts[u'color']
+
+    session.save(stoat)
+    session.flush()
+    session.clear()
+
+    critter = session.query(Animal).filter(Animal.name == u'stoat').one()
+    print critter[u'color']
+    print critter[u'cuteness']
+
+    critter[u'cuteness'] = u'very'
+
+    print 'changing cuteness:'
+    metadata.bind.echo = True
+    session.flush()
+    metadata.bind.echo = False
+
+    marten = Animal(u'marten')
+    marten[u'color'] = u'brown'
+    marten[u'cuteness'] = u'somewhat'
+    session.save(marten)
+
+    shrew = Animal(u'shrew')
+    shrew[u'cuteness'] = u'somewhat'
+    shrew[u'poisonous-part'] = u'saliva'
+    session.save(shrew)
+
+    loris = Animal(u'slow loris')
+    loris[u'cuteness'] = u'fairly'
+    loris[u'poisonous-part'] = u'elbows'
+    session.save(loris)
+    session.flush()
+
+    q = (session.query(Animal).
+         filter(Animal.facts.any(
+           and_(AnimalFact.key == u'color',
+                AnimalFact.value == u'reddish'))))
+    print 'reddish animals', q.all()
+
+    # Save some typing by wrapping that up in a function:
+    with_characteristic = lambda key, value: and_(AnimalFact.key == key,
+                                                  AnimalFact.value == value)
+
+    q = (session.query(Animal).
+         filter(Animal.facts.any(
+           with_characteristic(u'color', u'brown'))))
+    print 'brown animals', q.all()
+
+    q = (session.query(Animal).
+           filter(not_(Animal.facts.any(
+                         with_characteristic(u'poisonous-part', u'elbows')))))
+    print 'animals without poisonous-part == elbows', q.all()
+
+    q = (session.query(Animal).
+         filter(Animal.facts.any(AnimalFact.value == u'somewhat')))
+    print 'any animal with any .value of "somewhat"', q.all()
+
+    # Facts can be queried as well.
+    q = (session.query(AnimalFact).
+         filter(with_characteristic(u'cuteness', u'very')))
+    print 'just the facts', q.all()
+
+
+    metadata.drop_all()