scoping.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. # orm/scoping.py
  2. # Copyright (C) 2005-2022 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: https://www.opensource.org/licenses/mit-license.php
  7. from . import class_mapper
  8. from . import exc as orm_exc
  9. from .session import Session
  10. from .. import exc as sa_exc
  11. from ..util import create_proxy_methods
  12. from ..util import ScopedRegistry
  13. from ..util import ThreadLocalRegistry
  14. from ..util import warn
  15. from ..util import warn_deprecated
  16. __all__ = ["scoped_session", "ScopedSessionMixin"]
  17. class ScopedSessionMixin(object):
  18. @property
  19. def _proxied(self):
  20. return self.registry()
  21. def __call__(self, **kw):
  22. r"""Return the current :class:`.Session`, creating it
  23. using the :attr:`.scoped_session.session_factory` if not present.
  24. :param \**kw: Keyword arguments will be passed to the
  25. :attr:`.scoped_session.session_factory` callable, if an existing
  26. :class:`.Session` is not present. If the :class:`.Session` is present
  27. and keyword arguments have been passed,
  28. :exc:`~sqlalchemy.exc.InvalidRequestError` is raised.
  29. """
  30. if kw:
  31. if self.registry.has():
  32. raise sa_exc.InvalidRequestError(
  33. "Scoped session is already present; "
  34. "no new arguments may be specified."
  35. )
  36. else:
  37. sess = self.session_factory(**kw)
  38. self.registry.set(sess)
  39. else:
  40. sess = self.registry()
  41. if not self._support_async and sess._is_asyncio:
  42. warn_deprecated(
  43. "Using `scoped_session` with asyncio is deprecated and "
  44. "will raise an error in a future version. "
  45. "Please use `async_scoped_session` instead.",
  46. "1.4.23",
  47. )
  48. return sess
  49. def configure(self, **kwargs):
  50. """reconfigure the :class:`.sessionmaker` used by this
  51. :class:`.scoped_session`.
  52. See :meth:`.sessionmaker.configure`.
  53. """
  54. if self.registry.has():
  55. warn(
  56. "At least one scoped session is already present. "
  57. " configure() can not affect sessions that have "
  58. "already been created."
  59. )
  60. self.session_factory.configure(**kwargs)
  61. @create_proxy_methods(
  62. Session,
  63. ":class:`_orm.Session`",
  64. ":class:`_orm.scoping.scoped_session`",
  65. classmethods=["close_all", "object_session", "identity_key"],
  66. methods=[
  67. "__contains__",
  68. "__iter__",
  69. "add",
  70. "add_all",
  71. "begin",
  72. "begin_nested",
  73. "close",
  74. "commit",
  75. "connection",
  76. "delete",
  77. "execute",
  78. "expire",
  79. "expire_all",
  80. "expunge",
  81. "expunge_all",
  82. "flush",
  83. "get",
  84. "get_bind",
  85. "is_modified",
  86. "bulk_save_objects",
  87. "bulk_insert_mappings",
  88. "bulk_update_mappings",
  89. "merge",
  90. "query",
  91. "refresh",
  92. "rollback",
  93. "scalar",
  94. "scalars",
  95. ],
  96. attributes=[
  97. "bind",
  98. "dirty",
  99. "deleted",
  100. "new",
  101. "identity_map",
  102. "is_active",
  103. "autoflush",
  104. "no_autoflush",
  105. "info",
  106. "autocommit",
  107. ],
  108. )
  109. class scoped_session(ScopedSessionMixin):
  110. """Provides scoped management of :class:`.Session` objects.
  111. See :ref:`unitofwork_contextual` for a tutorial.
  112. .. note::
  113. When using :ref:`asyncio_toplevel`, the async-compatible
  114. :class:`_asyncio.async_scoped_session` class should be
  115. used in place of :class:`.scoped_session`.
  116. """
  117. _support_async = False
  118. session_factory = None
  119. """The `session_factory` provided to `__init__` is stored in this
  120. attribute and may be accessed at a later time. This can be useful when
  121. a new non-scoped :class:`.Session` or :class:`_engine.Connection` to the
  122. database is needed."""
  123. def __init__(self, session_factory, scopefunc=None):
  124. """Construct a new :class:`.scoped_session`.
  125. :param session_factory: a factory to create new :class:`.Session`
  126. instances. This is usually, but not necessarily, an instance
  127. of :class:`.sessionmaker`.
  128. :param scopefunc: optional function which defines
  129. the current scope. If not passed, the :class:`.scoped_session`
  130. object assumes "thread-local" scope, and will use
  131. a Python ``threading.local()`` in order to maintain the current
  132. :class:`.Session`. If passed, the function should return
  133. a hashable token; this token will be used as the key in a
  134. dictionary in order to store and retrieve the current
  135. :class:`.Session`.
  136. """
  137. self.session_factory = session_factory
  138. if scopefunc:
  139. self.registry = ScopedRegistry(session_factory, scopefunc)
  140. else:
  141. self.registry = ThreadLocalRegistry(session_factory)
  142. def remove(self):
  143. """Dispose of the current :class:`.Session`, if present.
  144. This will first call :meth:`.Session.close` method
  145. on the current :class:`.Session`, which releases any existing
  146. transactional/connection resources still being held; transactions
  147. specifically are rolled back. The :class:`.Session` is then
  148. discarded. Upon next usage within the same scope,
  149. the :class:`.scoped_session` will produce a new
  150. :class:`.Session` object.
  151. """
  152. if self.registry.has():
  153. self.registry().close()
  154. self.registry.clear()
  155. def query_property(self, query_cls=None):
  156. """return a class property which produces a :class:`_query.Query`
  157. object
  158. against the class and the current :class:`.Session` when called.
  159. e.g.::
  160. Session = scoped_session(sessionmaker())
  161. class MyClass(object):
  162. query = Session.query_property()
  163. # after mappers are defined
  164. result = MyClass.query.filter(MyClass.name=='foo').all()
  165. Produces instances of the session's configured query class by
  166. default. To override and use a custom implementation, provide
  167. a ``query_cls`` callable. The callable will be invoked with
  168. the class's mapper as a positional argument and a session
  169. keyword argument.
  170. There is no limit to the number of query properties placed on
  171. a class.
  172. """
  173. class query(object):
  174. def __get__(s, instance, owner):
  175. try:
  176. mapper = class_mapper(owner)
  177. if mapper:
  178. if query_cls:
  179. # custom query class
  180. return query_cls(mapper, session=self.registry())
  181. else:
  182. # session's configured query class
  183. return self.registry().query(mapper)
  184. except orm_exc.UnmappedClassError:
  185. return None
  186. return query()
  187. ScopedSession = scoped_session
  188. """Old name for backwards compatibility."""