hybrid.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. # ext/hybrid.py
  2. # Copyright (C) 2005-2017 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
  16. SQL 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. returns a new SQL expression::
  53. >>> print Interval.length
  54. interval."end" - interval.start
  55. >>> print Session().query(Interval).filter(Interval.length > 10)
  56. SELECT interval.id AS interval_id, interval.start AS interval_start,
  57. interval."end" AS interval_end
  58. FROM interval
  59. WHERE interval."end" - interval.start > :param_1
  60. ORM methods such as :meth:`~.Query.filter_by` generally use ``getattr()`` to
  61. locate attributes, so can also be used with hybrid attributes::
  62. >>> print Session().query(Interval).filter_by(length=5)
  63. SELECT interval.id AS interval_id, interval.start AS interval_start,
  64. interval."end" AS interval_end
  65. FROM interval
  66. WHERE interval."end" - interval.start = :param_1
  67. The ``Interval`` class example also illustrates two methods,
  68. ``contains()`` and ``intersects()``, decorated with
  69. :class:`.hybrid_method`. This decorator applies the same idea to
  70. methods that :class:`.hybrid_property` applies to attributes. The
  71. methods return boolean values, and take advantage of the Python ``|``
  72. and ``&`` bitwise operators to produce equivalent instance-level and
  73. SQL expression-level boolean behavior::
  74. >>> i1.contains(6)
  75. True
  76. >>> i1.contains(15)
  77. False
  78. >>> i1.intersects(Interval(7, 18))
  79. True
  80. >>> i1.intersects(Interval(25, 29))
  81. False
  82. >>> print Session().query(Interval).filter(Interval.contains(15))
  83. SELECT interval.id AS interval_id, interval.start AS interval_start,
  84. interval."end" AS interval_end
  85. FROM interval
  86. WHERE interval.start <= :start_1 AND interval."end" > :end_1
  87. >>> ia = aliased(Interval)
  88. >>> print Session().query(Interval, ia).filter(Interval.intersects(ia))
  89. SELECT interval.id AS interval_id, interval.start AS interval_start,
  90. interval."end" AS interval_end, interval_1.id AS interval_1_id,
  91. interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end
  92. FROM interval, interval AS interval_1
  93. WHERE interval.start <= interval_1.start
  94. AND interval."end" > interval_1.start
  95. OR interval.start <= interval_1."end"
  96. AND interval."end" > interval_1."end"
  97. Defining Expression Behavior Distinct from Attribute Behavior
  98. --------------------------------------------------------------
  99. Our usage of the ``&`` and ``|`` bitwise operators above was
  100. fortunate, considering our functions operated on two boolean values to
  101. return a new one. In many cases, the construction of an in-Python
  102. function and a SQLAlchemy SQL expression have enough differences that
  103. two separate Python expressions should be defined. The
  104. :mod:`~sqlalchemy.ext.hybrid` decorators define the
  105. :meth:`.hybrid_property.expression` modifier for this purpose. As an
  106. example we'll define the radius of the interval, which requires the
  107. usage of the absolute value function::
  108. from sqlalchemy import func
  109. class Interval(object):
  110. # ...
  111. @hybrid_property
  112. def radius(self):
  113. return abs(self.length) / 2
  114. @radius.expression
  115. def radius(cls):
  116. return func.abs(cls.length) / 2
  117. Above the Python function ``abs()`` is used for instance-level
  118. operations, the SQL function ``ABS()`` is used via the :data:`.func`
  119. object for class-level expressions::
  120. >>> i1.radius
  121. 2
  122. >>> print Session().query(Interval).filter(Interval.radius > 5)
  123. SELECT interval.id AS interval_id, interval.start AS interval_start,
  124. interval."end" AS interval_end
  125. FROM interval
  126. WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
  127. Defining Setters
  128. ----------------
  129. Hybrid properties can also define setter methods. If we wanted
  130. ``length`` above, when set, to modify the endpoint value::
  131. class Interval(object):
  132. # ...
  133. @hybrid_property
  134. def length(self):
  135. return self.end - self.start
  136. @length.setter
  137. def length(self, value):
  138. self.end = self.start + value
  139. The ``length(self, value)`` method is now called upon set::
  140. >>> i1 = Interval(5, 10)
  141. >>> i1.length
  142. 5
  143. >>> i1.length = 12
  144. >>> i1.end
  145. 17
  146. Working with Relationships
  147. --------------------------
  148. There's no essential difference when creating hybrids that work with
  149. related objects as opposed to column-based data. The need for distinct
  150. expressions tends to be greater. The two variants we'll illustrate
  151. are the "join-dependent" hybrid, and the "correlated subquery" hybrid.
  152. Join-Dependent Relationship Hybrid
  153. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  154. Consider the following declarative
  155. mapping which relates a ``User`` to a ``SavingsAccount``::
  156. from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
  157. from sqlalchemy.orm import relationship
  158. from sqlalchemy.ext.declarative import declarative_base
  159. from sqlalchemy.ext.hybrid import hybrid_property
  160. Base = declarative_base()
  161. class SavingsAccount(Base):
  162. __tablename__ = 'account'
  163. id = Column(Integer, primary_key=True)
  164. user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
  165. balance = Column(Numeric(15, 5))
  166. class User(Base):
  167. __tablename__ = 'user'
  168. id = Column(Integer, primary_key=True)
  169. name = Column(String(100), nullable=False)
  170. accounts = relationship("SavingsAccount", backref="owner")
  171. @hybrid_property
  172. def balance(self):
  173. if self.accounts:
  174. return self.accounts[0].balance
  175. else:
  176. return None
  177. @balance.setter
  178. def balance(self, value):
  179. if not self.accounts:
  180. account = Account(owner=self)
  181. else:
  182. account = self.accounts[0]
  183. account.balance = value
  184. @balance.expression
  185. def balance(cls):
  186. return SavingsAccount.balance
  187. The above hybrid property ``balance`` works with the first
  188. ``SavingsAccount`` entry in the list of accounts for this user. The
  189. in-Python getter/setter methods can treat ``accounts`` as a Python
  190. list available on ``self``.
  191. However, at the expression level, it's expected that the ``User`` class will
  192. be used in an appropriate context such that an appropriate join to
  193. ``SavingsAccount`` will be present::
  194. >>> print Session().query(User, User.balance).\
  195. ... join(User.accounts).filter(User.balance > 5000)
  196. SELECT "user".id AS user_id, "user".name AS user_name,
  197. account.balance AS account_balance
  198. FROM "user" JOIN account ON "user".id = account.user_id
  199. WHERE account.balance > :balance_1
  200. Note however, that while the instance level accessors need to worry
  201. about whether ``self.accounts`` is even present, this issue expresses
  202. itself differently at the SQL expression level, where we basically
  203. would use an outer join::
  204. >>> from sqlalchemy import or_
  205. >>> print (Session().query(User, User.balance).outerjoin(User.accounts).
  206. ... filter(or_(User.balance < 5000, User.balance == None)))
  207. SELECT "user".id AS user_id, "user".name AS user_name,
  208. account.balance AS account_balance
  209. FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
  210. WHERE account.balance < :balance_1 OR account.balance IS NULL
  211. Correlated Subquery Relationship Hybrid
  212. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  213. We can, of course, forego being dependent on the enclosing query's usage
  214. of joins in favor of the correlated subquery, which can portably be packed
  215. into a single column expression. A correlated subquery is more portable, but
  216. often performs more poorly at the SQL level. Using the same technique
  217. illustrated at :ref:`mapper_column_property_sql_expressions`,
  218. we can adjust our ``SavingsAccount`` example to aggregate the balances for
  219. *all* accounts, and use a correlated subquery for the column expression::
  220. from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
  221. from sqlalchemy.orm import relationship
  222. from sqlalchemy.ext.declarative import declarative_base
  223. from sqlalchemy.ext.hybrid import hybrid_property
  224. from sqlalchemy import select, func
  225. Base = declarative_base()
  226. class SavingsAccount(Base):
  227. __tablename__ = 'account'
  228. id = Column(Integer, primary_key=True)
  229. user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
  230. balance = Column(Numeric(15, 5))
  231. class User(Base):
  232. __tablename__ = 'user'
  233. id = Column(Integer, primary_key=True)
  234. name = Column(String(100), nullable=False)
  235. accounts = relationship("SavingsAccount", backref="owner")
  236. @hybrid_property
  237. def balance(self):
  238. return sum(acc.balance for acc in self.accounts)
  239. @balance.expression
  240. def balance(cls):
  241. return select([func.sum(SavingsAccount.balance)]).\
  242. where(SavingsAccount.user_id==cls.id).\
  243. label('total_balance')
  244. The above recipe will give us the ``balance`` column which renders
  245. a correlated SELECT::
  246. >>> print s.query(User).filter(User.balance > 400)
  247. SELECT "user".id AS user_id, "user".name AS user_name
  248. FROM "user"
  249. WHERE (SELECT sum(account.balance) AS sum_1
  250. FROM account
  251. WHERE account.user_id = "user".id) > :param_1
  252. .. _hybrid_custom_comparators:
  253. Building Custom Comparators
  254. ---------------------------
  255. The hybrid property also includes a helper that allows construction of
  256. custom comparators. A comparator object allows one to customize the
  257. behavior of each SQLAlchemy expression operator individually. They
  258. are useful when creating custom types that have some highly
  259. idiosyncratic behavior on the SQL side.
  260. The example class below allows case-insensitive comparisons on the attribute
  261. named ``word_insensitive``::
  262. from sqlalchemy.ext.hybrid import Comparator, hybrid_property
  263. from sqlalchemy import func, Column, Integer, String
  264. from sqlalchemy.orm import Session
  265. from sqlalchemy.ext.declarative import declarative_base
  266. Base = declarative_base()
  267. class CaseInsensitiveComparator(Comparator):
  268. def __eq__(self, other):
  269. return func.lower(self.__clause_element__()) == func.lower(other)
  270. class SearchWord(Base):
  271. __tablename__ = 'searchword'
  272. id = Column(Integer, primary_key=True)
  273. word = Column(String(255), nullable=False)
  274. @hybrid_property
  275. def word_insensitive(self):
  276. return self.word.lower()
  277. @word_insensitive.comparator
  278. def word_insensitive(cls):
  279. return CaseInsensitiveComparator(cls.word)
  280. Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()``
  281. SQL function to both sides::
  282. >>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
  283. SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
  284. FROM searchword
  285. WHERE lower(searchword.word) = lower(:lower_1)
  286. The ``CaseInsensitiveComparator`` above implements part of the
  287. :class:`.ColumnOperators` interface. A "coercion" operation like
  288. lowercasing can be applied to all comparison operations (i.e. ``eq``,
  289. ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
  290. class CaseInsensitiveComparator(Comparator):
  291. def operate(self, op, other):
  292. return op(func.lower(self.__clause_element__()), func.lower(other))
  293. Hybrid Value Objects
  294. --------------------
  295. Note in our previous example, if we were to compare the
  296. ``word_insensitive`` attribute of a ``SearchWord`` instance to a plain
  297. Python string, the plain Python string would not be coerced to lower
  298. case - the ``CaseInsensitiveComparator`` we built, being returned by
  299. ``@word_insensitive.comparator``, only applies to the SQL side.
  300. A more comprehensive form of the custom comparator is to construct a
  301. *Hybrid Value Object*. This technique applies the target value or
  302. expression to a value object which is then returned by the accessor in
  303. all cases. The value object allows control of all operations upon
  304. the value as well as how compared values are treated, both on the SQL
  305. expression side as well as the Python value side. Replacing the
  306. previous ``CaseInsensitiveComparator`` class with a new
  307. ``CaseInsensitiveWord`` class::
  308. class CaseInsensitiveWord(Comparator):
  309. "Hybrid value representing a lower case representation of a word."
  310. def __init__(self, word):
  311. if isinstance(word, basestring):
  312. self.word = word.lower()
  313. elif isinstance(word, CaseInsensitiveWord):
  314. self.word = word.word
  315. else:
  316. self.word = func.lower(word)
  317. def operate(self, op, other):
  318. if not isinstance(other, CaseInsensitiveWord):
  319. other = CaseInsensitiveWord(other)
  320. return op(self.word, other.word)
  321. def __clause_element__(self):
  322. return self.word
  323. def __str__(self):
  324. return self.word
  325. key = 'word'
  326. "Label to apply to Query tuple results"
  327. Above, the ``CaseInsensitiveWord`` object represents ``self.word``,
  328. which may be a SQL function, or may be a Python native. By
  329. overriding ``operate()`` and ``__clause_element__()`` to work in terms
  330. of ``self.word``, all comparison operations will work against the
  331. "converted" form of ``word``, whether it be SQL side or Python side.
  332. Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord``
  333. object unconditionally from a single hybrid call::
  334. class SearchWord(Base):
  335. __tablename__ = 'searchword'
  336. id = Column(Integer, primary_key=True)
  337. word = Column(String(255), nullable=False)
  338. @hybrid_property
  339. def word_insensitive(self):
  340. return CaseInsensitiveWord(self.word)
  341. The ``word_insensitive`` attribute now has case-insensitive comparison
  342. behavior universally, including SQL expression vs. Python expression
  343. (note the Python value is converted to lower case on the Python side
  344. here)::
  345. >>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
  346. SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
  347. FROM searchword
  348. WHERE lower(searchword.word) = :lower_1
  349. SQL expression versus SQL expression::
  350. >>> sw1 = aliased(SearchWord)
  351. >>> sw2 = aliased(SearchWord)
  352. >>> print Session().query(
  353. ... sw1.word_insensitive,
  354. ... sw2.word_insensitive).\
  355. ... filter(
  356. ... sw1.word_insensitive > sw2.word_insensitive
  357. ... )
  358. SELECT lower(searchword_1.word) AS lower_1,
  359. lower(searchword_2.word) AS lower_2
  360. FROM searchword AS searchword_1, searchword AS searchword_2
  361. WHERE lower(searchword_1.word) > lower(searchword_2.word)
  362. Python only expression::
  363. >>> ws1 = SearchWord(word="SomeWord")
  364. >>> ws1.word_insensitive == "sOmEwOrD"
  365. True
  366. >>> ws1.word_insensitive == "XOmEwOrX"
  367. False
  368. >>> print ws1.word_insensitive
  369. someword
  370. The Hybrid Value pattern is very useful for any kind of value that may
  371. have multiple representations, such as timestamps, time deltas, units
  372. of measurement, currencies and encrypted passwords.
  373. .. seealso::
  374. `Hybrids and Value Agnostic Types
  375. <http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_
  376. - on the techspot.zzzeek.org blog
  377. `Value Agnostic Types, Part II
  378. <http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ -
  379. on the techspot.zzzeek.org blog
  380. .. _hybrid_transformers:
  381. Building Transformers
  382. ----------------------
  383. A *transformer* is an object which can receive a :class:`.Query`
  384. object and return a new one. The :class:`.Query` object includes a
  385. method :meth:`.with_transformation` that returns a new :class:`.Query`
  386. transformed by the given function.
  387. We can combine this with the :class:`.Comparator` class to produce one type
  388. of recipe which can both set up the FROM clause of a query as well as assign
  389. filtering criterion.
  390. Consider a mapped class ``Node``, which assembles using adjacency list
  391. into a hierarchical tree pattern::
  392. from sqlalchemy import Column, Integer, ForeignKey
  393. from sqlalchemy.orm import relationship
  394. from sqlalchemy.ext.declarative import declarative_base
  395. Base = declarative_base()
  396. class Node(Base):
  397. __tablename__ = 'node'
  398. id = Column(Integer, primary_key=True)
  399. parent_id = Column(Integer, ForeignKey('node.id'))
  400. parent = relationship("Node", remote_side=id)
  401. Suppose we wanted to add an accessor ``grandparent``. This would
  402. return the ``parent`` of ``Node.parent``. When we have an instance of
  403. ``Node``, this is simple::
  404. from sqlalchemy.ext.hybrid import hybrid_property
  405. class Node(Base):
  406. # ...
  407. @hybrid_property
  408. def grandparent(self):
  409. return self.parent.parent
  410. For the expression, things are not so clear. We'd need to construct
  411. a :class:`.Query` where we :meth:`~.Query.join` twice along
  412. ``Node.parent`` to get to the ``grandparent``. We can instead return
  413. a transforming callable that we'll combine with the
  414. :class:`.Comparator` class to receive any :class:`.Query` object, and
  415. return a new one that's joined to the ``Node.parent`` attribute and
  416. filtered based on the given criterion::
  417. from sqlalchemy.ext.hybrid import Comparator
  418. class GrandparentTransformer(Comparator):
  419. def operate(self, op, other):
  420. def transform(q):
  421. cls = self.__clause_element__()
  422. parent_alias = aliased(cls)
  423. return q.join(parent_alias, cls.parent).\
  424. filter(op(parent_alias.parent, other))
  425. return transform
  426. Base = declarative_base()
  427. class Node(Base):
  428. __tablename__ = 'node'
  429. id =Column(Integer, primary_key=True)
  430. parent_id = Column(Integer, ForeignKey('node.id'))
  431. parent = relationship("Node", remote_side=id)
  432. @hybrid_property
  433. def grandparent(self):
  434. return self.parent.parent
  435. @grandparent.comparator
  436. def grandparent(cls):
  437. return GrandparentTransformer(cls)
  438. The ``GrandparentTransformer`` overrides the core
  439. :meth:`.Operators.operate` method at the base of the
  440. :class:`.Comparator` hierarchy to return a query-transforming
  441. callable, which then runs the given comparison operation in a
  442. particular context. Such as, in the example above, the ``operate``
  443. method is called, given the :attr:`.Operators.eq` callable as well as
  444. the right side of the comparison ``Node(id=5)``. A function
  445. ``transform`` is then returned which will transform a :class:`.Query`
  446. first to join to ``Node.parent``, then to compare ``parent_alias``
  447. using :attr:`.Operators.eq` against the left and right sides, passing
  448. into :class:`.Query.filter`:
  449. .. sourcecode:: pycon+sql
  450. >>> from sqlalchemy.orm import Session
  451. >>> session = Session()
  452. {sql}>>> session.query(Node).\
  453. ... with_transformation(Node.grandparent==Node(id=5)).\
  454. ... all()
  455. SELECT node.id AS node_id, node.parent_id AS node_parent_id
  456. FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
  457. WHERE :param_1 = node_1.parent_id
  458. {stop}
  459. We can modify the pattern to be more verbose but flexible by separating
  460. the "join" step from the "filter" step. The tricky part here is ensuring
  461. that successive instances of ``GrandparentTransformer`` use the same
  462. :class:`.AliasedClass` object against ``Node``. Below we use a simple
  463. memoizing approach that associates a ``GrandparentTransformer``
  464. with each class::
  465. class Node(Base):
  466. # ...
  467. @grandparent.comparator
  468. def grandparent(cls):
  469. # memoize a GrandparentTransformer
  470. # per class
  471. if '_gp' not in cls.__dict__:
  472. cls._gp = GrandparentTransformer(cls)
  473. return cls._gp
  474. class GrandparentTransformer(Comparator):
  475. def __init__(self, cls):
  476. self.parent_alias = aliased(cls)
  477. @property
  478. def join(self):
  479. def go(q):
  480. return q.join(self.parent_alias, Node.parent)
  481. return go
  482. def operate(self, op, other):
  483. return op(self.parent_alias.parent, other)
  484. .. sourcecode:: pycon+sql
  485. {sql}>>> session.query(Node).\
  486. ... with_transformation(Node.grandparent.join).\
  487. ... filter(Node.grandparent==Node(id=5))
  488. SELECT node.id AS node_id, node.parent_id AS node_parent_id
  489. FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
  490. WHERE :param_1 = node_1.parent_id
  491. {stop}
  492. The "transformer" pattern is an experimental pattern that starts
  493. to make usage of some functional programming paradigms.
  494. While it's only recommended for advanced and/or patient developers,
  495. there's probably a whole lot of amazing things it can be used for.
  496. """
  497. from .. import util
  498. from ..orm import attributes, interfaces
  499. HYBRID_METHOD = util.symbol('HYBRID_METHOD')
  500. """Symbol indicating an :class:`InspectionAttr` that's
  501. of type :class:`.hybrid_method`.
  502. Is assigned to the :attr:`.InspectionAttr.extension_type`
  503. attibute.
  504. .. seealso::
  505. :attr:`.Mapper.all_orm_attributes`
  506. """
  507. HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY')
  508. """Symbol indicating an :class:`InspectionAttr` that's
  509. of type :class:`.hybrid_method`.
  510. Is assigned to the :attr:`.InspectionAttr.extension_type`
  511. attibute.
  512. .. seealso::
  513. :attr:`.Mapper.all_orm_attributes`
  514. """
  515. class hybrid_method(interfaces.InspectionAttrInfo):
  516. """A decorator which allows definition of a Python object method with both
  517. instance-level and class-level behavior.
  518. """
  519. is_attribute = True
  520. extension_type = HYBRID_METHOD
  521. def __init__(self, func, expr=None):
  522. """Create a new :class:`.hybrid_method`.
  523. Usage is typically via decorator::
  524. from sqlalchemy.ext.hybrid import hybrid_method
  525. class SomeClass(object):
  526. @hybrid_method
  527. def value(self, x, y):
  528. return self._value + x + y
  529. @value.expression
  530. def value(self, x, y):
  531. return func.some_function(self._value, x, y)
  532. """
  533. self.func = func
  534. self.expression(expr or func)
  535. def __get__(self, instance, owner):
  536. if instance is None:
  537. return self.expr.__get__(owner, owner.__class__)
  538. else:
  539. return self.func.__get__(instance, owner)
  540. def expression(self, expr):
  541. """Provide a modifying decorator that defines a
  542. SQL-expression producing method."""
  543. self.expr = expr
  544. if not self.expr.__doc__:
  545. self.expr.__doc__ = self.func.__doc__
  546. return self
  547. class hybrid_property(interfaces.InspectionAttrInfo):
  548. """A decorator which allows definition of a Python descriptor with both
  549. instance-level and class-level behavior.
  550. """
  551. is_attribute = True
  552. extension_type = HYBRID_PROPERTY
  553. def __init__(self, fget, fset=None, fdel=None, expr=None):
  554. """Create a new :class:`.hybrid_property`.
  555. Usage is typically via decorator::
  556. from sqlalchemy.ext.hybrid import hybrid_property
  557. class SomeClass(object):
  558. @hybrid_property
  559. def value(self):
  560. return self._value
  561. @value.setter
  562. def value(self, value):
  563. self._value = value
  564. """
  565. self.fget = fget
  566. self.fset = fset
  567. self.fdel = fdel
  568. self.expression(expr or fget)
  569. util.update_wrapper(self, fget)
  570. def __get__(self, instance, owner):
  571. if instance is None:
  572. return self.expr(owner)
  573. else:
  574. return self.fget(instance)
  575. def __set__(self, instance, value):
  576. if self.fset is None:
  577. raise AttributeError("can't set attribute")
  578. self.fset(instance, value)
  579. def __delete__(self, instance):
  580. if self.fdel is None:
  581. raise AttributeError("can't delete attribute")
  582. self.fdel(instance)
  583. def setter(self, fset):
  584. """Provide a modifying decorator that defines a value-setter method."""
  585. self.fset = fset
  586. return self
  587. def deleter(self, fdel):
  588. """Provide a modifying decorator that defines a
  589. value-deletion method."""
  590. self.fdel = fdel
  591. return self
  592. def expression(self, expr):
  593. """Provide a modifying decorator that defines a SQL-expression
  594. producing method."""
  595. def _expr(cls):
  596. return ExprComparator(expr(cls), self)
  597. util.update_wrapper(_expr, expr)
  598. self.expr = _expr
  599. return self.comparator(_expr)
  600. def comparator(self, comparator):
  601. """Provide a modifying decorator that defines a custom
  602. comparator producing method.
  603. The return value of the decorated method should be an instance of
  604. :class:`~.hybrid.Comparator`.
  605. """
  606. proxy_attr = attributes.\
  607. create_proxied_attribute(self)
  608. def expr(owner):
  609. return proxy_attr(
  610. owner, self.__name__, self, comparator(owner),
  611. doc=comparator.__doc__ or self.__doc__)
  612. self.expr = expr
  613. return self
  614. class Comparator(interfaces.PropComparator):
  615. """A helper class that allows easy construction of custom
  616. :class:`~.orm.interfaces.PropComparator`
  617. classes for usage with hybrids."""
  618. property = None
  619. def __init__(self, expression):
  620. self.expression = expression
  621. def __clause_element__(self):
  622. expr = self.expression
  623. if hasattr(expr, '__clause_element__'):
  624. expr = expr.__clause_element__()
  625. return expr
  626. def adapt_to_entity(self, adapt_to_entity):
  627. # interesting....
  628. return self
  629. class ExprComparator(Comparator):
  630. def __init__(self, expression, hybrid):
  631. self.expression = expression
  632. self.hybrid = hybrid
  633. def __getattr__(self, key):
  634. return getattr(self.expression, key)
  635. @property
  636. def info(self):
  637. return self.hybrid.info
  638. @property
  639. def property(self):
  640. return self.expression.property
  641. def operate(self, op, *other, **kwargs):
  642. return op(self.expression, *other, **kwargs)
  643. def reverse_operate(self, op, other, **kwargs):
  644. return op(other, self.expression, **kwargs)