extensions.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. # ext/declarative/extensions.py
  2. # Copyright (C) 2005-2024 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. """Public API functions and helpers for declarative."""
  8. from ... import inspection
  9. from ... import util
  10. from ...orm import exc as orm_exc
  11. from ...orm import registry
  12. from ...orm import relationships
  13. from ...orm.base import _mapper_or_none
  14. from ...orm.clsregistry import _resolver
  15. from ...orm.decl_base import _DeferredMapperConfig
  16. from ...orm.util import polymorphic_union
  17. from ...schema import Table
  18. from ...util import OrderedDict
  19. @util.deprecated(
  20. "2.0",
  21. "the instrument_declarative function is deprecated "
  22. "and will be removed in SQLAlhcemy 2.0. Please use "
  23. ":meth:`_orm.registry.map_declaratively",
  24. )
  25. def instrument_declarative(cls, cls_registry, metadata):
  26. """Given a class, configure the class declaratively,
  27. using the given registry, which can be any dictionary, and
  28. MetaData object.
  29. """
  30. registry(metadata=metadata, class_registry=cls_registry).map_declaratively(
  31. cls
  32. )
  33. class ConcreteBase(object):
  34. """A helper class for 'concrete' declarative mappings.
  35. :class:`.ConcreteBase` will use the :func:`.polymorphic_union`
  36. function automatically, against all tables mapped as a subclass
  37. to this class. The function is called via the
  38. ``__declare_last__()`` function, which is essentially
  39. a hook for the :meth:`.after_configured` event.
  40. :class:`.ConcreteBase` produces a mapped
  41. table for the class itself. Compare to :class:`.AbstractConcreteBase`,
  42. which does not.
  43. Example::
  44. from sqlalchemy.ext.declarative import ConcreteBase
  45. class Employee(ConcreteBase, Base):
  46. __tablename__ = 'employee'
  47. employee_id = Column(Integer, primary_key=True)
  48. name = Column(String(50))
  49. __mapper_args__ = {
  50. 'polymorphic_identity':'employee',
  51. 'concrete':True}
  52. class Manager(Employee):
  53. __tablename__ = 'manager'
  54. employee_id = Column(Integer, primary_key=True)
  55. name = Column(String(50))
  56. manager_data = Column(String(40))
  57. __mapper_args__ = {
  58. 'polymorphic_identity':'manager',
  59. 'concrete':True}
  60. The name of the discriminator column used by :func:`.polymorphic_union`
  61. defaults to the name ``type``. To suit the use case of a mapping where an
  62. actual column in a mapped table is already named ``type``, the
  63. discriminator name can be configured by setting the
  64. ``_concrete_discriminator_name`` attribute::
  65. class Employee(ConcreteBase, Base):
  66. _concrete_discriminator_name = '_concrete_discriminator'
  67. .. versionadded:: 1.3.19 Added the ``_concrete_discriminator_name``
  68. attribute to :class:`_declarative.ConcreteBase` so that the
  69. virtual discriminator column name can be customized.
  70. .. versionchanged:: 1.4.2 The ``_concrete_discriminator_name`` attribute
  71. need only be placed on the basemost class to take correct effect for
  72. all subclasses. An explicit error message is now raised if the
  73. mapped column names conflict with the discriminator name, whereas
  74. in the 1.3.x series there would be some warnings and then a non-useful
  75. query would be generated.
  76. .. seealso::
  77. :class:`.AbstractConcreteBase`
  78. :ref:`concrete_inheritance`
  79. """
  80. @classmethod
  81. def _create_polymorphic_union(cls, mappers, discriminator_name):
  82. return polymorphic_union(
  83. OrderedDict(
  84. (mp.polymorphic_identity, mp.local_table) for mp in mappers
  85. ),
  86. discriminator_name,
  87. "pjoin",
  88. )
  89. @classmethod
  90. def __declare_first__(cls):
  91. m = cls.__mapper__
  92. if m.with_polymorphic:
  93. return
  94. discriminator_name = (
  95. getattr(cls, "_concrete_discriminator_name", None) or "type"
  96. )
  97. mappers = list(m.self_and_descendants)
  98. pjoin = cls._create_polymorphic_union(mappers, discriminator_name)
  99. m._set_with_polymorphic(("*", pjoin))
  100. m._set_polymorphic_on(pjoin.c[discriminator_name])
  101. class AbstractConcreteBase(ConcreteBase):
  102. """A helper class for 'concrete' declarative mappings.
  103. :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union`
  104. function automatically, against all tables mapped as a subclass
  105. to this class. The function is called via the
  106. ``__declare_last__()`` function, which is essentially
  107. a hook for the :meth:`.after_configured` event.
  108. :class:`.AbstractConcreteBase` does produce a mapped class
  109. for the base class, however it is not persisted to any table; it
  110. is instead mapped directly to the "polymorphic" selectable directly
  111. and is only used for selecting. Compare to :class:`.ConcreteBase`,
  112. which does create a persisted table for the base class.
  113. .. note::
  114. The :class:`.AbstractConcreteBase` delays the mapper creation of the
  115. base class until all the subclasses have been defined,
  116. as it needs to create a mapping against a selectable that will include
  117. all subclass tables. In order to achieve this, it waits for the
  118. **mapper configuration event** to occur, at which point it scans
  119. through all the configured subclasses and sets up a mapping that will
  120. query against all subclasses at once.
  121. While this event is normally invoked automatically, in the case of
  122. :class:`.AbstractConcreteBase`, it may be necessary to invoke it
  123. explicitly after **all** subclass mappings are defined, if the first
  124. operation is to be a query against this base class. To do so, once all
  125. the desired classes have been configured, the
  126. :meth:`_orm.registry.configure` method on the :class:`_orm.registry`
  127. in use can be invoked, which is available in relation to a particular
  128. declarative base class::
  129. Base.registry.configure()
  130. Example::
  131. from sqlalchemy.ext.declarative import AbstractConcreteBase
  132. from sqlalchemy.orm import declarative_base
  133. Base = declarative_base()
  134. class Employee(AbstractConcreteBase, Base):
  135. pass
  136. class Manager(Employee):
  137. __tablename__ = 'manager'
  138. employee_id = Column(Integer, primary_key=True)
  139. name = Column(String(50))
  140. manager_data = Column(String(40))
  141. __mapper_args__ = {
  142. 'polymorphic_identity':'manager',
  143. 'concrete':True
  144. }
  145. Base.registry.configure()
  146. The abstract base class is handled by declarative in a special way;
  147. at class configuration time, it behaves like a declarative mixin
  148. or an ``__abstract__`` base class. Once classes are configured
  149. and mappings are produced, it then gets mapped itself, but
  150. after all of its descendants. This is a very unique system of mapping
  151. not found in any other SQLAlchemy system.
  152. Using this approach, we can specify columns and properties
  153. that will take place on mapped subclasses, in the way that
  154. we normally do as in :ref:`declarative_mixins`::
  155. class Company(Base):
  156. __tablename__ = 'company'
  157. id = Column(Integer, primary_key=True)
  158. class Employee(AbstractConcreteBase, Base):
  159. employee_id = Column(Integer, primary_key=True)
  160. @declared_attr
  161. def company_id(cls):
  162. return Column(ForeignKey('company.id'))
  163. @declared_attr
  164. def company(cls):
  165. return relationship("Company")
  166. class Manager(Employee):
  167. __tablename__ = 'manager'
  168. name = Column(String(50))
  169. manager_data = Column(String(40))
  170. __mapper_args__ = {
  171. 'polymorphic_identity':'manager',
  172. 'concrete':True
  173. }
  174. Base.registry.configure()
  175. When we make use of our mappings however, both ``Manager`` and
  176. ``Employee`` will have an independently usable ``.company`` attribute::
  177. session.execute(
  178. select(Employee).filter(Employee.company.has(id=5))
  179. )
  180. .. seealso::
  181. :class:`.ConcreteBase`
  182. :ref:`concrete_inheritance`
  183. :ref:`abstract_concrete_base`
  184. """
  185. __no_table__ = True
  186. @classmethod
  187. def __declare_first__(cls):
  188. cls._sa_decl_prepare_nocascade()
  189. @classmethod
  190. def _sa_decl_prepare_nocascade(cls):
  191. if getattr(cls, "__mapper__", None):
  192. return
  193. to_map = _DeferredMapperConfig.config_for_cls(cls)
  194. # can't rely on 'self_and_descendants' here
  195. # since technically an immediate subclass
  196. # might not be mapped, but a subclass
  197. # may be.
  198. mappers = []
  199. stack = list(cls.__subclasses__())
  200. while stack:
  201. klass = stack.pop()
  202. stack.extend(klass.__subclasses__())
  203. mn = _mapper_or_none(klass)
  204. if mn is not None:
  205. mappers.append(mn)
  206. discriminator_name = (
  207. getattr(cls, "_concrete_discriminator_name", None) or "type"
  208. )
  209. pjoin = cls._create_polymorphic_union(mappers, discriminator_name)
  210. # For columns that were declared on the class, these
  211. # are normally ignored with the "__no_table__" mapping,
  212. # unless they have a different attribute key vs. col name
  213. # and are in the properties argument.
  214. # In that case, ensure we update the properties entry
  215. # to the correct column from the pjoin target table.
  216. declared_cols = set(to_map.declared_columns)
  217. for k, v in list(to_map.properties.items()):
  218. if v in declared_cols:
  219. to_map.properties[k] = pjoin.c[v.key]
  220. to_map.local_table = pjoin
  221. m_args = to_map.mapper_args_fn or dict
  222. def mapper_args():
  223. args = m_args()
  224. args["polymorphic_on"] = pjoin.c[discriminator_name]
  225. return args
  226. to_map.mapper_args_fn = mapper_args
  227. m = to_map.map()
  228. for scls in cls.__subclasses__():
  229. sm = _mapper_or_none(scls)
  230. if sm and sm.concrete and cls in scls.__bases__:
  231. sm._set_concrete_base(m)
  232. @classmethod
  233. def _sa_raise_deferred_config(cls):
  234. raise orm_exc.UnmappedClassError(
  235. cls,
  236. msg="Class %s is a subclass of AbstractConcreteBase and "
  237. "has a mapping pending until all subclasses are defined. "
  238. "Call the sqlalchemy.orm.configure_mappers() function after "
  239. "all subclasses have been defined to "
  240. "complete the mapping of this class."
  241. % orm_exc._safe_cls_name(cls),
  242. )
  243. class DeferredReflection(object):
  244. """A helper class for construction of mappings based on
  245. a deferred reflection step.
  246. Normally, declarative can be used with reflection by
  247. setting a :class:`_schema.Table` object using autoload_with=engine
  248. as the ``__table__`` attribute on a declarative class.
  249. The caveat is that the :class:`_schema.Table` must be fully
  250. reflected, or at the very least have a primary key column,
  251. at the point at which a normal declarative mapping is
  252. constructed, meaning the :class:`_engine.Engine` must be available
  253. at class declaration time.
  254. The :class:`.DeferredReflection` mixin moves the construction
  255. of mappers to be at a later point, after a specific
  256. method is called which first reflects all :class:`_schema.Table`
  257. objects created so far. Classes can define it as such::
  258. from sqlalchemy.ext.declarative import declarative_base
  259. from sqlalchemy.ext.declarative import DeferredReflection
  260. Base = declarative_base()
  261. class MyClass(DeferredReflection, Base):
  262. __tablename__ = 'mytable'
  263. Above, ``MyClass`` is not yet mapped. After a series of
  264. classes have been defined in the above fashion, all tables
  265. can be reflected and mappings created using
  266. :meth:`.prepare`::
  267. engine = create_engine("someengine://...")
  268. DeferredReflection.prepare(engine)
  269. The :class:`.DeferredReflection` mixin can be applied to individual
  270. classes, used as the base for the declarative base itself,
  271. or used in a custom abstract class. Using an abstract base
  272. allows that only a subset of classes to be prepared for a
  273. particular prepare step, which is necessary for applications
  274. that use more than one engine. For example, if an application
  275. has two engines, you might use two bases, and prepare each
  276. separately, e.g.::
  277. class ReflectedOne(DeferredReflection, Base):
  278. __abstract__ = True
  279. class ReflectedTwo(DeferredReflection, Base):
  280. __abstract__ = True
  281. class MyClass(ReflectedOne):
  282. __tablename__ = 'mytable'
  283. class MyOtherClass(ReflectedOne):
  284. __tablename__ = 'myothertable'
  285. class YetAnotherClass(ReflectedTwo):
  286. __tablename__ = 'yetanothertable'
  287. # ... etc.
  288. Above, the class hierarchies for ``ReflectedOne`` and
  289. ``ReflectedTwo`` can be configured separately::
  290. ReflectedOne.prepare(engine_one)
  291. ReflectedTwo.prepare(engine_two)
  292. .. seealso::
  293. :ref:`orm_declarative_reflected_deferred_reflection` - in the
  294. :ref:`orm_declarative_table_config_toplevel` section.
  295. """
  296. @classmethod
  297. def prepare(cls, engine):
  298. """Reflect all :class:`_schema.Table` objects for all current
  299. :class:`.DeferredReflection` subclasses"""
  300. to_map = _DeferredMapperConfig.classes_for_base(cls)
  301. with inspection.inspect(engine)._inspection_context() as insp:
  302. for thingy in to_map:
  303. cls._sa_decl_prepare(thingy.local_table, insp)
  304. thingy.map()
  305. mapper = thingy.cls.__mapper__
  306. metadata = mapper.class_.metadata
  307. for rel in mapper._props.values():
  308. if (
  309. isinstance(rel, relationships.RelationshipProperty)
  310. and rel.secondary is not None
  311. ):
  312. if isinstance(rel.secondary, Table):
  313. cls._reflect_table(rel.secondary, insp)
  314. elif isinstance(rel.secondary, str):
  315. _, resolve_arg = _resolver(rel.parent.class_, rel)
  316. rel.secondary = resolve_arg(rel.secondary)
  317. rel.secondary._resolvers += (
  318. cls._sa_deferred_table_resolver(
  319. insp, metadata
  320. ),
  321. )
  322. # controversy! do we resolve it here? or leave
  323. # it deferred? I think doing it here is necessary
  324. # so the connection does not leak.
  325. rel.secondary = rel.secondary()
  326. @classmethod
  327. def _sa_deferred_table_resolver(cls, inspector, metadata):
  328. def _resolve(key):
  329. t1 = Table(key, metadata)
  330. cls._reflect_table(t1, inspector)
  331. return t1
  332. return _resolve
  333. @classmethod
  334. def _sa_decl_prepare(cls, local_table, inspector):
  335. # autoload Table, which is already
  336. # present in the metadata. This
  337. # will fill in db-loaded columns
  338. # into the existing Table object.
  339. if local_table is not None:
  340. cls._reflect_table(local_table, inspector)
  341. @classmethod
  342. def _sa_raise_deferred_config(cls):
  343. raise orm_exc.UnmappedClassError(
  344. cls,
  345. msg="Class %s is a subclass of DeferredReflection. "
  346. "Mappings are not produced until the .prepare() "
  347. "method is called on the class hierarchy."
  348. % orm_exc._safe_cls_name(cls),
  349. )
  350. @classmethod
  351. def _reflect_table(cls, table, inspector):
  352. Table(
  353. table.name,
  354. table.metadata,
  355. extend_existing=True,
  356. autoload_replace=False,
  357. autoload_with=inspector,
  358. schema=table.schema,
  359. )