]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
new example - apply Beaker caching to a relation().
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 8 Jan 2010 19:53:17 +0000 (19:53 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 8 Jan 2010 19:53:17 +0000 (19:53 +0000)
examples/query_caching/per_relation.py [new file with mode: 0644]

diff --git a/examples/query_caching/per_relation.py b/examples/query_caching/per_relation.py
new file mode 100644 (file)
index 0000000..6e0df46
--- /dev/null
@@ -0,0 +1,176 @@
+"""
+Ready for some really powerful stuff ?
+
+We're going to use Beaker caching, and create functions that load and cache what we want, which
+can be used in any scenario.  Then we're going to associate them with many-to-one relations
+for individual queries.  
+
+Think of it as lazy loading from a long term cache.  For rarely-mutated objects, this is a super
+performing way to go.
+
+"""
+
+from sqlalchemy.orm.query import Query, _generative
+from sqlalchemy.orm.interfaces import MapperOption
+from sqlalchemy.orm.session import Session
+from sqlalchemy.sql import visitors
+
+class CachingQuery(Query):
+    """override __iter__ to pull results from a callable 
+       that might have been attached to the Query.
+        
+    """
+    def __iter__(self):
+        if hasattr(self, 'cache_callable'):
+            try:
+                ret = self.cache_callable(self)
+            except KeyError:
+                ret = list(Query.__iter__(self))
+                for x in ret:
+                    self.session.expunge(x)
+
+            return iter(self.session.merge(x, dont_load=True) for x in ret)
+
+        else:
+            return Query.__iter__(self)
+
+class FromCallable(MapperOption):
+    """A MapperOption that associates a callable with particular 'path' load.
+    
+    When a lazyload occurs, the Query has a "path" which is a tuple of
+    (mapper, key, mapper, key) indicating the path along relations from
+    the original mapper to the endpoint mapper.
+    
+    """
+    
+    propagate_to_loaders = True
+    
+    def __init__(self, key):
+        self.cls_ = key.property.parent.class_
+        self.propname = key.property.key
+    
+    def __call__(self, q):
+        raise NotImplementedError()
+        
+    def process_query(self, query):
+        if query._current_path:
+            mapper, key = query._current_path[-2:]
+            if mapper.class_ is self.cls_ and key == self.propname:
+                query.cache_callable = self
+
+def params_from_query(query):
+    """Pull the bind parameter values from a query.
+    
+    This takes into account any scalar attribute bindparam set up.
+    
+    E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
+    would return [5, 7].
+    
+    
+    """
+    
+    v = []
+    def visit_bindparam(bind):
+        value = query._params.get(bind.key, bind.value)
+        v.append(value)
+    visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
+    return v
+                
+if __name__ == '__main__':
+    """Usage example.  We'll use Beaker to set up a region and then a short def
+    that loads a 'Widget' object by id.
+    
+    """
+    from sqlalchemy.orm import sessionmaker, scoped_session
+    from sqlalchemy import create_engine
+    
+    # beaker 1.4 or above
+    from beaker.cache import CacheManager 
+
+    # sample CacheManager.   In reality, use memcached.   (seriously, don't bother
+    # with any other backend.)
+    cache_manager = CacheManager(
+                        cache_regions={
+                            'default_region':{'type':'memory','expire':3600}
+                        }
+                    )
+
+    # SQLA configuration
+    engine=create_engine('sqlite://', echo=True)
+    Session = scoped_session(sessionmaker(query_cls=CachingQuery, bind=engine))
+    
+    from sqlalchemy import Column, Integer, String, ForeignKey
+    from sqlalchemy.orm import relation
+    from sqlalchemy.ext.declarative import declarative_base
+
+    # mappings
+    Base = declarative_base()
+    
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(100))
+        widget_id = Column(Integer, ForeignKey('widget.id'))
+        
+        widget = relation("Widget")
+
+    class Widget(Base):
+        __tablename__ = 'widget'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(100))
+
+    # Widget loading.
+    
+    @cache_manager.region('default_region', 'byid')
+    def load_widget(widget_id):
+        """Load a widget by id, caching the result in Beaker."""
+        
+        return Session.query(Widget).filter(Widget.id==widget_id).first()
+
+    class CachedWidget(FromCallable):
+        """A MapperOption that will pull user widget links from Beaker.
+        
+        We build a subclass of FromCallable with a __call__ method
+        so that the option itself is pickleable.
+        
+        """
+        def __call__(self, q):
+            return [load_widget(*params_from_query(q))]
+
+    Base.metadata.create_all(engine)
+    
+    sess = Session()
+    
+    # create data.
+    w1 = Widget(name='w1')
+    w2 = Widget(name='w2')
+    sess.add_all(
+        [User(name='u1', widget=w1), User(name='u2', widget=w1), User(name='u3', widget=w2)]
+    )
+    sess.commit()
+    
+    # call load_widget with 1 and 2.  this will cache those widget objects in beaker.
+    w1 = load_widget(1)
+    w2 = load_widget(2)
+
+    # clear session entirely.
+    sess.expunge_all()
+    
+    # load users, sending over our option.
+    u1, u2, u3 = sess.query(User).options(CachedWidget(User.widget)).order_by(User.id).all()
+    
+    print "------------------------"
+
+    # access the "Widget".   No SQL occurs below this line !
+    assert u1.widget.name == 'w1'
+
+    # access the same "Widget" on u2.  Local w1 is reused, no extra cache roundtrip !
+    assert u2.widget.name == 'w1'
+    assert u2.widget is u1.widget
+    
+    assert u3.widget.name == 'w2'
+
+    # user + the option (embedded in its state)
+    # are pickleable themselves (important for further caching)
+    import pickle
+    assert pickle.dumps(u1)
\ No newline at end of file