baked.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. # sqlalchemy/ext/baked.py
  2. # Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """Baked query extension.
  8. Provides a creational pattern for the :class:`.query.Query` object which
  9. allows the fully constructed object, Core select statement, and string
  10. compiled result to be fully cached.
  11. """
  12. from ..orm.query import Query
  13. from ..orm import strategies, attributes, properties, \
  14. strategy_options, util as orm_util, interfaces
  15. from .. import log as sqla_log
  16. from ..sql import util as sql_util, func, literal_column
  17. from ..orm import exc as orm_exc
  18. from .. import exc as sa_exc
  19. from .. import util
  20. import copy
  21. import logging
  22. log = logging.getLogger(__name__)
  23. class BakedQuery(object):
  24. """A builder object for :class:`.query.Query` objects."""
  25. __slots__ = 'steps', '_bakery', '_cache_key', '_spoiled'
  26. def __init__(self, bakery, initial_fn, args=()):
  27. self._cache_key = ()
  28. self._update_cache_key(initial_fn, args)
  29. self.steps = [initial_fn]
  30. self._spoiled = False
  31. self._bakery = bakery
  32. @classmethod
  33. def bakery(cls, size=200):
  34. """Construct a new bakery."""
  35. _bakery = util.LRUCache(size)
  36. def call(initial_fn, *args):
  37. return cls(_bakery, initial_fn, args)
  38. return call
  39. def _clone(self):
  40. b1 = BakedQuery.__new__(BakedQuery)
  41. b1._cache_key = self._cache_key
  42. b1.steps = list(self.steps)
  43. b1._bakery = self._bakery
  44. b1._spoiled = self._spoiled
  45. return b1
  46. def _update_cache_key(self, fn, args=()):
  47. self._cache_key += (fn.__code__,) + args
  48. def __iadd__(self, other):
  49. if isinstance(other, tuple):
  50. self.add_criteria(*other)
  51. else:
  52. self.add_criteria(other)
  53. return self
  54. def __add__(self, other):
  55. if isinstance(other, tuple):
  56. return self.with_criteria(*other)
  57. else:
  58. return self.with_criteria(other)
  59. def add_criteria(self, fn, *args):
  60. """Add a criteria function to this :class:`.BakedQuery`.
  61. This is equivalent to using the ``+=`` operator to
  62. modify a :class:`.BakedQuery` in-place.
  63. """
  64. self._update_cache_key(fn, args)
  65. self.steps.append(fn)
  66. return self
  67. def with_criteria(self, fn, *args):
  68. """Add a criteria function to a :class:`.BakedQuery` cloned from this one.
  69. This is equivalent to using the ``+`` operator to
  70. produce a new :class:`.BakedQuery` with modifications.
  71. """
  72. return self._clone().add_criteria(fn, *args)
  73. def for_session(self, session):
  74. """Return a :class:`.Result` object for this :class:`.BakedQuery`.
  75. This is equivalent to calling the :class:`.BakedQuery` as a
  76. Python callable, e.g. ``result = my_baked_query(session)``.
  77. """
  78. return Result(self, session)
  79. def __call__(self, session):
  80. return self.for_session(session)
  81. def spoil(self, full=False):
  82. """Cancel any query caching that will occur on this BakedQuery object.
  83. The BakedQuery can continue to be used normally, however additional
  84. creational functions will not be cached; they will be called
  85. on every invocation.
  86. This is to support the case where a particular step in constructing
  87. a baked query disqualifies the query from being cacheable, such
  88. as a variant that relies upon some uncacheable value.
  89. :param full: if False, only functions added to this
  90. :class:`.BakedQuery` object subsequent to the spoil step will be
  91. non-cached; the state of the :class:`.BakedQuery` up until
  92. this point will be pulled from the cache. If True, then the
  93. entire :class:`.Query` object is built from scratch each
  94. time, with all creational functions being called on each
  95. invocation.
  96. """
  97. if not full:
  98. _spoil_point = self._clone()
  99. _spoil_point._cache_key += ('_query_only', )
  100. self.steps = [_spoil_point._retrieve_baked_query]
  101. self._spoiled = True
  102. return self
  103. def _retrieve_baked_query(self, session):
  104. query = self._bakery.get(self._cache_key, None)
  105. if query is None:
  106. query = self._as_query(session)
  107. self._bakery[self._cache_key] = query.with_session(None)
  108. return query.with_session(session)
  109. def _bake(self, session):
  110. query = self._as_query(session)
  111. context = query._compile_context()
  112. self._bake_subquery_loaders(session, context)
  113. context.session = None
  114. context.query = query = context.query.with_session(None)
  115. query._execution_options = query._execution_options.union(
  116. {"compiled_cache": self._bakery}
  117. )
  118. # we'll be holding onto the query for some of its state,
  119. # so delete some compilation-use-only attributes that can take up
  120. # space
  121. for attr in (
  122. '_correlate', '_from_obj', '_mapper_adapter_map',
  123. '_joinpath', '_joinpoint'):
  124. query.__dict__.pop(attr, None)
  125. self._bakery[self._cache_key] = context
  126. return context
  127. def _as_query(self, session):
  128. query = self.steps[0](session)
  129. for step in self.steps[1:]:
  130. query = step(query)
  131. return query
  132. def _bake_subquery_loaders(self, session, context):
  133. """convert subquery eager loaders in the cache into baked queries.
  134. For subquery eager loading to work, all we need here is that the
  135. Query point to the correct session when it is run. However, since
  136. we are "baking" anyway, we may as well also turn the query into
  137. a "baked" query so that we save on performance too.
  138. """
  139. context.attributes['baked_queries'] = baked_queries = []
  140. for k, v in list(context.attributes.items()):
  141. if isinstance(v, Query):
  142. if 'subquery' in k:
  143. bk = BakedQuery(self._bakery, lambda *args: v)
  144. bk._cache_key = self._cache_key + k
  145. bk._bake(session)
  146. baked_queries.append((k, bk._cache_key, v))
  147. del context.attributes[k]
  148. def _unbake_subquery_loaders(self, session, context, params):
  149. """Retrieve subquery eager loaders stored by _bake_subquery_loaders
  150. and turn them back into Result objects that will iterate just
  151. like a Query object.
  152. """
  153. for k, cache_key, query in context.attributes["baked_queries"]:
  154. bk = BakedQuery(self._bakery,
  155. lambda sess, q=query: q.with_session(sess))
  156. bk._cache_key = cache_key
  157. context.attributes[k] = bk.for_session(session).params(**params)
  158. class Result(object):
  159. """Invokes a :class:`.BakedQuery` against a :class:`.Session`.
  160. The :class:`.Result` object is where the actual :class:`.query.Query`
  161. object gets created, or retrieved from the cache,
  162. against a target :class:`.Session`, and is then invoked for results.
  163. """
  164. __slots__ = 'bq', 'session', '_params'
  165. def __init__(self, bq, session):
  166. self.bq = bq
  167. self.session = session
  168. self._params = {}
  169. def params(self, *args, **kw):
  170. """Specify parameters to be replaced into the string SQL statement."""
  171. if len(args) == 1:
  172. kw.update(args[0])
  173. elif len(args) > 0:
  174. raise sa_exc.ArgumentError(
  175. "params() takes zero or one positional argument, "
  176. "which is a dictionary.")
  177. self._params.update(kw)
  178. return self
  179. def _as_query(self):
  180. return self.bq._as_query(self.session).params(self._params)
  181. def __str__(self):
  182. return str(self._as_query())
  183. def __iter__(self):
  184. bq = self.bq
  185. if bq._spoiled:
  186. return iter(self._as_query())
  187. baked_context = bq._bakery.get(bq._cache_key, None)
  188. if baked_context is None:
  189. baked_context = bq._bake(self.session)
  190. context = copy.copy(baked_context)
  191. context.session = self.session
  192. context.attributes = context.attributes.copy()
  193. bq._unbake_subquery_loaders(self.session, context, self._params)
  194. context.statement.use_labels = True
  195. if context.autoflush and not context.populate_existing:
  196. self.session._autoflush()
  197. return context.query.params(self._params).\
  198. with_session(self.session)._execute_and_instances(context)
  199. def count(self):
  200. """return the 'count'.
  201. Equivalent to :meth:`.Query.count`.
  202. Note this uses a subquery to ensure an accurate count regardless
  203. of the structure of the original statement.
  204. .. versionadded:: 1.1.6
  205. """
  206. col = func.count(literal_column('*'))
  207. bq = self.bq.with_criteria(lambda q: q.from_self(col))
  208. return bq.for_session(self.session).params(self._params).scalar()
  209. def scalar(self):
  210. """Return the first element of the first result or None
  211. if no rows present. If multiple rows are returned,
  212. raises MultipleResultsFound.
  213. Equivalent to :meth:`.Query.scalar`.
  214. .. versionadded:: 1.1.6
  215. """
  216. try:
  217. ret = self.one()
  218. if not isinstance(ret, tuple):
  219. return ret
  220. return ret[0]
  221. except orm_exc.NoResultFound:
  222. return None
  223. def first(self):
  224. """Return the first row.
  225. Equivalent to :meth:`.Query.first`.
  226. """
  227. bq = self.bq.with_criteria(lambda q: q.slice(0, 1))
  228. ret = list(bq.for_session(self.session).params(self._params))
  229. if len(ret) > 0:
  230. return ret[0]
  231. else:
  232. return None
  233. def one(self):
  234. """Return exactly one result or raise an exception.
  235. Equivalent to :meth:`.Query.one`.
  236. """
  237. try:
  238. ret = self.one_or_none()
  239. except orm_exc.MultipleResultsFound:
  240. raise orm_exc.MultipleResultsFound(
  241. "Multiple rows were found for one()")
  242. else:
  243. if ret is None:
  244. raise orm_exc.NoResultFound("No row was found for one()")
  245. return ret
  246. def one_or_none(self):
  247. """Return one or zero results, or raise an exception for multiple
  248. rows.
  249. Equivalent to :meth:`.Query.one_or_none`.
  250. .. versionadded:: 1.0.9
  251. """
  252. ret = list(self)
  253. l = len(ret)
  254. if l == 1:
  255. return ret[0]
  256. elif l == 0:
  257. return None
  258. else:
  259. raise orm_exc.MultipleResultsFound(
  260. "Multiple rows were found for one_or_none()")
  261. def all(self):
  262. """Return all rows.
  263. Equivalent to :meth:`.Query.all`.
  264. """
  265. return list(self)
  266. def get(self, ident):
  267. """Retrieve an object based on identity.
  268. Equivalent to :meth:`.Query.get`.
  269. """
  270. query = self.bq.steps[0](self.session)
  271. return query._get_impl(ident, self._load_on_ident)
  272. def _load_on_ident(self, query, key):
  273. """Load the given identity key from the database."""
  274. ident = key[1]
  275. mapper = query._mapper_zero()
  276. _get_clause, _get_params = mapper._get_clause
  277. def setup(query):
  278. _lcl_get_clause = _get_clause
  279. q = query._clone()
  280. q._get_condition()
  281. q._order_by = None
  282. # None present in ident - turn those comparisons
  283. # into "IS NULL"
  284. if None in ident:
  285. nones = set([
  286. _get_params[col].key for col, value in
  287. zip(mapper.primary_key, ident) if value is None
  288. ])
  289. _lcl_get_clause = sql_util.adapt_criterion_to_null(
  290. _lcl_get_clause, nones)
  291. _lcl_get_clause = q._adapt_clause(_lcl_get_clause, True, False)
  292. q._criterion = _lcl_get_clause
  293. return q
  294. # cache the query against a key that includes
  295. # which positions in the primary key are NULL
  296. # (remember, we can map to an OUTER JOIN)
  297. bq = self.bq
  298. # add the clause we got from mapper._get_clause to the cache
  299. # key so that if a race causes multiple calls to _get_clause,
  300. # we've cached on ours
  301. bq = bq._clone()
  302. bq._cache_key += (_get_clause, )
  303. bq = bq.with_criteria(setup, tuple(elem is None for elem in ident))
  304. params = dict([
  305. (_get_params[primary_key].key, id_val)
  306. for id_val, primary_key in zip(ident, mapper.primary_key)
  307. ])
  308. result = list(bq.for_session(self.session).params(**params))
  309. l = len(result)
  310. if l > 1:
  311. raise orm_exc.MultipleResultsFound()
  312. elif l:
  313. return result[0]
  314. else:
  315. return None
  316. def bake_lazy_loaders():
  317. """Enable the use of baked queries for all lazyloaders systemwide.
  318. This operation should be safe for all lazy loaders, and will reduce
  319. Python overhead for these operations.
  320. """
  321. BakedLazyLoader._strategy_keys[:] = []
  322. properties.RelationshipProperty.strategy_for(
  323. lazy="select")(BakedLazyLoader)
  324. properties.RelationshipProperty.strategy_for(
  325. lazy=True)(BakedLazyLoader)
  326. properties.RelationshipProperty.strategy_for(
  327. lazy="baked_select")(BakedLazyLoader)
  328. strategies.LazyLoader._strategy_keys[:] = BakedLazyLoader._strategy_keys[:]
  329. def unbake_lazy_loaders():
  330. """Disable the use of baked queries for all lazyloaders systemwide.
  331. This operation reverts the changes produced by :func:`.bake_lazy_loaders`.
  332. """
  333. strategies.LazyLoader._strategy_keys[:] = []
  334. BakedLazyLoader._strategy_keys[:] = []
  335. properties.RelationshipProperty.strategy_for(
  336. lazy="select")(strategies.LazyLoader)
  337. properties.RelationshipProperty.strategy_for(
  338. lazy=True)(strategies.LazyLoader)
  339. properties.RelationshipProperty.strategy_for(
  340. lazy="baked_select")(BakedLazyLoader)
  341. assert strategies.LazyLoader._strategy_keys
  342. @sqla_log.class_logger
  343. @properties.RelationshipProperty.strategy_for(lazy="baked_select")
  344. class BakedLazyLoader(strategies.LazyLoader):
  345. def _emit_lazyload(self, session, state, ident_key, passive):
  346. q = BakedQuery(
  347. self.mapper._compiled_cache,
  348. lambda session: session.query(self.mapper))
  349. q.add_criteria(
  350. lambda q: q._adapt_all_clauses()._with_invoke_all_eagers(False),
  351. self.parent_property)
  352. if not self.parent_property.bake_queries:
  353. q.spoil(full=True)
  354. if self.parent_property.secondary is not None:
  355. q.add_criteria(
  356. lambda q:
  357. q.select_from(self.mapper, self.parent_property.secondary))
  358. pending = not state.key
  359. # don't autoflush on pending
  360. if pending or passive & attributes.NO_AUTOFLUSH:
  361. q.add_criteria(lambda q: q.autoflush(False))
  362. if state.load_options:
  363. q.spoil()
  364. args = state.load_path[self.parent_property]
  365. q.add_criteria(
  366. lambda q:
  367. q._with_current_path(args), args)
  368. q.add_criteria(
  369. lambda q: q._conditional_options(*state.load_options))
  370. if self.use_get:
  371. return q(session)._load_on_ident(
  372. session.query(self.mapper), ident_key)
  373. if self.parent_property.order_by:
  374. q.add_criteria(
  375. lambda q:
  376. q.order_by(*util.to_list(self.parent_property.order_by)))
  377. for rev in self.parent_property._reverse_property:
  378. # reverse props that are MANYTOONE are loading *this*
  379. # object from get(), so don't need to eager out to those.
  380. if rev.direction is interfaces.MANYTOONE and \
  381. rev._use_get and \
  382. not isinstance(rev.strategy, strategies.LazyLoader):
  383. q.add_criteria(
  384. lambda q:
  385. q.options(
  386. strategy_options.Load.for_existing_path(
  387. q._current_path[rev.parent]
  388. ).baked_lazyload(rev.key)
  389. )
  390. )
  391. lazy_clause, params = self._generate_lazy_clause(state, passive)
  392. if pending:
  393. if orm_util._none_set.intersection(params.values()):
  394. return None
  395. q.add_criteria(lambda q: q.filter(lazy_clause))
  396. result = q(session).params(**params).all()
  397. if self.uselist:
  398. return result
  399. else:
  400. l = len(result)
  401. if l:
  402. if l > 1:
  403. util.warn(
  404. "Multiple rows returned with "
  405. "uselist=False for lazily-loaded attribute '%s' "
  406. % self.parent_property)
  407. return result[0]
  408. else:
  409. return None
  410. @strategy_options.loader_option()
  411. def baked_lazyload(loadopt, attr):
  412. """Indicate that the given attribute should be loaded using "lazy"
  413. loading with a "baked" query used in the load.
  414. """
  415. return loadopt.set_relationship_strategy(attr, {"lazy": "baked_select"})
  416. @baked_lazyload._add_unbound_fn
  417. def baked_lazyload(*keys):
  418. return strategy_options._UnboundLoad._from_keys(
  419. strategy_options._UnboundLoad.baked_lazyload, keys, False, {})
  420. @baked_lazyload._add_unbound_all_fn
  421. def baked_lazyload_all(*keys):
  422. return strategy_options._UnboundLoad._from_keys(
  423. strategy_options._UnboundLoad.baked_lazyload, keys, True, {})
  424. baked_lazyload = baked_lazyload._unbound_fn
  425. baked_lazyload_all = baked_lazyload_all._unbound_all_fn
  426. bakery = BakedQuery.bakery