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.

1205 lines
41KB

  1. # ext/hybrid.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. r"""Define attributes on ORM-mapped classes that have "hybrid" behavior.
  8. "hybrid" means the attribute has distinct behaviors defined at the
  9. class level and at the instance level.
  10. The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of
  11. method decorator, is around 50 lines of code and has almost no
  12. dependencies on the rest of SQLAlchemy. It can, in theory, work with
  13. any descriptor-based expression system.
  14. Consider a mapping ``Interval``, representing integer ``start`` and ``end``
  15. values. We can define higher level functions on mapped classes that produce SQL
  16. expressions at the class level, and Python expression evaluation at the
  17. instance level. Below, each function decorated with :class:`.hybrid_method` or
  18. :class:`.hybrid_property` may receive ``self`` as an instance of the class, or
  19. as the class itself::
  20. from sqlalchemy import Column, Integer
  21. from sqlalchemy.ext.declarative import declarative_base
  22. from sqlalchemy.orm import Session, aliased
  23. from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
  24. Base = declarative_base()
  25. class Interval(Base):
  26. __tablename__ = 'interval'
  27. id = Column(Integer, primary_key=True)
  28. start = Column(Integer, nullable=False)
  29. end = Column(Integer, nullable=False)
  30. def __init__(self, start, end):
  31. self.start = start
  32. self.end = end
  33. @hybrid_property
  34. def length(self):
  35. return self.end - self.start
  36. @hybrid_method
  37. def contains(self, point):
  38. return (self.start <= point) & (point <= self.end)
  39. @hybrid_method
  40. def intersects(self, other):
  41. return self.contains(other.start) | self.contains(other.end)
  42. Above, the ``length`` property returns the difference between the
  43. ``end`` and ``start`` attributes. With an instance of ``Interval``,
  44. this subtraction occurs in Python, using normal Python descriptor
  45. mechanics::
  46. >>> i1 = Interval(5, 10)
  47. >>> i1.length
  48. 5
  49. When dealing with the ``Interval`` class itself, the :class:`.hybrid_property`
  50. descriptor evaluates the function body given the ``Interval`` class as
  51. the argument, which when evaluated with SQLAlchemy expression mechanics
  52. (here using the :attr:`.QueryableAttribute.expression` accessor)
  53. returns a new SQL expression::
  54. >>> print(Interval.length.expression)
  55. interval."end" - interval.start
  56. >>> print(Session().query(Interval).filter(Interval.length > 10))
  57. SELECT interval.id AS interval_id, interval.start AS interval_start,
  58. interval."end" AS interval_end
  59. FROM interval
  60. WHERE interval."end" - interval.start > :param_1
  61. ORM methods such as :meth:`_query.Query.filter_by`
  62. generally use ``getattr()`` to
  63. locate attributes, so can also be used with hybrid attributes::
  64. >>> print(Session().query(Interval).filter_by(length=5))
  65. SELECT interval.id AS interval_id, interval.start AS interval_start,
  66. interval."end" AS interval_end
  67. FROM interval
  68. WHERE interval."end" - interval.start = :param_1
  69. The ``Interval`` class example also illustrates two methods,
  70. ``contains()`` and ``intersects()``, decorated with
  71. :class:`.hybrid_method`. This decorator applies the same idea to
  72. methods that :class:`.hybrid_property` applies to attributes. The
  73. methods return boolean values, and take advantage of the Python ``|``
  74. and ``&`` bitwise operators to produce equivalent instance-level and
  75. SQL expression-level boolean behavior::
  76. >>> i1.contains(6)
  77. True
  78. >>> i1.contains(15)
  79. False
  80. >>> i1.intersects(Interval(7, 18))
  81. True
  82. >>> i1.intersects(Interval(25, 29))
  83. False
  84. >>> print(Session().query(Interval).filter(Interval.contains(15)))
  85. SELECT interval.id AS interval_id, interval.start AS interval_start,
  86. interval."end" AS interval_end
  87. FROM interval
  88. WHERE interval.start <= :start_1 AND interval."end" > :end_1
  89. >>> ia = aliased(Interval)
  90. >>> print(Session().query(Interval, ia).filter(Interval.intersects(ia)))
  91. SELECT interval.id AS interval_id, interval.start AS interval_start,
  92. interval."end" AS interval_end, interval_1.id AS interval_1_id,
  93. interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end
  94. FROM interval, interval AS interval_1
  95. WHERE interval.start <= interval_1.start
  96. AND interval."end" > interval_1.start
  97. OR interval.start <= interval_1."end"
  98. AND interval."end" > interval_1."end"
  99. .. _hybrid_distinct_expression:
  100. Defining Expression Behavior Distinct from Attribute Behavior
  101. --------------------------------------------------------------
  102. Our usage of the ``&`` and ``|`` bitwise operators above was
  103. fortunate, considering our functions operated on two boolean values to
  104. return a new one. In many cases, the construction of an in-Python
  105. function and a SQLAlchemy SQL expression have enough differences that
  106. two separate Python expressions should be defined. The
  107. :mod:`~sqlalchemy.ext.hybrid` decorators define the
  108. :meth:`.hybrid_property.expression` modifier for this purpose. As an
  109. example we'll define the radius of the interval, which requires the
  110. usage of the absolute value function::
  111. from sqlalchemy import func
  112. class Interval(object):
  113. # ...
  114. @hybrid_property
  115. def radius(self):
  116. return abs(self.length) / 2
  117. @radius.expression
  118. def radius(cls):
  119. return func.abs(cls.length) / 2
  120. Above the Python function ``abs()`` is used for instance-level
  121. operations, the SQL function ``ABS()`` is used via the :data:`.func`
  122. object for class-level expressions::
  123. >>> i1.radius
  124. 2
  125. >>> print(Session().query(Interval).filter(Interval.radius > 5))
  126. SELECT interval.id AS interval_id, interval.start AS interval_start,
  127. interval."end" AS interval_end
  128. FROM interval
  129. WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
  130. .. note:: When defining an expression for a hybrid property or method, the
  131. expression method **must** retain the name of the original hybrid, else
  132. the new hybrid with the additional state will be attached to the class
  133. with the non-matching name. To use the example above::
  134. class Interval(object):
  135. # ...
  136. @hybrid_property
  137. def radius(self):
  138. return abs(self.length) / 2
  139. # WRONG - the non-matching name will cause this function to be
  140. # ignored
  141. @radius.expression
  142. def radius_expression(cls):
  143. return func.abs(cls.length) / 2
  144. This is also true for other mutator methods, such as
  145. :meth:`.hybrid_property.update_expression`. This is the same behavior
  146. as that of the ``@property`` construct that is part of standard Python.
  147. Defining Setters
  148. ----------------
  149. Hybrid properties can also define setter methods. If we wanted
  150. ``length`` above, when set, to modify the endpoint value::
  151. class Interval(object):
  152. # ...
  153. @hybrid_property
  154. def length(self):
  155. return self.end - self.start
  156. @length.setter
  157. def length(self, value):
  158. self.end = self.start + value
  159. The ``length(self, value)`` method is now called upon set::
  160. >>> i1 = Interval(5, 10)
  161. >>> i1.length
  162. 5
  163. >>> i1.length = 12
  164. >>> i1.end
  165. 17
  166. .. _hybrid_bulk_update:
  167. Allowing Bulk ORM Update
  168. ------------------------
  169. A hybrid can define a custom "UPDATE" handler for when using the
  170. :meth:`_query.Query.update` method, allowing the hybrid to be used in the
  171. SET clause of the update.
  172. Normally, when using a hybrid with :meth:`_query.Query.update`, the SQL
  173. expression is used as the column that's the target of the SET. If our
  174. ``Interval`` class had a hybrid ``start_point`` that linked to
  175. ``Interval.start``, this could be substituted directly::
  176. session.query(Interval).update({Interval.start_point: 10})
  177. However, when using a composite hybrid like ``Interval.length``, this
  178. hybrid represents more than one column. We can set up a handler that will
  179. accommodate a value passed to :meth:`_query.Query.update` which can affect
  180. this, using the :meth:`.hybrid_property.update_expression` decorator.
  181. A handler that works similarly to our setter would be::
  182. class Interval(object):
  183. # ...
  184. @hybrid_property
  185. def length(self):
  186. return self.end - self.start
  187. @length.setter
  188. def length(self, value):
  189. self.end = self.start + value
  190. @length.update_expression
  191. def length(cls, value):
  192. return [
  193. (cls.end, cls.start + value)
  194. ]
  195. Above, if we use ``Interval.length`` in an UPDATE expression as::
  196. session.query(Interval).update(
  197. {Interval.length: 25}, synchronize_session='fetch')
  198. We'll get an UPDATE statement along the lines of::
  199. UPDATE interval SET end=start + :value
  200. In some cases, the default "evaluate" strategy can't perform the SET
  201. expression in Python; while the addition operator we're using above
  202. is supported, for more complex SET expressions it will usually be necessary
  203. to use either the "fetch" or False synchronization strategy as illustrated
  204. above.
  205. .. note:: For ORM bulk updates to work with hybrids, the function name
  206. of the hybrid must match that of how it is accessed. Something
  207. like this wouldn't work::
  208. class Interval(object):
  209. # ...
  210. def _get(self):
  211. return self.end - self.start
  212. def _set(self, value):
  213. self.end = self.start + value
  214. def _update_expr(cls, value):
  215. return [
  216. (cls.end, cls.start + value)
  217. ]
  218. length = hybrid_property(
  219. fget=_get, fset=_set, update_expr=_update_expr
  220. )
  221. The Python descriptor protocol does not provide any reliable way for
  222. a descriptor to know what attribute name it was accessed as, and
  223. the UPDATE scheme currently relies upon being able to access the
  224. attribute from an instance by name in order to perform the instance
  225. synchronization step.
  226. .. versionadded:: 1.2 added support for bulk updates to hybrid properties.
  227. Working with Relationships
  228. --------------------------
  229. There's no essential difference when creating hybrids that work with
  230. related objects as opposed to column-based data. The need for distinct
  231. expressions tends to be greater. The two variants we'll illustrate
  232. are the "join-dependent" hybrid, and the "correlated subquery" hybrid.
  233. Join-Dependent Relationship Hybrid
  234. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  235. Consider the following declarative
  236. mapping which relates a ``User`` to a ``SavingsAccount``::
  237. from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
  238. from sqlalchemy.orm import relationship
  239. from sqlalchemy.ext.declarative import declarative_base
  240. from sqlalchemy.ext.hybrid import hybrid_property
  241. Base = declarative_base()
  242. class SavingsAccount(Base):
  243. __tablename__ = 'account'
  244. id = Column(Integer, primary_key=True)
  245. user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
  246. balance = Column(Numeric(15, 5))
  247. class User(Base):
  248. __tablename__ = 'user'
  249. id = Column(Integer, primary_key=True)
  250. name = Column(String(100), nullable=False)
  251. accounts = relationship("SavingsAccount", backref="owner")
  252. @hybrid_property
  253. def balance(self):
  254. if self.accounts:
  255. return self.accounts[0].balance
  256. else:
  257. return None
  258. @balance.setter
  259. def balance(self, value):
  260. if not self.accounts:
  261. account = Account(owner=self)
  262. else:
  263. account = self.accounts[0]
  264. account.balance = value
  265. @balance.expression
  266. def balance(cls):
  267. return SavingsAccount.balance
  268. The above hybrid property ``balance`` works with the first
  269. ``SavingsAccount`` entry in the list of accounts for this user. The
  270. in-Python getter/setter methods can treat ``accounts`` as a Python
  271. list available on ``self``.
  272. However, at the expression level, it's expected that the ``User`` class will
  273. be used in an appropriate context such that an appropriate join to
  274. ``SavingsAccount`` will be present::
  275. >>> print(Session().query(User, User.balance).
  276. ... join(User.accounts).filter(User.balance > 5000))
  277. SELECT "user".id AS user_id, "user".name AS user_name,
  278. account.balance AS account_balance
  279. FROM "user" JOIN account ON "user".id = account.user_id
  280. WHERE account.balance > :balance_1
  281. Note however, that while the instance level accessors need to worry
  282. about whether ``self.accounts`` is even present, this issue expresses
  283. itself differently at the SQL expression level, where we basically
  284. would use an outer join::
  285. >>> from sqlalchemy import or_
  286. >>> print (Session().query(User, User.balance).outerjoin(User.accounts).
  287. ... filter(or_(User.balance < 5000, User.balance == None)))
  288. SELECT "user".id AS user_id, "user".name AS user_name,
  289. account.balance AS account_balance
  290. FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
  291. WHERE account.balance < :balance_1 OR account.balance IS NULL
  292. Correlated Subquery Relationship Hybrid
  293. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  294. We can, of course, forego being dependent on the enclosing query's usage
  295. of joins in favor of the correlated subquery, which can portably be packed
  296. into a single column expression. A correlated subquery is more portable, but
  297. often performs more poorly at the SQL level. Using the same technique
  298. illustrated at :ref:`mapper_column_property_sql_expressions`,
  299. we can adjust our ``SavingsAccount`` example to aggregate the balances for
  300. *all* accounts, and use a correlated subquery for the column expression::
  301. from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
  302. from sqlalchemy.orm import relationship
  303. from sqlalchemy.ext.declarative import declarative_base
  304. from sqlalchemy.ext.hybrid import hybrid_property
  305. from sqlalchemy import select, func
  306. Base = declarative_base()
  307. class SavingsAccount(Base):
  308. __tablename__ = 'account'
  309. id = Column(Integer, primary_key=True)
  310. user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
  311. balance = Column(Numeric(15, 5))
  312. class User(Base):
  313. __tablename__ = 'user'
  314. id = Column(Integer, primary_key=True)
  315. name = Column(String(100), nullable=False)
  316. accounts = relationship("SavingsAccount", backref="owner")
  317. @hybrid_property
  318. def balance(self):
  319. return sum(acc.balance for acc in self.accounts)
  320. @balance.expression
  321. def balance(cls):
  322. return select(func.sum(SavingsAccount.balance)).\
  323. where(SavingsAccount.user_id==cls.id).\
  324. label('total_balance')
  325. The above recipe will give us the ``balance`` column which renders
  326. a correlated SELECT::
  327. >>> print(s.query(User).filter(User.balance > 400))
  328. SELECT "user".id AS user_id, "user".name AS user_name
  329. FROM "user"
  330. WHERE (SELECT sum(account.balance) AS sum_1
  331. FROM account
  332. WHERE account.user_id = "user".id) > :param_1
  333. .. _hybrid_custom_comparators:
  334. Building Custom Comparators
  335. ---------------------------
  336. The hybrid property also includes a helper that allows construction of
  337. custom comparators. A comparator object allows one to customize the
  338. behavior of each SQLAlchemy expression operator individually. They
  339. are useful when creating custom types that have some highly
  340. idiosyncratic behavior on the SQL side.
  341. .. note:: The :meth:`.hybrid_property.comparator` decorator introduced
  342. in this section **replaces** the use of the
  343. :meth:`.hybrid_property.expression` decorator.
  344. They cannot be used together.
  345. The example class below allows case-insensitive comparisons on the attribute
  346. named ``word_insensitive``::
  347. from sqlalchemy.ext.hybrid import Comparator, hybrid_property
  348. from sqlalchemy import func, Column, Integer, String
  349. from sqlalchemy.orm import Session
  350. from sqlalchemy.ext.declarative import declarative_base
  351. Base = declarative_base()
  352. class CaseInsensitiveComparator(Comparator):
  353. def __eq__(self, other):
  354. return func.lower(self.__clause_element__()) == func.lower(other)
  355. class SearchWord(Base):
  356. __tablename__ = 'searchword'
  357. id = Column(Integer, primary_key=True)
  358. word = Column(String(255), nullable=False)
  359. @hybrid_property
  360. def word_insensitive(self):
  361. return self.word.lower()
  362. @word_insensitive.comparator
  363. def word_insensitive(cls):
  364. return CaseInsensitiveComparator(cls.word)
  365. Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()``
  366. SQL function to both sides::
  367. >>> print(Session().query(SearchWord).filter_by(word_insensitive="Trucks"))
  368. SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
  369. FROM searchword
  370. WHERE lower(searchword.word) = lower(:lower_1)
  371. The ``CaseInsensitiveComparator`` above implements part of the
  372. :class:`.ColumnOperators` interface. A "coercion" operation like
  373. lowercasing can be applied to all comparison operations (i.e. ``eq``,
  374. ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
  375. class CaseInsensitiveComparator(Comparator):
  376. def operate(self, op, other):
  377. return op(func.lower(self.__clause_element__()), func.lower(other))
  378. .. _hybrid_reuse_subclass:
  379. Reusing Hybrid Properties across Subclasses
  380. -------------------------------------------
  381. A hybrid can be referred to from a superclass, to allow modifying
  382. methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter`
  383. to be used to redefine those methods on a subclass. This is similar to
  384. how the standard Python ``@property`` object works::
  385. class FirstNameOnly(Base):
  386. # ...
  387. first_name = Column(String)
  388. @hybrid_property
  389. def name(self):
  390. return self.first_name
  391. @name.setter
  392. def name(self, value):
  393. self.first_name = value
  394. class FirstNameLastName(FirstNameOnly):
  395. # ...
  396. last_name = Column(String)
  397. @FirstNameOnly.name.getter
  398. def name(self):
  399. return self.first_name + ' ' + self.last_name
  400. @name.setter
  401. def name(self, value):
  402. self.first_name, self.last_name = value.split(' ', 1)
  403. Above, the ``FirstNameLastName`` class refers to the hybrid from
  404. ``FirstNameOnly.name`` to repurpose its getter and setter for the subclass.
  405. When overriding :meth:`.hybrid_property.expression` and
  406. :meth:`.hybrid_property.comparator` alone as the first reference to the
  407. superclass, these names conflict with the same-named accessors on the class-
  408. level :class:`.QueryableAttribute` object returned at the class level. To
  409. override these methods when referring directly to the parent class descriptor,
  410. add the special qualifier :attr:`.hybrid_property.overrides`, which will de-
  411. reference the instrumented attribute back to the hybrid object::
  412. class FirstNameLastName(FirstNameOnly):
  413. # ...
  414. last_name = Column(String)
  415. @FirstNameOnly.name.overrides.expression
  416. def name(cls):
  417. return func.concat(cls.first_name, ' ', cls.last_name)
  418. .. versionadded:: 1.2 Added :meth:`.hybrid_property.getter` as well as the
  419. ability to redefine accessors per-subclass.
  420. Hybrid Value Objects
  421. --------------------
  422. Note in our previous example, if we were to compare the ``word_insensitive``
  423. attribute of a ``SearchWord`` instance to a plain Python string, the plain
  424. Python string would not be coerced to lower case - the
  425. ``CaseInsensitiveComparator`` we built, being returned by
  426. ``@word_insensitive.comparator``, only applies to the SQL side.
  427. A more comprehensive form of the custom comparator is to construct a *Hybrid
  428. Value Object*. This technique applies the target value or expression to a value
  429. object which is then returned by the accessor in all cases. The value object
  430. allows control of all operations upon the value as well as how compared values
  431. are treated, both on the SQL expression side as well as the Python value side.
  432. Replacing the previous ``CaseInsensitiveComparator`` class with a new
  433. ``CaseInsensitiveWord`` class::
  434. class CaseInsensitiveWord(Comparator):
  435. "Hybrid value representing a lower case representation of a word."
  436. def __init__(self, word):
  437. if isinstance(word, basestring):
  438. self.word = word.lower()
  439. elif isinstance(word, CaseInsensitiveWord):
  440. self.word = word.word
  441. else:
  442. self.word = func.lower(word)
  443. def operate(self, op, other):
  444. if not isinstance(other, CaseInsensitiveWord):
  445. other = CaseInsensitiveWord(other)
  446. return op(self.word, other.word)
  447. def __clause_element__(self):
  448. return self.word
  449. def __str__(self):
  450. return self.word
  451. key = 'word'
  452. "Label to apply to Query tuple results"
  453. Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may
  454. be a SQL function, or may be a Python native. By overriding ``operate()`` and
  455. ``__clause_element__()`` to work in terms of ``self.word``, all comparison
  456. operations will work against the "converted" form of ``word``, whether it be
  457. SQL side or Python side. Our ``SearchWord`` class can now deliver the
  458. ``CaseInsensitiveWord`` object unconditionally from a single hybrid call::
  459. class SearchWord(Base):
  460. __tablename__ = 'searchword'
  461. id = Column(Integer, primary_key=True)
  462. word = Column(String(255), nullable=False)
  463. @hybrid_property
  464. def word_insensitive(self):
  465. return CaseInsensitiveWord(self.word)
  466. The ``word_insensitive`` attribute now has case-insensitive comparison behavior
  467. universally, including SQL expression vs. Python expression (note the Python
  468. value is converted to lower case on the Python side here)::
  469. >>> print(Session().query(SearchWord).filter_by(word_insensitive="Trucks"))
  470. SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
  471. FROM searchword
  472. WHERE lower(searchword.word) = :lower_1
  473. SQL expression versus SQL expression::
  474. >>> sw1 = aliased(SearchWord)
  475. >>> sw2 = aliased(SearchWord)
  476. >>> print(Session().query(
  477. ... sw1.word_insensitive,
  478. ... sw2.word_insensitive).\
  479. ... filter(
  480. ... sw1.word_insensitive > sw2.word_insensitive
  481. ... ))
  482. SELECT lower(searchword_1.word) AS lower_1,
  483. lower(searchword_2.word) AS lower_2
  484. FROM searchword AS searchword_1, searchword AS searchword_2
  485. WHERE lower(searchword_1.word) > lower(searchword_2.word)
  486. Python only expression::
  487. >>> ws1 = SearchWord(word="SomeWord")
  488. >>> ws1.word_insensitive == "sOmEwOrD"
  489. True
  490. >>> ws1.word_insensitive == "XOmEwOrX"
  491. False
  492. >>> print(ws1.word_insensitive)
  493. someword
  494. The Hybrid Value pattern is very useful for any kind of value that may have
  495. multiple representations, such as timestamps, time deltas, units of
  496. measurement, currencies and encrypted passwords.
  497. .. seealso::
  498. `Hybrids and Value Agnostic Types
  499. <http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_
  500. - on the techspot.zzzeek.org blog
  501. `Value Agnostic Types, Part II
  502. <http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ -
  503. on the techspot.zzzeek.org blog
  504. .. _hybrid_transformers:
  505. Building Transformers
  506. ----------------------
  507. A *transformer* is an object which can receive a :class:`_query.Query`
  508. object and
  509. return a new one. The :class:`_query.Query` object includes a method
  510. :meth:`.with_transformation` that returns a new :class:`_query.Query`
  511. transformed by
  512. the given function.
  513. We can combine this with the :class:`.Comparator` class to produce one type
  514. of recipe which can both set up the FROM clause of a query as well as assign
  515. filtering criterion.
  516. Consider a mapped class ``Node``, which assembles using adjacency list into a
  517. hierarchical tree pattern::
  518. from sqlalchemy import Column, Integer, ForeignKey
  519. from sqlalchemy.orm import relationship
  520. from sqlalchemy.ext.declarative import declarative_base
  521. Base = declarative_base()
  522. class Node(Base):
  523. __tablename__ = 'node'
  524. id = Column(Integer, primary_key=True)
  525. parent_id = Column(Integer, ForeignKey('node.id'))
  526. parent = relationship("Node", remote_side=id)
  527. Suppose we wanted to add an accessor ``grandparent``. This would return the
  528. ``parent`` of ``Node.parent``. When we have an instance of ``Node``, this is
  529. simple::
  530. from sqlalchemy.ext.hybrid import hybrid_property
  531. class Node(Base):
  532. # ...
  533. @hybrid_property
  534. def grandparent(self):
  535. return self.parent.parent
  536. For the expression, things are not so clear. We'd need to construct a
  537. :class:`_query.Query` where we :meth:`_query.Query.join` twice along
  538. ``Node.parent`` to get to the ``grandparent``. We can instead return a
  539. transforming callable that we'll combine with the :class:`.Comparator` class to
  540. receive any :class:`_query.Query` object, and return a new one that's joined to
  541. the ``Node.parent`` attribute and filtered based on the given criterion::
  542. from sqlalchemy.ext.hybrid import Comparator
  543. class GrandparentTransformer(Comparator):
  544. def operate(self, op, other):
  545. def transform(q):
  546. cls = self.__clause_element__()
  547. parent_alias = aliased(cls)
  548. return q.join(parent_alias, cls.parent).\
  549. filter(op(parent_alias.parent, other))
  550. return transform
  551. Base = declarative_base()
  552. class Node(Base):
  553. __tablename__ = 'node'
  554. id = Column(Integer, primary_key=True)
  555. parent_id = Column(Integer, ForeignKey('node.id'))
  556. parent = relationship("Node", remote_side=id)
  557. @hybrid_property
  558. def grandparent(self):
  559. return self.parent.parent
  560. @grandparent.comparator
  561. def grandparent(cls):
  562. return GrandparentTransformer(cls)
  563. The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate`
  564. method at the base of the :class:`.Comparator` hierarchy to return a query-
  565. transforming callable, which then runs the given comparison operation in a
  566. particular context. Such as, in the example above, the ``operate`` method is
  567. called, given the :attr:`.Operators.eq` callable as well as the right side of
  568. the comparison ``Node(id=5)``. A function ``transform`` is then returned which
  569. will transform a :class:`_query.Query` first to join to ``Node.parent``,
  570. then to
  571. compare ``parent_alias`` using :attr:`.Operators.eq` against the left and right
  572. sides, passing into :meth:`_query.Query.filter`:
  573. .. sourcecode:: pycon+sql
  574. >>> from sqlalchemy.orm import Session
  575. >>> session = Session()
  576. {sql}>>> session.query(Node).\
  577. ... with_transformation(Node.grandparent==Node(id=5)).\
  578. ... all()
  579. SELECT node.id AS node_id, node.parent_id AS node_parent_id
  580. FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
  581. WHERE :param_1 = node_1.parent_id
  582. {stop}
  583. We can modify the pattern to be more verbose but flexible by separating the
  584. "join" step from the "filter" step. The tricky part here is ensuring that
  585. successive instances of ``GrandparentTransformer`` use the same
  586. :class:`.AliasedClass` object against ``Node``. Below we use a simple
  587. memoizing approach that associates a ``GrandparentTransformer`` with each
  588. class::
  589. class Node(Base):
  590. # ...
  591. @grandparent.comparator
  592. def grandparent(cls):
  593. # memoize a GrandparentTransformer
  594. # per class
  595. if '_gp' not in cls.__dict__:
  596. cls._gp = GrandparentTransformer(cls)
  597. return cls._gp
  598. class GrandparentTransformer(Comparator):
  599. def __init__(self, cls):
  600. self.parent_alias = aliased(cls)
  601. @property
  602. def join(self):
  603. def go(q):
  604. return q.join(self.parent_alias, Node.parent)
  605. return go
  606. def operate(self, op, other):
  607. return op(self.parent_alias.parent, other)
  608. .. sourcecode:: pycon+sql
  609. {sql}>>> session.query(Node).\
  610. ... with_transformation(Node.grandparent.join).\
  611. ... filter(Node.grandparent==Node(id=5))
  612. SELECT node.id AS node_id, node.parent_id AS node_parent_id
  613. FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
  614. WHERE :param_1 = node_1.parent_id
  615. {stop}
  616. The "transformer" pattern is an experimental pattern that starts to make usage
  617. of some functional programming paradigms. While it's only recommended for
  618. advanced and/or patient developers, there's probably a whole lot of amazing
  619. things it can be used for.
  620. """ # noqa
  621. from .. import util
  622. from ..orm import attributes
  623. from ..orm import interfaces
  624. from ..sql import elements
  625. HYBRID_METHOD = util.symbol("HYBRID_METHOD")
  626. """Symbol indicating an :class:`InspectionAttr` that's
  627. of type :class:`.hybrid_method`.
  628. Is assigned to the :attr:`.InspectionAttr.extension_type`
  629. attribute.
  630. .. seealso::
  631. :attr:`_orm.Mapper.all_orm_attributes`
  632. """
  633. HYBRID_PROPERTY = util.symbol("HYBRID_PROPERTY")
  634. """Symbol indicating an :class:`InspectionAttr` that's
  635. of type :class:`.hybrid_method`.
  636. Is assigned to the :attr:`.InspectionAttr.extension_type`
  637. attribute.
  638. .. seealso::
  639. :attr:`_orm.Mapper.all_orm_attributes`
  640. """
  641. class hybrid_method(interfaces.InspectionAttrInfo):
  642. """A decorator which allows definition of a Python object method with both
  643. instance-level and class-level behavior.
  644. """
  645. is_attribute = True
  646. extension_type = HYBRID_METHOD
  647. def __init__(self, func, expr=None):
  648. """Create a new :class:`.hybrid_method`.
  649. Usage is typically via decorator::
  650. from sqlalchemy.ext.hybrid import hybrid_method
  651. class SomeClass(object):
  652. @hybrid_method
  653. def value(self, x, y):
  654. return self._value + x + y
  655. @value.expression
  656. def value(self, x, y):
  657. return func.some_function(self._value, x, y)
  658. """
  659. self.func = func
  660. self.expression(expr or func)
  661. def __get__(self, instance, owner):
  662. if instance is None:
  663. return self.expr.__get__(owner, owner.__class__)
  664. else:
  665. return self.func.__get__(instance, owner)
  666. def expression(self, expr):
  667. """Provide a modifying decorator that defines a
  668. SQL-expression producing method."""
  669. self.expr = expr
  670. if not self.expr.__doc__:
  671. self.expr.__doc__ = self.func.__doc__
  672. return self
  673. class hybrid_property(interfaces.InspectionAttrInfo):
  674. """A decorator which allows definition of a Python descriptor with both
  675. instance-level and class-level behavior.
  676. """
  677. is_attribute = True
  678. extension_type = HYBRID_PROPERTY
  679. def __init__(
  680. self,
  681. fget,
  682. fset=None,
  683. fdel=None,
  684. expr=None,
  685. custom_comparator=None,
  686. update_expr=None,
  687. ):
  688. """Create a new :class:`.hybrid_property`.
  689. Usage is typically via decorator::
  690. from sqlalchemy.ext.hybrid import hybrid_property
  691. class SomeClass(object):
  692. @hybrid_property
  693. def value(self):
  694. return self._value
  695. @value.setter
  696. def value(self, value):
  697. self._value = value
  698. """
  699. self.fget = fget
  700. self.fset = fset
  701. self.fdel = fdel
  702. self.expr = expr
  703. self.custom_comparator = custom_comparator
  704. self.update_expr = update_expr
  705. util.update_wrapper(self, fget)
  706. def __get__(self, instance, owner):
  707. if instance is None:
  708. return self._expr_comparator(owner)
  709. else:
  710. return self.fget(instance)
  711. def __set__(self, instance, value):
  712. if self.fset is None:
  713. raise AttributeError("can't set attribute")
  714. self.fset(instance, value)
  715. def __delete__(self, instance):
  716. if self.fdel is None:
  717. raise AttributeError("can't delete attribute")
  718. self.fdel(instance)
  719. def _copy(self, **kw):
  720. defaults = {
  721. key: value
  722. for key, value in self.__dict__.items()
  723. if not key.startswith("_")
  724. }
  725. defaults.update(**kw)
  726. return type(self)(**defaults)
  727. @property
  728. def overrides(self):
  729. """Prefix for a method that is overriding an existing attribute.
  730. The :attr:`.hybrid_property.overrides` accessor just returns
  731. this hybrid object, which when called at the class level from
  732. a parent class, will de-reference the "instrumented attribute"
  733. normally returned at this level, and allow modifying decorators
  734. like :meth:`.hybrid_property.expression` and
  735. :meth:`.hybrid_property.comparator`
  736. to be used without conflicting with the same-named attributes
  737. normally present on the :class:`.QueryableAttribute`::
  738. class SuperClass(object):
  739. # ...
  740. @hybrid_property
  741. def foobar(self):
  742. return self._foobar
  743. class SubClass(SuperClass):
  744. # ...
  745. @SuperClass.foobar.overrides.expression
  746. def foobar(cls):
  747. return func.subfoobar(self._foobar)
  748. .. versionadded:: 1.2
  749. .. seealso::
  750. :ref:`hybrid_reuse_subclass`
  751. """
  752. return self
  753. def getter(self, fget):
  754. """Provide a modifying decorator that defines a getter method.
  755. .. versionadded:: 1.2
  756. """
  757. return self._copy(fget=fget)
  758. def setter(self, fset):
  759. """Provide a modifying decorator that defines a setter method."""
  760. return self._copy(fset=fset)
  761. def deleter(self, fdel):
  762. """Provide a modifying decorator that defines a deletion method."""
  763. return self._copy(fdel=fdel)
  764. def expression(self, expr):
  765. """Provide a modifying decorator that defines a SQL-expression
  766. producing method.
  767. When a hybrid is invoked at the class level, the SQL expression given
  768. here is wrapped inside of a specialized :class:`.QueryableAttribute`,
  769. which is the same kind of object used by the ORM to represent other
  770. mapped attributes. The reason for this is so that other class-level
  771. attributes such as docstrings and a reference to the hybrid itself may
  772. be maintained within the structure that's returned, without any
  773. modifications to the original SQL expression passed in.
  774. .. note::
  775. When referring to a hybrid property from an owning class (e.g.
  776. ``SomeClass.some_hybrid``), an instance of
  777. :class:`.QueryableAttribute` is returned, representing the
  778. expression or comparator object as well as this hybrid object.
  779. However, that object itself has accessors called ``expression`` and
  780. ``comparator``; so when attempting to override these decorators on a
  781. subclass, it may be necessary to qualify it using the
  782. :attr:`.hybrid_property.overrides` modifier first. See that
  783. modifier for details.
  784. .. seealso::
  785. :ref:`hybrid_distinct_expression`
  786. """
  787. return self._copy(expr=expr)
  788. def comparator(self, comparator):
  789. """Provide a modifying decorator that defines a custom
  790. comparator producing method.
  791. The return value of the decorated method should be an instance of
  792. :class:`~.hybrid.Comparator`.
  793. .. note:: The :meth:`.hybrid_property.comparator` decorator
  794. **replaces** the use of the :meth:`.hybrid_property.expression`
  795. decorator. They cannot be used together.
  796. When a hybrid is invoked at the class level, the
  797. :class:`~.hybrid.Comparator` object given here is wrapped inside of a
  798. specialized :class:`.QueryableAttribute`, which is the same kind of
  799. object used by the ORM to represent other mapped attributes. The
  800. reason for this is so that other class-level attributes such as
  801. docstrings and a reference to the hybrid itself may be maintained
  802. within the structure that's returned, without any modifications to the
  803. original comparator object passed in.
  804. .. note::
  805. When referring to a hybrid property from an owning class (e.g.
  806. ``SomeClass.some_hybrid``), an instance of
  807. :class:`.QueryableAttribute` is returned, representing the
  808. expression or comparator object as this hybrid object. However,
  809. that object itself has accessors called ``expression`` and
  810. ``comparator``; so when attempting to override these decorators on a
  811. subclass, it may be necessary to qualify it using the
  812. :attr:`.hybrid_property.overrides` modifier first. See that
  813. modifier for details.
  814. """
  815. return self._copy(custom_comparator=comparator)
  816. def update_expression(self, meth):
  817. """Provide a modifying decorator that defines an UPDATE tuple
  818. producing method.
  819. The method accepts a single value, which is the value to be
  820. rendered into the SET clause of an UPDATE statement. The method
  821. should then process this value into individual column expressions
  822. that fit into the ultimate SET clause, and return them as a
  823. sequence of 2-tuples. Each tuple
  824. contains a column expression as the key and a value to be rendered.
  825. E.g.::
  826. class Person(Base):
  827. # ...
  828. first_name = Column(String)
  829. last_name = Column(String)
  830. @hybrid_property
  831. def fullname(self):
  832. return first_name + " " + last_name
  833. @fullname.update_expression
  834. def fullname(cls, value):
  835. fname, lname = value.split(" ", 1)
  836. return [
  837. (cls.first_name, fname),
  838. (cls.last_name, lname)
  839. ]
  840. .. versionadded:: 1.2
  841. """
  842. return self._copy(update_expr=meth)
  843. @util.memoized_property
  844. def _expr_comparator(self):
  845. if self.custom_comparator is not None:
  846. return self._get_comparator(self.custom_comparator)
  847. elif self.expr is not None:
  848. return self._get_expr(self.expr)
  849. else:
  850. return self._get_expr(self.fget)
  851. def _get_expr(self, expr):
  852. def _expr(cls):
  853. return ExprComparator(cls, expr(cls), self)
  854. util.update_wrapper(_expr, expr)
  855. return self._get_comparator(_expr)
  856. def _get_comparator(self, comparator):
  857. proxy_attr = attributes.create_proxied_attribute(self)
  858. def expr_comparator(owner):
  859. # because this is the descriptor protocol, we don't really know
  860. # what our attribute name is. so search for it through the
  861. # MRO.
  862. for lookup in owner.__mro__:
  863. if self.__name__ in lookup.__dict__:
  864. if lookup.__dict__[self.__name__] is self:
  865. name = self.__name__
  866. break
  867. else:
  868. name = attributes.NO_KEY
  869. return proxy_attr(
  870. owner,
  871. name,
  872. self,
  873. comparator(owner),
  874. doc=comparator.__doc__ or self.__doc__,
  875. )
  876. return expr_comparator
  877. class Comparator(interfaces.PropComparator):
  878. """A helper class that allows easy construction of custom
  879. :class:`~.orm.interfaces.PropComparator`
  880. classes for usage with hybrids."""
  881. property = None
  882. def __init__(self, expression):
  883. self.expression = expression
  884. def __clause_element__(self):
  885. expr = self.expression
  886. if hasattr(expr, "__clause_element__"):
  887. expr = expr.__clause_element__()
  888. return expr
  889. def adapt_to_entity(self, adapt_to_entity):
  890. # interesting....
  891. return self
  892. class ExprComparator(Comparator):
  893. def __init__(self, cls, expression, hybrid):
  894. self.cls = cls
  895. self.expression = expression
  896. self.hybrid = hybrid
  897. def __getattr__(self, key):
  898. return getattr(self.expression, key)
  899. @property
  900. def info(self):
  901. return self.hybrid.info
  902. def _bulk_update_tuples(self, value):
  903. if isinstance(value, elements.BindParameter):
  904. value = value.value
  905. if isinstance(self.expression, attributes.QueryableAttribute):
  906. return self.expression._bulk_update_tuples(value)
  907. elif self.hybrid.update_expr is not None:
  908. return self.hybrid.update_expr(self.cls, value)
  909. else:
  910. return [(self.expression, value)]
  911. @property
  912. def property(self):
  913. return self.expression.property
  914. def operate(self, op, *other, **kwargs):
  915. return op(self.expression, *other, **kwargs)
  916. def reverse_operate(self, op, other, **kwargs):
  917. return op(other, self.expression, **kwargs)