]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- start reworking examples to include more code from the wiki.
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 29 Nov 2013 23:53:53 +0000 (18:53 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 29 Nov 2013 23:53:53 +0000 (18:53 -0500)
- add the other versioning examples from the wiki
- modernize the dictlike examples

doc/build/orm/examples.rst
examples/versioned_history/__init__.py [moved from examples/versioning/__init__.py with 87% similarity]
examples/versioned_history/history_meta.py [moved from examples/versioning/history_meta.py with 100% similarity]
examples/versioned_history/test_versioning.py [moved from examples/versioning/test_versioning.py with 100% similarity]
examples/vertical/dictlike-polymorphic.py
examples/vertical/dictlike.py

index 60d075a3693263670b8cdc3c5aae0227bbcd438e..484865e7eebbb4076e8d317de39ba7a6ad337311 100644 (file)
@@ -1,18 +1,21 @@
 .. _examples_toplevel:
 
-Examples
-========
+============
+ORM Examples
+============
 
 The SQLAlchemy distribution includes a variety of code examples illustrating
 a select set of patterns, some typical and some not so typical.   All are
 runnable and can be found in the ``/examples`` directory of the
-distribution.   Each example contains a README in its ``__init__.py`` file,
-each of which are listed below.
+distribution.   Descriptions and source code for all can be found here.
 
 Additional SQLAlchemy examples, some user contributed, are available on the
 wiki at `<http://www.sqlalchemy.org/trac/wiki/UsageRecipes>`_.
 
 
+Mapping Recipes
+===============
+
 .. _examples_adjacencylist:
 
 Adjacency List
@@ -27,80 +30,98 @@ Associations
 
 .. automodule:: examples.association
 
-
-.. _examples_instrumentation:
-
-Attribute Instrumentation
--------------------------
-
-.. automodule:: examples.custom_attributes
-
-.. _examples_caching:
-
-Dogpile Caching
----------------
-
-.. automodule:: examples.dogpile_caching
-
 Directed Graphs
 ---------------
 
 .. automodule:: examples.graphs
 
 Dynamic Relations as Dictionaries
-----------------------------------
+------------------------------------
 
 .. automodule:: examples.dynamic_dict
 
 .. _examples_generic_associations:
 
 Generic Associations
---------------------
+------------------------
 
 .. automodule:: examples.generic_associations
 
-.. _examples_sharding:
-
-Horizontal Sharding
--------------------
-
-.. automodule:: examples.sharding
-
-Inheritance Mappings
---------------------
-
-.. automodule:: examples.inheritance
-
 Large Collections
------------------
+------------------------
 
 .. automodule:: examples.large_collection
 
 Nested Sets
------------
+------------
 
 .. automodule:: examples.nested_sets
 
-.. _examples_postgis:
+.. _examples_xmlpersistence:
 
-PostGIS Integration
--------------------
+XML Persistence
+------------------------
 
-.. automodule:: examples.postgis
+.. automodule:: examples.elementtree
+
+Versioning Objects
+------------------------
+
+Versioning with a History Table
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. automodule:: examples.versioned_history
 
-Versioned Objects
------------------
+Versioning using Temporal Rows
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-.. automodule:: examples.versioning
+.. automodule:: examples.versioned_rows
 
 Vertical Attribute Mapping
---------------------------
+------------------------------------
 
 .. automodule:: examples.vertical
 
-.. _examples_xmlpersistence:
 
-XML Persistence
----------------
+Inheritance Mapping Recipes
+============================
+
+Basic Inheritance Mappings
+----------------------------------
+
+.. automodule:: examples.inheritance
+
+Special APIs
+============
+
+.. _examples_instrumentation:
+
+Attribute Instrumentation
+------------------------------------
+
+.. automodule:: examples.custom_attributes
+
+.. _examples_sharding:
+
+Horizontal Sharding
+------------------------
+
+.. automodule:: examples.sharding
+
+Extending the ORM
+=================
+
+.. _examples_caching:
+
+Dogpile Caching
+------------------------
+
+.. automodule:: examples.dogpile_caching
+
+.. _examples_postgis:
+
+PostGIS Integration
+------------------------
+
+.. automodule:: examples.postgis
 
-.. automodule:: examples.elementtree
similarity index 87%
rename from examples/versioning/__init__.py
rename to examples/versioned_history/__init__.py
index a2a6a1813c04b45aa8aef6c4fb9705db0025de6a..d630b994b06b9cde66a2ec71f8b2ddf252dd666d 100644 (file)
@@ -1,8 +1,6 @@
 """
 Illustrates an extension which creates version tables for entities and stores
-records for each change. The same idea as Elixir's versioned extension, but
-more efficient (uses attribute API to get history) and handles class
-inheritance. The given extensions generate an anonymous "history" class which
+records for each change. The given extensions generate an anonymous "history" class which
 represents historical versions of the target object.
 
 Usage is illustrated via a unit test module ``test_versioning.py``, which can
index 872a7c52edf151b92607e505383bedf96821a4c8..e3d5ba5788d97e7eb0833cc492fc68d056866eea 100644 (file)
@@ -1,10 +1,7 @@
 """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::
+Builds upon the dictlike.py example to also add differently typed
+columns to the "fact" table, e.g.::
 
   Table('properties', metadata
         Column('owner_id', Integer, ForeignKey('owner.id'),
@@ -25,80 +22,43 @@ we'll use a @hybrid_property to build a smart '.value' attribute that wraps up
 reading and writing those various '_value' columns and keeps the '.type' up to
 date.
 
-Class decorators are used, so Python 2.6 or greater is required.
 """
 
 from sqlalchemy.orm.interfaces import PropComparator
-from sqlalchemy.orm import comparable_property
 from sqlalchemy.ext.hybrid import hybrid_property
-
-# Using the VerticalPropertyDictMixin from the base example
-from .dictlike import VerticalPropertyDictMixin
+from sqlalchemy import event
+from sqlalchemy import literal_column
+from .dictlike import ProxiedDictMixin
 
 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'),
-      }
+    The class which is mapped should indicate typing information
+    within the "info" dictionary of mapped Column objects; see
+    the AnimalFact mapping below for an example.
 
-    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),
-        }
-
     def __init__(self, key, value=None):
         self.key = key
         self.value = value
 
     @hybrid_property
     def value(self):
-        for discriminator, field in self.type_map.values():
-            if self.type == discriminator:
-                return getattr(self, field)
-        return None
+        fieldname, discriminator = self.type_map[self.type]
+        if fieldname is None:
+            return None
+        else:
+            return getattr(self, fieldname)
 
     @value.setter
     def 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)
+        fieldname, discriminator = self.type_map[py_type]
+
+        self.type = discriminator
+        if fieldname is not None:
+            setattr(self, fieldname, value)
 
     @value.deleter
     def value(self):
@@ -113,9 +73,14 @@ class PolymorphicVerticalProperty(object):
             self.cls = cls
 
         def _case(self):
-            whens = [(text("'%s'" % p[0]), cast(getattr(self.cls, p[1]), String))
-                     for p in self.cls.type_map.values()
-                     if p[1] is not None]
+            pairs = set(self.cls.type_map.values())
+            whens = [
+                (
+                    literal_column("'%s'" % discriminator),
+                    cast(getattr(self.cls, attribute), String)
+                ) for attribute, discriminator in pairs
+                if attribute is not None
+            ]
             return case(whens, self.cls.type, null())
         def __eq__(self, other):
             return self._case() == cast(other, String)
@@ -125,69 +90,78 @@ class PolymorphicVerticalProperty(object):
     def __repr__(self):
         return '<%s %r=%r>' % (self.__class__.__name__, self.key, self.value)
 
+@event.listens_for(PolymorphicVerticalProperty, "mapper_configured", propagate=True)
+def on_new_class(mapper, cls_):
+    """Look for Column objects with type info in them, and work up
+    a lookup table."""
+
+    info_dict = {}
+    info_dict[type(None)] = (None, 'none')
+    info_dict['none'] = (None, 'none')
+
+    for k in mapper.c.keys():
+        col = mapper.c[k]
+        if 'type' in col.info:
+            python_type, discriminator = col.info['type']
+            info_dict[python_type] = (k, discriminator)
+            info_dict[discriminator] = (k, discriminator)
+    cls_.type_map = info_dict
 
 if __name__ == '__main__':
-    from sqlalchemy import (MetaData, Table, Column, Integer, Unicode,
-        ForeignKey, UnicodeText, and_, not_, or_, String, Boolean, cast, text,
+    from sqlalchemy import (Column, Integer, Unicode,
+        ForeignKey, UnicodeText, and_, or_, String, Boolean, cast,
         null, case, create_engine)
-    from sqlalchemy.orm import mapper, relationship, Session
+    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()
 
-    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: ('integer', 'int_value'),
-            str: ('char', 'char_value'),
-            bool: ('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'
+    class AnimalFact(PolymorphicVerticalProperty, 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)
+        type = Column(Unicode(16))
+
+        # add information about storage for different types
+        # in the info dictionary of Columns
+        int_value = Column(Integer, info={'type': (int, 'integer')})
+        char_value = Column(UnicodeText, info={'type': (str, 'string')})
+        boolean_value = Column(Boolean, info={'type': (bool, 'boolean')})
+
+    class Animal(ProxiedDictMixin._base_class(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 '<%s %r>' % (self.__class__.__name__, self.name)
-
+            return "Animal(%r)" % self.name
 
-    mapper(Animal, animals, properties={
-        'facts': relationship(
-            AnimalFact, backref='animal',
-            collection_class=attribute_mapped_collection('key')),
-        })
-
-    mapper(AnimalFact, chars)
+        @classmethod
+        def with_characteristic(self, key, value):
+            return self.facts.any(key=key, value=value)
 
     engine = create_engine('sqlite://', echo=True)
 
-    metadata.create_all(engine)
+    Base.metadata.create_all(engine)
     session = Session(engine)
 
     stoat = Animal('stoat')
@@ -227,30 +201,24 @@ if __name__ == '__main__':
                 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('weasel-like', True))))
+         filter(Animal.with_characteristic('weasel-like', True)))
     print('weasel-like animals again', q.all())
 
     q = (session.query(Animal).
-           filter(Animal.facts.any(with_characteristic('poisonous', False))))
+           filter(Animal.with_characteristic('poisonous', False)))
     print('animals with poisonous=False', q.all())
 
     q = (session.query(Animal).
-         filter(or_(Animal.facts.any(
-                      with_characteristic('poisonous', False)),
-                    not_(Animal.facts.any(AnimalFact.key == 'poisonous')))))
+         filter(or_(
+                    Animal.with_characteristic('poisonous', False),
+                    ~Animal.facts.any(AnimalFact.key == '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('cuteness', 'very cute')))
-    print(q.all())
index f17d1acc88875a52206e6c124bd05481c4238726..1a3631354b9bbb366c68ff5dba47e40040dc2648 100644 (file)
@@ -30,150 +30,101 @@ 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
+from sqlalchemy.util.compat import with_metaclass
+from collections import MutableMapping
+from abc import ABCMeta
+from sqlalchemy.ext.declarative import DeclarativeMeta
 
-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):
+class ProxiedDictMixin(MutableMapping):
     """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
-      relationship of _property_type instances.
-
-    Using the VerticalProperty class above as an example,::
+    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.
 
-      class MyObj(VerticalPropertyDictMixin):
-          _property_type = VerticalProperty
-          _property_mapping = 'props'
-
-      mapper(MyObj, sometable, properties={
-        'props': relationship(VerticalProperty,
-                          collection_class=attribute_mapped_collection('key'))})
-
-    Dict-like access to MyObj is proxied through to the 'props' relationship::
-
-      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
+    The use of this class is optional, as it requires some
+    elaborate metaclass arithmetic in order to use it with declarative.
 
     """
 
-    _property_type = VerticalProperty
-    _property_mapping = None
+    def __len__(self):
+        return len(self._proxied)
 
-    __map = property(lambda self: getattr(self, self._property_mapping))
+    def __iter__(self):
+        return iter(self._proxied)
 
     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]
+        return self._proxied[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()
+        return key in self._proxied
 
-    def values(self):
-        return [prop.value for prop in self.__map.values()]
+    def __setitem__(self, key, value):
+        self._proxied[key] = value
 
-    def items(self):
-        return [(key, prop.value) for key, prop in self.__map.items()]
+    def __delitem__(self, key):
+        del self._proxied[key]
 
-    def __iter__(self):
-        return iter(self.keys())
+    @classmethod
+    def _base_class(cls, base):
+        """Perform the requisite metaclass trickery in order
+        to get DeclarativeMeta and ABCMeta to play nicely together,
+        while also remaining Python 2/3 agnostic.
 
+        """
+        return with_metaclass(
+                        type(str("MutableBase"), (ABCMeta, DeclarativeMeta), {}),
+                        base, cls)
 
 if __name__ == '__main__':
-    from sqlalchemy import (MetaData, Table, Column, Integer, Unicode,
-        ForeignKey, UnicodeText, and_, not_, create_engine)
-    from sqlalchemy.orm import mapper, relationship, Session
+    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
 
-    metadata = MetaData()
+    Base = declarative_base()
 
-    # 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)))
+    class AnimalFact(Base):
+        """A fact about an animal."""
 
-    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),)
+        __tablename__ = 'animal_fact'
 
-    class AnimalFact(VerticalProperty):
-        """A fact about an animal."""
+        animal_id = Column(ForeignKey('animal.id'), primary_key=True)
+        key = Column(Unicode(64), primary_key=True)
+        value = Column(UnicodeText)
 
-    class Animal(VerticalPropertyDictMixin):
-        """An animal.
+    class Animal(ProxiedDictMixin._base_class(Base)):
+        """an Animal"""
 
-        Animal facts are available via the 'facts' property or by using
-        dict-like accessors on an Animal instance::
+        __tablename__ = 'animal'
 
-          cat['color'] = 'calico'
-          # or, equivalently:
-          cat.facts['color'] = AnimalFact('color', 'calico')
-        """
+        id = Column(Integer, primary_key=True)
+        name = Column(Unicode(100))
 
-        _property_type = AnimalFact
-        _property_mapping = 'facts'
+        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 '<%s %r>' % (self.__class__.__name__, self.name)
-
+            return "Animal(%r)" % self.name
 
-    mapper(Animal, animals, properties={
-        'facts': relationship(
-            AnimalFact, backref='animal',
-            collection_class=attribute_mapped_collection('key')),
-        })
-    mapper(AnimalFact, facts)
+        @classmethod
+        def with_characteristic(self, key, value):
+            return self.facts.any(key=key, value=value)
 
     engine = create_engine("sqlite://")
-    metadata.create_all(engine)
+    Base.metadata.create_all(engine)
+
     session = Session(bind=engine)
 
     stoat = Animal('stoat')
@@ -194,9 +145,6 @@ if __name__ == '__main__':
     critter['cuteness'] = 'very'
 
     print('changing cuteness:')
-    engine.echo = True
-    session.commit()
-    engine.echo = False
 
     marten = Animal('marten')
     marten['color'] = 'brown'
@@ -212,7 +160,6 @@ if __name__ == '__main__':
     loris['cuteness'] = 'fairly'
     loris['poisonous-part'] = 'elbows'
     session.add(loris)
-    session.commit()
 
     q = (session.query(Animal).
          filter(Animal.facts.any(
@@ -220,27 +167,17 @@ if __name__ == '__main__':
                 AnimalFact.value == '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('color', 'brown'))))
+    q = session.query(Animal).\
+            filter(Animal.with_characteristic("color", 'brown'))
     print('brown animals', q.all())
 
-    q = (session.query(Animal).
-           filter(not_(Animal.facts.any(
-                         with_characteristic('poisonous-part', 'elbows')))))
+    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(AnimalFact.value == 'somewhat')))
+         filter(Animal.facts.any(value='somewhat')))
     print('any animal with any .value of "somewhat"', q.all())
 
-    # Facts can be queried as well.
-    q = (session.query(AnimalFact).
-         filter(with_characteristic('cuteness', 'very')))
-    print('just the facts', q.all())