]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [feature] Added an example to the hybrid docs
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 5 Dec 2011 22:06:15 +0000 (17:06 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 5 Dec 2011 22:06:15 +0000 (17:06 -0500)
of a "transformer" - a hybrid that returns a
query-transforming callable in combination
with a custom comparator.   Uses a new method
on Query called with_transformation().  The use
case here is fairly experimental, but only
adds one line of code to Query.

CHANGES
lib/sqlalchemy/ext/hybrid.py
lib/sqlalchemy/orm/query.py

diff --git a/CHANGES b/CHANGES
index 2b0220714d9bc801d89fb29b28cd36811a8b3947..06c55158f2162800699111085b6b64e6936710ab 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -252,6 +252,15 @@ CHANGES
   - [bug] Unicode adjustments allow latest pymysql 
     (post 0.4) to pass 100% on Python 2.
 
+- ext
+   - [feature] Added an example to the hybrid docs
+     of a "transformer" - a hybrid that returns a
+     query-transforming callable in combination
+     with a custom comparator.   Uses a new method
+     on Query called with_transformation().  The use
+     case here is fairly experimental, but only
+     adds one line of code to Query.
+
 - examples
    - [bug] Fixed bug in history_meta.py example where
      the "unique" flag was not removed from a 
index b968ed001a913a5ec13849c8e079d9d45d9f91fb..31732b93b39f21fe298436cb1d0a46c18edb9d05 100644 (file)
@@ -386,6 +386,140 @@ Python only expression::
 The Hybrid Value pattern is very useful for any kind of value that may have multiple representations,
 such as timestamps, time deltas, units of measurement, currencies and encrypted passwords.
 
+See Also:
+
+`Hybrids and Value Agnostic Types <http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_ - on the techspot.zzzeek.org blog
+
+`Value Agnostic Types, Part II <http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ - on the techspot.zzzeek.org blog
+
+.. _hybrid_transformers:
+
+Building Transformers
+----------------------
+
+A *transformer* is an object which can receive a :class:`.Query` object and return a
+new one.   The :class:`.Query` object includes a method :meth:`.with_transformation` 
+that simply returns a new :class:`.Query` transformed by the given function.
+
+We can combine this with the :class:`.Comparator` class to produce one type
+of recipe which can both set up the FROM clause of a query as well as assign
+filtering criterion.
+
+Consider a mapped class ``Node``, which assembles using adjacency list into a hierarchical
+tree pattern::
+    
+    from sqlalchemy import Column, Integer, ForeignKey
+    from sqlalchemy.orm import relationship
+    from sqlalchemy.ext.declarative import declarative_base
+    Base = declarative_base()
+    
+    class Node(Base):
+        __tablename__ = 'node'
+        id =Column(Integer, primary_key=True)
+        parent_id = Column(Integer, ForeignKey('node.id'))
+        parent = relationship("Node", remote_side=id)
+    
+Suppose we wanted to add an accessor ``grandparent``.  This would return the ``parent`` of
+``Node.parent``.  When we have an instance of ``Node``, this is simple::
+
+    from sqlalchemy.ext.hybrid import hybrid_property
+
+    class Node(Base):
+        # ...
+        
+        @hybrid_property
+        def grandparent(self):
+            return self.parent.parent
+
+For the expression, things are not so clear.   We'd need to construct a :class:`.Query` where we
+:meth:`~.Query.join` twice along ``Node.parent`` to get to the ``grandparent``.   We can instead
+return a transforming callable that we'll combine with the :class:`.Comparator` class
+to receive any :class:`.Query` object, and return a new one that's joined to the ``Node.parent``
+attribute and filtered based on the given criterion::
+
+    from sqlalchemy.ext.hybrid import Comparator
+
+    class GrandparentTransformer(Comparator):
+        def operate(self, op, other):
+            def transform(q):
+                cls = self.__clause_element__()
+                parent_alias = aliased(cls)
+                return q.join(parent_alias, cls.parent).\\
+                            filter(op(parent_alias.parent, other))
+            return transform
+
+    Base = declarative_base()
+
+    class Node(Base):
+        __tablename__ = 'node'
+        id =Column(Integer, primary_key=True)
+        parent_id = Column(Integer, ForeignKey('node.id'))
+        parent = relationship("Node", remote_side=id)
+        
+        @hybrid_property
+        def grandparent(self):
+            return self.parent.parent
+
+        @grandparent.comparator
+        def grandparent(cls):
+            return GrandparentTransformer(cls)
+
+The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate` method
+at the base of the :class:`.Comparator` hierarchy to return a query-transforming
+callable, which then runs the given comparison operation in a particular context.
+Such as, in the example above, the ``operate`` method is called, given the
+:attr:`.Operators.eq` callable as well as the right side of the comparison
+``Node(id=5)``.  A function ``transform`` is then returned which will transform
+a :class:`.Query` first to join to ``Node.parent``, then to compare ``parent_alias``
+using :attr:`.Operators.eq` against the left and right sides, passing into
+:class:`.Query.filter`:
+
+.. sourcecode:: pycon+sql
+
+    >>> from sqlalchemy.orm import Session
+    >>> session = Session()
+    {sql}>>> session.query(Node).\\
+    ...        with_transformation(Node.grandparent==Node(id=5)).\\
+    ...        all()
+    SELECT node.id AS node_id, node.parent_id AS node_parent_id 
+    FROM node JOIN node AS node_1 ON node_1.id = node.parent_id 
+    WHERE :param_1 = node_1.parent_id
+    {stop}
+
+We can modify the pattern to be more verbose but flexible by separating
+the "join" step from the "filter" step.  The tricky part here is ensuring
+that successive instances of ``GrandparentTransformer`` use the same
+:class:`.AliasedClass` object against ``Node`` - we put it at the 
+class level here but other memoizing approaches can be used::
+
+    class GrandparentTransformer(Comparator):
+        parent_alias = aliased(Node)
+
+        @property
+        def join(self):
+            def go(q):
+                expression = self.__clause_element__()
+                return q.join(self.parent_alias, Node.parent)
+            return go
+
+        def operate(self, op, other):
+            return op(self.parent_alias.parent, other)
+
+.. sourcecode:: pycon+sql
+
+    {sql}>>> session.query(Node).\\
+    ...            with_transformation(Node.grandparent.join).\\
+    ...            filter(Node.grandparent==Node(id=5))
+    SELECT node.id AS node_id, node.parent_id AS node_parent_id 
+    FROM node JOIN node AS node_1 ON node_1.id = node.parent_id 
+    WHERE :param_1 = node_1.parent_id
+    {stop}
+
+The "transformer" pattern is an experimental pattern that starts
+to make usage of some functional programming paradigms.
+While it's only recommended for advanced and/or patient developers, 
+there's probably a whole lot of amazing things it can be used for.
+
 """
 from sqlalchemy import util
 from sqlalchemy.orm import attributes, interfaces
index 60176dfe3b5e45e6f916395012fc7c0d5c7baa45..d8f9dc691389b7e093007c3ba5aa55dc6b3d238f 100644 (file)
@@ -989,6 +989,27 @@ class Query(object):
             for opt in opts:
                 opt.process_query(self)
 
+    def with_transformation(self, fn):
+        """Return a new :class:`.Query` object transformed by
+        the given function.
+        
+        E.g.::
+        
+            def filter_something(criterion):
+                def transform(q):
+                    return q.filter(criterion)
+                return transform
+            
+            q = q.with_transformation(filter_something(x==5))
+        
+        This allows ad-hoc recipes to be created for :class:`.Query`
+        objects.  See the example at :ref:`hybrid_transformers`.
+
+        :meth:`~.Query.with_transformation` is new in SQLAlchemy 0.7.4.
+
+        """
+        return fn(self)
+
     @_generative()
     def with_hint(self, selectable, text, dialect_name='*'):
         """Add an indexing hint for the given entity or selectable to