You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

212 lines
6.5KB

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