extensions.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. # ext/declarative/extensions.py
  2. # Copyright (C) 2005-2025 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. # mypy: ignore-errors
  8. """Public API functions and helpers for declarative."""
  9. from __future__ import annotations
  10. import collections
  11. import contextlib
  12. from typing import Any
  13. from typing import Callable
  14. from typing import TYPE_CHECKING
  15. from typing import Union
  16. from ... import exc as sa_exc
  17. from ...engine import Connection
  18. from ...engine import Engine
  19. from ...orm import exc as orm_exc
  20. from ...orm import relationships
  21. from ...orm.base import _mapper_or_none
  22. from ...orm.clsregistry import _resolver
  23. from ...orm.decl_base import _DeferredMapperConfig
  24. from ...orm.util import polymorphic_union
  25. from ...schema import Table
  26. from ...util import OrderedDict
  27. if TYPE_CHECKING:
  28. from ...sql.schema import MetaData
  29. class ConcreteBase:
  30. """A helper class for 'concrete' declarative mappings.
  31. :class:`.ConcreteBase` will use the :func:`.polymorphic_union`
  32. function automatically, against all tables mapped as a subclass
  33. to this class. The function is called via the
  34. ``__declare_last__()`` function, which is essentially
  35. a hook for the :meth:`.after_configured` event.
  36. :class:`.ConcreteBase` produces a mapped
  37. table for the class itself. Compare to :class:`.AbstractConcreteBase`,
  38. which does not.
  39. Example::
  40. from sqlalchemy.ext.declarative import ConcreteBase
  41. class Employee(ConcreteBase, Base):
  42. __tablename__ = "employee"
  43. employee_id = Column(Integer, primary_key=True)
  44. name = Column(String(50))
  45. __mapper_args__ = {
  46. "polymorphic_identity": "employee",
  47. "concrete": True,
  48. }
  49. class Manager(Employee):
  50. __tablename__ = "manager"
  51. employee_id = Column(Integer, primary_key=True)
  52. name = Column(String(50))
  53. manager_data = Column(String(40))
  54. __mapper_args__ = {
  55. "polymorphic_identity": "manager",
  56. "concrete": True,
  57. }
  58. The name of the discriminator column used by :func:`.polymorphic_union`
  59. defaults to the name ``type``. To suit the use case of a mapping where an
  60. actual column in a mapped table is already named ``type``, the
  61. discriminator name can be configured by setting the
  62. ``_concrete_discriminator_name`` attribute::
  63. class Employee(ConcreteBase, Base):
  64. _concrete_discriminator_name = "_concrete_discriminator"
  65. .. versionadded:: 1.3.19 Added the ``_concrete_discriminator_name``
  66. attribute to :class:`_declarative.ConcreteBase` so that the
  67. virtual discriminator column name can be customized.
  68. .. versionchanged:: 1.4.2 The ``_concrete_discriminator_name`` attribute
  69. need only be placed on the basemost class to take correct effect for
  70. all subclasses. An explicit error message is now raised if the
  71. mapped column names conflict with the discriminator name, whereas
  72. in the 1.3.x series there would be some warnings and then a non-useful
  73. query would be generated.
  74. .. seealso::
  75. :class:`.AbstractConcreteBase`
  76. :ref:`concrete_inheritance`
  77. """
  78. @classmethod
  79. def _create_polymorphic_union(cls, mappers, discriminator_name):
  80. return polymorphic_union(
  81. OrderedDict(
  82. (mp.polymorphic_identity, mp.local_table) for mp in mappers
  83. ),
  84. discriminator_name,
  85. "pjoin",
  86. )
  87. @classmethod
  88. def __declare_first__(cls):
  89. m = cls.__mapper__
  90. if m.with_polymorphic:
  91. return
  92. discriminator_name = (
  93. getattr(cls, "_concrete_discriminator_name", None) or "type"
  94. )
  95. mappers = list(m.self_and_descendants)
  96. pjoin = cls._create_polymorphic_union(mappers, discriminator_name)
  97. m._set_with_polymorphic(("*", pjoin))
  98. m._set_polymorphic_on(pjoin.c[discriminator_name])
  99. class AbstractConcreteBase(ConcreteBase):
  100. """A helper class for 'concrete' declarative mappings.
  101. :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union`
  102. function automatically, against all tables mapped as a subclass
  103. to this class. The function is called via the
  104. ``__declare_first__()`` function, which is essentially
  105. a hook for the :meth:`.before_configured` event.
  106. :class:`.AbstractConcreteBase` applies :class:`_orm.Mapper` for its
  107. immediately inheriting class, as would occur for any other
  108. declarative mapped class. However, the :class:`_orm.Mapper` is not
  109. mapped to any particular :class:`.Table` object. Instead, it's
  110. mapped directly to the "polymorphic" selectable produced by
  111. :func:`.polymorphic_union`, and performs no persistence operations on its
  112. own. Compare to :class:`.ConcreteBase`, which maps its
  113. immediately inheriting class to an actual
  114. :class:`.Table` that stores rows directly.
  115. .. note::
  116. The :class:`.AbstractConcreteBase` delays the mapper creation of the
  117. base class until all the subclasses have been defined,
  118. as it needs to create a mapping against a selectable that will include
  119. all subclass tables. In order to achieve this, it waits for the
  120. **mapper configuration event** to occur, at which point it scans
  121. through all the configured subclasses and sets up a mapping that will
  122. query against all subclasses at once.
  123. While this event is normally invoked automatically, in the case of
  124. :class:`.AbstractConcreteBase`, it may be necessary to invoke it
  125. explicitly after **all** subclass mappings are defined, if the first
  126. operation is to be a query against this base class. To do so, once all
  127. the desired classes have been configured, the
  128. :meth:`_orm.registry.configure` method on the :class:`_orm.registry`
  129. in use can be invoked, which is available in relation to a particular
  130. declarative base class::
  131. Base.registry.configure()
  132. Example::
  133. from sqlalchemy.orm import DeclarativeBase
  134. from sqlalchemy.ext.declarative import AbstractConcreteBase
  135. class Base(DeclarativeBase):
  136. pass
  137. class Employee(AbstractConcreteBase, Base):
  138. pass
  139. class Manager(Employee):
  140. __tablename__ = "manager"
  141. employee_id = Column(Integer, primary_key=True)
  142. name = Column(String(50))
  143. manager_data = Column(String(40))
  144. __mapper_args__ = {
  145. "polymorphic_identity": "manager",
  146. "concrete": True,
  147. }
  148. Base.registry.configure()
  149. The abstract base class is handled by declarative in a special way;
  150. at class configuration time, it behaves like a declarative mixin
  151. or an ``__abstract__`` base class. Once classes are configured
  152. and mappings are produced, it then gets mapped itself, but
  153. after all of its descendants. This is a very unique system of mapping
  154. not found in any other SQLAlchemy API feature.
  155. Using this approach, we can specify columns and properties
  156. that will take place on mapped subclasses, in the way that
  157. we normally do as in :ref:`declarative_mixins`::
  158. from sqlalchemy.ext.declarative import AbstractConcreteBase
  159. class Company(Base):
  160. __tablename__ = "company"
  161. id = Column(Integer, primary_key=True)
  162. class Employee(AbstractConcreteBase, Base):
  163. strict_attrs = True
  164. employee_id = Column(Integer, primary_key=True)
  165. @declared_attr
  166. def company_id(cls):
  167. return Column(ForeignKey("company.id"))
  168. @declared_attr
  169. def company(cls):
  170. return relationship("Company")
  171. class Manager(Employee):
  172. __tablename__ = "manager"
  173. name = Column(String(50))
  174. manager_data = Column(String(40))
  175. __mapper_args__ = {
  176. "polymorphic_identity": "manager",
  177. "concrete": True,
  178. }
  179. Base.registry.configure()
  180. When we make use of our mappings however, both ``Manager`` and
  181. ``Employee`` will have an independently usable ``.company`` attribute::
  182. session.execute(select(Employee).filter(Employee.company.has(id=5)))
  183. :param strict_attrs: when specified on the base class, "strict" attribute
  184. mode is enabled which attempts to limit ORM mapped attributes on the
  185. base class to only those that are immediately present, while still
  186. preserving "polymorphic" loading behavior.
  187. .. versionadded:: 2.0
  188. .. seealso::
  189. :class:`.ConcreteBase`
  190. :ref:`concrete_inheritance`
  191. :ref:`abstract_concrete_base`
  192. """
  193. __no_table__ = True
  194. @classmethod
  195. def __declare_first__(cls):
  196. cls._sa_decl_prepare_nocascade()
  197. @classmethod
  198. def _sa_decl_prepare_nocascade(cls):
  199. if getattr(cls, "__mapper__", None):
  200. return
  201. to_map = _DeferredMapperConfig.config_for_cls(cls)
  202. # can't rely on 'self_and_descendants' here
  203. # since technically an immediate subclass
  204. # might not be mapped, but a subclass
  205. # may be.
  206. mappers = []
  207. stack = list(cls.__subclasses__())
  208. while stack:
  209. klass = stack.pop()
  210. stack.extend(klass.__subclasses__())
  211. mn = _mapper_or_none(klass)
  212. if mn is not None:
  213. mappers.append(mn)
  214. discriminator_name = (
  215. getattr(cls, "_concrete_discriminator_name", None) or "type"
  216. )
  217. pjoin = cls._create_polymorphic_union(mappers, discriminator_name)
  218. # For columns that were declared on the class, these
  219. # are normally ignored with the "__no_table__" mapping,
  220. # unless they have a different attribute key vs. col name
  221. # and are in the properties argument.
  222. # In that case, ensure we update the properties entry
  223. # to the correct column from the pjoin target table.
  224. declared_cols = set(to_map.declared_columns)
  225. declared_col_keys = {c.key for c in declared_cols}
  226. for k, v in list(to_map.properties.items()):
  227. if v in declared_cols:
  228. to_map.properties[k] = pjoin.c[v.key]
  229. declared_col_keys.remove(v.key)
  230. to_map.local_table = pjoin
  231. strict_attrs = cls.__dict__.get("strict_attrs", False)
  232. m_args = to_map.mapper_args_fn or dict
  233. def mapper_args():
  234. args = m_args()
  235. args["polymorphic_on"] = pjoin.c[discriminator_name]
  236. args["polymorphic_abstract"] = True
  237. if strict_attrs:
  238. args["include_properties"] = (
  239. set(pjoin.primary_key)
  240. | declared_col_keys
  241. | {discriminator_name}
  242. )
  243. args["with_polymorphic"] = ("*", pjoin)
  244. return args
  245. to_map.mapper_args_fn = mapper_args
  246. to_map.map()
  247. stack = [cls]
  248. while stack:
  249. scls = stack.pop(0)
  250. stack.extend(scls.__subclasses__())
  251. sm = _mapper_or_none(scls)
  252. if sm and sm.concrete and sm.inherits is None:
  253. for sup_ in scls.__mro__[1:]:
  254. sup_sm = _mapper_or_none(sup_)
  255. if sup_sm:
  256. sm._set_concrete_base(sup_sm)
  257. break
  258. @classmethod
  259. def _sa_raise_deferred_config(cls):
  260. raise orm_exc.UnmappedClassError(
  261. cls,
  262. msg="Class %s is a subclass of AbstractConcreteBase and "
  263. "has a mapping pending until all subclasses are defined. "
  264. "Call the sqlalchemy.orm.configure_mappers() function after "
  265. "all subclasses have been defined to "
  266. "complete the mapping of this class."
  267. % orm_exc._safe_cls_name(cls),
  268. )
  269. class DeferredReflection:
  270. """A helper class for construction of mappings based on
  271. a deferred reflection step.
  272. Normally, declarative can be used with reflection by
  273. setting a :class:`_schema.Table` object using autoload_with=engine
  274. as the ``__table__`` attribute on a declarative class.
  275. The caveat is that the :class:`_schema.Table` must be fully
  276. reflected, or at the very least have a primary key column,
  277. at the point at which a normal declarative mapping is
  278. constructed, meaning the :class:`_engine.Engine` must be available
  279. at class declaration time.
  280. The :class:`.DeferredReflection` mixin moves the construction
  281. of mappers to be at a later point, after a specific
  282. method is called which first reflects all :class:`_schema.Table`
  283. objects created so far. Classes can define it as such::
  284. from sqlalchemy.ext.declarative import declarative_base
  285. from sqlalchemy.ext.declarative import DeferredReflection
  286. Base = declarative_base()
  287. class MyClass(DeferredReflection, Base):
  288. __tablename__ = "mytable"
  289. Above, ``MyClass`` is not yet mapped. After a series of
  290. classes have been defined in the above fashion, all tables
  291. can be reflected and mappings created using
  292. :meth:`.prepare`::
  293. engine = create_engine("someengine://...")
  294. DeferredReflection.prepare(engine)
  295. The :class:`.DeferredReflection` mixin can be applied to individual
  296. classes, used as the base for the declarative base itself,
  297. or used in a custom abstract class. Using an abstract base
  298. allows that only a subset of classes to be prepared for a
  299. particular prepare step, which is necessary for applications
  300. that use more than one engine. For example, if an application
  301. has two engines, you might use two bases, and prepare each
  302. separately, e.g.::
  303. class ReflectedOne(DeferredReflection, Base):
  304. __abstract__ = True
  305. class ReflectedTwo(DeferredReflection, Base):
  306. __abstract__ = True
  307. class MyClass(ReflectedOne):
  308. __tablename__ = "mytable"
  309. class MyOtherClass(ReflectedOne):
  310. __tablename__ = "myothertable"
  311. class YetAnotherClass(ReflectedTwo):
  312. __tablename__ = "yetanothertable"
  313. # ... etc.
  314. Above, the class hierarchies for ``ReflectedOne`` and
  315. ``ReflectedTwo`` can be configured separately::
  316. ReflectedOne.prepare(engine_one)
  317. ReflectedTwo.prepare(engine_two)
  318. .. seealso::
  319. :ref:`orm_declarative_reflected_deferred_reflection` - in the
  320. :ref:`orm_declarative_table_config_toplevel` section.
  321. """
  322. @classmethod
  323. def prepare(
  324. cls, bind: Union[Engine, Connection], **reflect_kw: Any
  325. ) -> None:
  326. r"""Reflect all :class:`_schema.Table` objects for all current
  327. :class:`.DeferredReflection` subclasses
  328. :param bind: :class:`_engine.Engine` or :class:`_engine.Connection`
  329. instance
  330. ..versionchanged:: 2.0.16 a :class:`_engine.Connection` is also
  331. accepted.
  332. :param \**reflect_kw: additional keyword arguments passed to
  333. :meth:`_schema.MetaData.reflect`, such as
  334. :paramref:`_schema.MetaData.reflect.views`.
  335. .. versionadded:: 2.0.16
  336. """
  337. to_map = _DeferredMapperConfig.classes_for_base(cls)
  338. metadata_to_table = collections.defaultdict(set)
  339. # first collect the primary __table__ for each class into a
  340. # collection of metadata/schemaname -> table names
  341. for thingy in to_map:
  342. if thingy.local_table is not None:
  343. metadata_to_table[
  344. (thingy.local_table.metadata, thingy.local_table.schema)
  345. ].add(thingy.local_table.name)
  346. # then reflect all those tables into their metadatas
  347. if isinstance(bind, Connection):
  348. conn = bind
  349. ctx = contextlib.nullcontext(enter_result=conn)
  350. elif isinstance(bind, Engine):
  351. ctx = bind.connect()
  352. else:
  353. raise sa_exc.ArgumentError(
  354. f"Expected Engine or Connection, got {bind!r}"
  355. )
  356. with ctx as conn:
  357. for (metadata, schema), table_names in metadata_to_table.items():
  358. metadata.reflect(
  359. conn,
  360. only=table_names,
  361. schema=schema,
  362. extend_existing=True,
  363. autoload_replace=False,
  364. **reflect_kw,
  365. )
  366. metadata_to_table.clear()
  367. # .map() each class, then go through relationships and look
  368. # for secondary
  369. for thingy in to_map:
  370. thingy.map()
  371. mapper = thingy.cls.__mapper__
  372. metadata = mapper.class_.metadata
  373. for rel in mapper._props.values():
  374. if (
  375. isinstance(rel, relationships.RelationshipProperty)
  376. and rel._init_args.secondary._is_populated()
  377. ):
  378. secondary_arg = rel._init_args.secondary
  379. if isinstance(secondary_arg.argument, Table):
  380. secondary_table = secondary_arg.argument
  381. metadata_to_table[
  382. (
  383. secondary_table.metadata,
  384. secondary_table.schema,
  385. )
  386. ].add(secondary_table.name)
  387. elif isinstance(secondary_arg.argument, str):
  388. _, resolve_arg = _resolver(rel.parent.class_, rel)
  389. resolver = resolve_arg(
  390. secondary_arg.argument, True
  391. )
  392. metadata_to_table[
  393. (metadata, thingy.local_table.schema)
  394. ].add(secondary_arg.argument)
  395. resolver._resolvers += (
  396. cls._sa_deferred_table_resolver(metadata),
  397. )
  398. secondary_arg.argument = resolver()
  399. for (metadata, schema), table_names in metadata_to_table.items():
  400. metadata.reflect(
  401. conn,
  402. only=table_names,
  403. schema=schema,
  404. extend_existing=True,
  405. autoload_replace=False,
  406. )
  407. @classmethod
  408. def _sa_deferred_table_resolver(
  409. cls, metadata: MetaData
  410. ) -> Callable[[str], Table]:
  411. def _resolve(key: str) -> Table:
  412. # reflection has already occurred so this Table would have
  413. # its contents already
  414. return Table(key, metadata)
  415. return _resolve
  416. _sa_decl_prepare = True
  417. @classmethod
  418. def _sa_raise_deferred_config(cls):
  419. raise orm_exc.UnmappedClassError(
  420. cls,
  421. msg="Class %s is a subclass of DeferredReflection. "
  422. "Mappings are not produced until the .prepare() "
  423. "method is called on the class hierarchy."
  424. % orm_exc._safe_cls_name(cls),
  425. )