# encoding: utf-8
# HasFullTableCache
from . import get_one
import logging
[docs]class HasFullTableCache(object):
"""A mixin class for ORM classes that maintain an in-memory cache of
(hopefully) every item in the database table for performance reasons.
"""
RESET = object()
# You MUST define your own class-specific '_cache' and '_id_cache'
# variables, like so:
#
# _cache = HasFullTableCache.RESET
# _id_cache = HasFullTableCache.RESET
[docs] @classmethod
def reset_cache(cls):
cls._cache = cls.RESET
cls._id_cache = cls.RESET
[docs] def cache_key(self):
raise NotImplementedError()
@classmethod
def _cache_insert(cls, obj, cache, id_cache):
"""Cache an object for later retrieval, possibly by a different
database session.
"""
key = obj.cache_key()
id = obj.id
try:
if cache != cls.RESET:
cache[key] = obj
if id_cache != cls.RESET:
id_cache[id] = obj
except TypeError as e:
# The cache was reset in between the time we checked for a
# reset and the time we tried to put an object in the
# cache. Stop trying to mess with the cache.
pass
[docs] @classmethod
def populate_cache(cls, _db):
"""Populate the in-memory caches from scratch with every single
object from the database table.
"""
cache = {}
id_cache = {}
for obj in _db.query(cls):
cls._cache_insert(obj, cache, id_cache)
cls._cache = cache
cls._id_cache = id_cache
@classmethod
def _cache_lookup(cls, _db, cache, cache_name, cache_key, lookup_hook):
"""Helper method used by both by_id and by_cache_key.
Looks up `cache_key` in `cache` and calls `lookup_hook`
to find/create it if it's not in there.
"""
new = False
obj = None
if cache == cls.RESET:
# The cache has been reset. Populate it with the contents
# of the table.
cls.populate_cache(_db)
# Get the new value of the cache, replacing the value
# that turned out to be cls.RESET.
cache = getattr(cls, cache_name)
if cache != cls.RESET:
try:
obj = cache.get(cache_key)
except TypeError as e:
# This shouldn't happen. Even if the actual cache was
# reset just now, we still have a copy of the 'old'
# cache which passed the 'cache != cls.RESET' test.
pass
if not obj:
# Either this object didn't exist when the cache was
# populated, or the cache was reset while we were trying
# to look it up.
#
# Give up on the cache and go direct to the database,
# creating the object if necessary.
if lookup_hook:
obj, new = lookup_hook()
else:
obj = None
if not obj:
# The object doesn't exist and couldn't be created.
return obj, new
# Stick the object in the caches, assuming they're not
# currently in a reset state.
cls._cache_insert(obj, cls._cache, cls._id_cache)
if obj and obj not in _db:
try:
obj = _db.merge(obj, load=False)
except Exception as e:
logging.error(
"Unable to merge cached object %r into database session",
obj, exc_info=e
)
# Try to look up a fresh copy of the object.
obj, new = lookup_hook()
if obj and obj in _db:
logging.error("Was able to look up a fresh copy of %r", obj)
return obj, new
# That didn't work. Re-raise the original exception.
logging.error("Unable to look up a fresh copy of %r", obj)
raise
return obj, new
[docs] @classmethod
def by_id(cls, _db, id):
"""Look up an item by its unique database ID."""
def lookup_hook():
return get_one(_db, cls, id=id), False
obj, is_new = cls._cache_lookup(
_db, cls._id_cache, '_id_cache', id, lookup_hook
)
return obj
[docs] @classmethod
def by_cache_key(cls, _db, cache_key, lookup_hook):
return cls._cache_lookup(
_db, cls._cache, '_cache', cache_key, lookup_hook
)