|
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204 |
- # ext/hybrid.py
- # Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: http://www.opensource.org/licenses/mit-license.php
-
- r"""Define attributes on ORM-mapped classes that have "hybrid" behavior.
-
- "hybrid" means the attribute has distinct behaviors defined at the
- class level and at the instance level.
-
- The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of
- method decorator, is around 50 lines of code and has almost no
- dependencies on the rest of SQLAlchemy. It can, in theory, work with
- any descriptor-based expression system.
-
- Consider a mapping ``Interval``, representing integer ``start`` and ``end``
- values. We can define higher level functions on mapped classes that produce SQL
- expressions at the class level, and Python expression evaluation at the
- instance level. Below, each function decorated with :class:`.hybrid_method` or
- :class:`.hybrid_property` may receive ``self`` as an instance of the class, or
- as the class itself::
-
- from sqlalchemy import Column, Integer
- from sqlalchemy.ext.declarative import declarative_base
- from sqlalchemy.orm import Session, aliased
- from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
-
- Base = declarative_base()
-
- class Interval(Base):
- __tablename__ = 'interval'
-
- id = Column(Integer, primary_key=True)
- start = Column(Integer, nullable=False)
- end = Column(Integer, nullable=False)
-
- def __init__(self, start, end):
- self.start = start
- self.end = end
-
- @hybrid_property
- def length(self):
- return self.end - self.start
-
- @hybrid_method
- def contains(self, point):
- return (self.start <= point) & (point <= self.end)
-
- @hybrid_method
- def intersects(self, other):
- return self.contains(other.start) | self.contains(other.end)
-
- Above, the ``length`` property returns the difference between the
- ``end`` and ``start`` attributes. With an instance of ``Interval``,
- this subtraction occurs in Python, using normal Python descriptor
- mechanics::
-
- >>> i1 = Interval(5, 10)
- >>> i1.length
- 5
-
- When dealing with the ``Interval`` class itself, the :class:`.hybrid_property`
- descriptor evaluates the function body given the ``Interval`` class as
- the argument, which when evaluated with SQLAlchemy expression mechanics
- (here using the :attr:`.QueryableAttribute.expression` accessor)
- returns a new SQL expression::
-
- >>> print(Interval.length.expression)
- interval."end" - interval.start
-
- >>> print(Session().query(Interval).filter(Interval.length > 10))
- SELECT interval.id AS interval_id, interval.start AS interval_start,
- interval."end" AS interval_end
- FROM interval
- WHERE interval."end" - interval.start > :param_1
-
- ORM methods such as :meth:`_query.Query.filter_by`
- generally use ``getattr()`` to
- locate attributes, so can also be used with hybrid attributes::
-
- >>> print(Session().query(Interval).filter_by(length=5))
- SELECT interval.id AS interval_id, interval.start AS interval_start,
- interval."end" AS interval_end
- FROM interval
- WHERE interval."end" - interval.start = :param_1
-
- The ``Interval`` class example also illustrates two methods,
- ``contains()`` and ``intersects()``, decorated with
- :class:`.hybrid_method`. This decorator applies the same idea to
- methods that :class:`.hybrid_property` applies to attributes. The
- methods return boolean values, and take advantage of the Python ``|``
- and ``&`` bitwise operators to produce equivalent instance-level and
- SQL expression-level boolean behavior::
-
- >>> i1.contains(6)
- True
- >>> i1.contains(15)
- False
- >>> i1.intersects(Interval(7, 18))
- True
- >>> i1.intersects(Interval(25, 29))
- False
-
- >>> print(Session().query(Interval).filter(Interval.contains(15)))
- SELECT interval.id AS interval_id, interval.start AS interval_start,
- interval."end" AS interval_end
- FROM interval
- WHERE interval.start <= :start_1 AND interval."end" > :end_1
-
- >>> ia = aliased(Interval)
- >>> print(Session().query(Interval, ia).filter(Interval.intersects(ia)))
- SELECT interval.id AS interval_id, interval.start AS interval_start,
- interval."end" AS interval_end, interval_1.id AS interval_1_id,
- interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end
- FROM interval, interval AS interval_1
- WHERE interval.start <= interval_1.start
- AND interval."end" > interval_1.start
- OR interval.start <= interval_1."end"
- AND interval."end" > interval_1."end"
-
- .. _hybrid_distinct_expression:
-
- Defining Expression Behavior Distinct from Attribute Behavior
- --------------------------------------------------------------
-
- Our usage of the ``&`` and ``|`` bitwise operators above was
- fortunate, considering our functions operated on two boolean values to
- return a new one. In many cases, the construction of an in-Python
- function and a SQLAlchemy SQL expression have enough differences that
- two separate Python expressions should be defined. The
- :mod:`~sqlalchemy.ext.hybrid` decorators define the
- :meth:`.hybrid_property.expression` modifier for this purpose. As an
- example we'll define the radius of the interval, which requires the
- usage of the absolute value function::
-
- from sqlalchemy import func
-
- class Interval(object):
- # ...
-
- @hybrid_property
- def radius(self):
- return abs(self.length) / 2
-
- @radius.expression
- def radius(cls):
- return func.abs(cls.length) / 2
-
- Above the Python function ``abs()`` is used for instance-level
- operations, the SQL function ``ABS()`` is used via the :data:`.func`
- object for class-level expressions::
-
- >>> i1.radius
- 2
-
- >>> print(Session().query(Interval).filter(Interval.radius > 5))
- SELECT interval.id AS interval_id, interval.start AS interval_start,
- interval."end" AS interval_end
- FROM interval
- WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
-
- .. note:: When defining an expression for a hybrid property or method, the
- expression method **must** retain the name of the original hybrid, else
- the new hybrid with the additional state will be attached to the class
- with the non-matching name. To use the example above::
-
- class Interval(object):
- # ...
-
- @hybrid_property
- def radius(self):
- return abs(self.length) / 2
-
- # WRONG - the non-matching name will cause this function to be
- # ignored
- @radius.expression
- def radius_expression(cls):
- return func.abs(cls.length) / 2
-
- This is also true for other mutator methods, such as
- :meth:`.hybrid_property.update_expression`. This is the same behavior
- as that of the ``@property`` construct that is part of standard Python.
-
- Defining Setters
- ----------------
-
- Hybrid properties can also define setter methods. If we wanted
- ``length`` above, when set, to modify the endpoint value::
-
- class Interval(object):
- # ...
-
- @hybrid_property
- def length(self):
- return self.end - self.start
-
- @length.setter
- def length(self, value):
- self.end = self.start + value
-
- The ``length(self, value)`` method is now called upon set::
-
- >>> i1 = Interval(5, 10)
- >>> i1.length
- 5
- >>> i1.length = 12
- >>> i1.end
- 17
-
- .. _hybrid_bulk_update:
-
- Allowing Bulk ORM Update
- ------------------------
-
- A hybrid can define a custom "UPDATE" handler for when using the
- :meth:`_query.Query.update` method, allowing the hybrid to be used in the
- SET clause of the update.
-
- Normally, when using a hybrid with :meth:`_query.Query.update`, the SQL
- expression is used as the column that's the target of the SET. If our
- ``Interval`` class had a hybrid ``start_point`` that linked to
- ``Interval.start``, this could be substituted directly::
-
- session.query(Interval).update({Interval.start_point: 10})
-
- However, when using a composite hybrid like ``Interval.length``, this
- hybrid represents more than one column. We can set up a handler that will
- accommodate a value passed to :meth:`_query.Query.update` which can affect
- this, using the :meth:`.hybrid_property.update_expression` decorator.
- A handler that works similarly to our setter would be::
-
- class Interval(object):
- # ...
-
- @hybrid_property
- def length(self):
- return self.end - self.start
-
- @length.setter
- def length(self, value):
- self.end = self.start + value
-
- @length.update_expression
- def length(cls, value):
- return [
- (cls.end, cls.start + value)
- ]
-
- Above, if we use ``Interval.length`` in an UPDATE expression as::
-
- session.query(Interval).update(
- {Interval.length: 25}, synchronize_session='fetch')
-
- We'll get an UPDATE statement along the lines of::
-
- UPDATE interval SET end=start + :value
-
- In some cases, the default "evaluate" strategy can't perform the SET
- expression in Python; while the addition operator we're using above
- is supported, for more complex SET expressions it will usually be necessary
- to use either the "fetch" or False synchronization strategy as illustrated
- above.
-
- .. note:: For ORM bulk updates to work with hybrids, the function name
- of the hybrid must match that of how it is accessed. Something
- like this wouldn't work::
-
- class Interval(object):
- # ...
-
- def _get(self):
- return self.end - self.start
-
- def _set(self, value):
- self.end = self.start + value
-
- def _update_expr(cls, value):
- return [
- (cls.end, cls.start + value)
- ]
-
- length = hybrid_property(
- fget=_get, fset=_set, update_expr=_update_expr
- )
-
- The Python descriptor protocol does not provide any reliable way for
- a descriptor to know what attribute name it was accessed as, and
- the UPDATE scheme currently relies upon being able to access the
- attribute from an instance by name in order to perform the instance
- synchronization step.
-
- .. versionadded:: 1.2 added support for bulk updates to hybrid properties.
-
- Working with Relationships
- --------------------------
-
- There's no essential difference when creating hybrids that work with
- related objects as opposed to column-based data. The need for distinct
- expressions tends to be greater. The two variants we'll illustrate
- are the "join-dependent" hybrid, and the "correlated subquery" hybrid.
-
- Join-Dependent Relationship Hybrid
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- Consider the following declarative
- mapping which relates a ``User`` to a ``SavingsAccount``::
-
- from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
- from sqlalchemy.orm import relationship
- from sqlalchemy.ext.declarative import declarative_base
- from sqlalchemy.ext.hybrid import hybrid_property
-
- Base = declarative_base()
-
- class SavingsAccount(Base):
- __tablename__ = 'account'
- id = Column(Integer, primary_key=True)
- user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
- balance = Column(Numeric(15, 5))
-
- class User(Base):
- __tablename__ = 'user'
- id = Column(Integer, primary_key=True)
- name = Column(String(100), nullable=False)
-
- accounts = relationship("SavingsAccount", backref="owner")
-
- @hybrid_property
- def balance(self):
- if self.accounts:
- return self.accounts[0].balance
- else:
- return None
-
- @balance.setter
- def balance(self, value):
- if not self.accounts:
- account = Account(owner=self)
- else:
- account = self.accounts[0]
- account.balance = value
-
- @balance.expression
- def balance(cls):
- return SavingsAccount.balance
-
- The above hybrid property ``balance`` works with the first
- ``SavingsAccount`` entry in the list of accounts for this user. The
- in-Python getter/setter methods can treat ``accounts`` as a Python
- list available on ``self``.
-
- However, at the expression level, it's expected that the ``User`` class will
- be used in an appropriate context such that an appropriate join to
- ``SavingsAccount`` will be present::
-
- >>> print(Session().query(User, User.balance).
- ... join(User.accounts).filter(User.balance > 5000))
- SELECT "user".id AS user_id, "user".name AS user_name,
- account.balance AS account_balance
- FROM "user" JOIN account ON "user".id = account.user_id
- WHERE account.balance > :balance_1
-
- Note however, that while the instance level accessors need to worry
- about whether ``self.accounts`` is even present, this issue expresses
- itself differently at the SQL expression level, where we basically
- would use an outer join::
-
- >>> from sqlalchemy import or_
- >>> print (Session().query(User, User.balance).outerjoin(User.accounts).
- ... filter(or_(User.balance < 5000, User.balance == None)))
- SELECT "user".id AS user_id, "user".name AS user_name,
- account.balance AS account_balance
- FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
- WHERE account.balance < :balance_1 OR account.balance IS NULL
-
- Correlated Subquery Relationship Hybrid
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- We can, of course, forego being dependent on the enclosing query's usage
- of joins in favor of the correlated subquery, which can portably be packed
- into a single column expression. A correlated subquery is more portable, but
- often performs more poorly at the SQL level. Using the same technique
- illustrated at :ref:`mapper_column_property_sql_expressions`,
- we can adjust our ``SavingsAccount`` example to aggregate the balances for
- *all* accounts, and use a correlated subquery for the column expression::
-
- from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
- from sqlalchemy.orm import relationship
- from sqlalchemy.ext.declarative import declarative_base
- from sqlalchemy.ext.hybrid import hybrid_property
- from sqlalchemy import select, func
-
- Base = declarative_base()
-
- class SavingsAccount(Base):
- __tablename__ = 'account'
- id = Column(Integer, primary_key=True)
- user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
- balance = Column(Numeric(15, 5))
-
- class User(Base):
- __tablename__ = 'user'
- id = Column(Integer, primary_key=True)
- name = Column(String(100), nullable=False)
-
- accounts = relationship("SavingsAccount", backref="owner")
-
- @hybrid_property
- def balance(self):
- return sum(acc.balance for acc in self.accounts)
-
- @balance.expression
- def balance(cls):
- return select(func.sum(SavingsAccount.balance)).\
- where(SavingsAccount.user_id==cls.id).\
- label('total_balance')
-
- The above recipe will give us the ``balance`` column which renders
- a correlated SELECT::
-
- >>> print(s.query(User).filter(User.balance > 400))
- SELECT "user".id AS user_id, "user".name AS user_name
- FROM "user"
- WHERE (SELECT sum(account.balance) AS sum_1
- FROM account
- WHERE account.user_id = "user".id) > :param_1
-
- .. _hybrid_custom_comparators:
-
- Building Custom Comparators
- ---------------------------
-
- The hybrid property also includes a helper that allows construction of
- custom comparators. A comparator object allows one to customize the
- behavior of each SQLAlchemy expression operator individually. They
- are useful when creating custom types that have some highly
- idiosyncratic behavior on the SQL side.
-
- .. note:: The :meth:`.hybrid_property.comparator` decorator introduced
- in this section **replaces** the use of the
- :meth:`.hybrid_property.expression` decorator.
- They cannot be used together.
-
- The example class below allows case-insensitive comparisons on the attribute
- named ``word_insensitive``::
-
- from sqlalchemy.ext.hybrid import Comparator, hybrid_property
- from sqlalchemy import func, Column, Integer, String
- from sqlalchemy.orm import Session
- from sqlalchemy.ext.declarative import declarative_base
-
- Base = declarative_base()
-
- class CaseInsensitiveComparator(Comparator):
- def __eq__(self, other):
- return func.lower(self.__clause_element__()) == func.lower(other)
-
- class SearchWord(Base):
- __tablename__ = 'searchword'
- id = Column(Integer, primary_key=True)
- word = Column(String(255), nullable=False)
-
- @hybrid_property
- def word_insensitive(self):
- return self.word.lower()
-
- @word_insensitive.comparator
- def word_insensitive(cls):
- return CaseInsensitiveComparator(cls.word)
-
- Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()``
- SQL function to both sides::
-
- >>> print(Session().query(SearchWord).filter_by(word_insensitive="Trucks"))
- SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
- FROM searchword
- WHERE lower(searchword.word) = lower(:lower_1)
-
- The ``CaseInsensitiveComparator`` above implements part of the
- :class:`.ColumnOperators` interface. A "coercion" operation like
- lowercasing can be applied to all comparison operations (i.e. ``eq``,
- ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
-
- class CaseInsensitiveComparator(Comparator):
- def operate(self, op, other):
- return op(func.lower(self.__clause_element__()), func.lower(other))
-
- .. _hybrid_reuse_subclass:
-
- Reusing Hybrid Properties across Subclasses
- -------------------------------------------
-
- A hybrid can be referred to from a superclass, to allow modifying
- methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter`
- to be used to redefine those methods on a subclass. This is similar to
- how the standard Python ``@property`` object works::
-
- class FirstNameOnly(Base):
- # ...
-
- first_name = Column(String)
-
- @hybrid_property
- def name(self):
- return self.first_name
-
- @name.setter
- def name(self, value):
- self.first_name = value
-
- class FirstNameLastName(FirstNameOnly):
- # ...
-
- last_name = Column(String)
-
- @FirstNameOnly.name.getter
- def name(self):
- return self.first_name + ' ' + self.last_name
-
- @name.setter
- def name(self, value):
- self.first_name, self.last_name = value.split(' ', 1)
-
- Above, the ``FirstNameLastName`` class refers to the hybrid from
- ``FirstNameOnly.name`` to repurpose its getter and setter for the subclass.
-
- When overriding :meth:`.hybrid_property.expression` and
- :meth:`.hybrid_property.comparator` alone as the first reference to the
- superclass, these names conflict with the same-named accessors on the class-
- level :class:`.QueryableAttribute` object returned at the class level. To
- override these methods when referring directly to the parent class descriptor,
- add the special qualifier :attr:`.hybrid_property.overrides`, which will de-
- reference the instrumented attribute back to the hybrid object::
-
- class FirstNameLastName(FirstNameOnly):
- # ...
-
- last_name = Column(String)
-
- @FirstNameOnly.name.overrides.expression
- def name(cls):
- return func.concat(cls.first_name, ' ', cls.last_name)
-
- .. versionadded:: 1.2 Added :meth:`.hybrid_property.getter` as well as the
- ability to redefine accessors per-subclass.
-
-
- Hybrid Value Objects
- --------------------
-
- Note in our previous example, if we were to compare the ``word_insensitive``
- attribute of a ``SearchWord`` instance to a plain Python string, the plain
- Python string would not be coerced to lower case - the
- ``CaseInsensitiveComparator`` we built, being returned by
- ``@word_insensitive.comparator``, only applies to the SQL side.
-
- A more comprehensive form of the custom comparator is to construct a *Hybrid
- Value Object*. This technique applies the target value or expression to a value
- object which is then returned by the accessor in all cases. The value object
- allows control of all operations upon the value as well as how compared values
- are treated, both on the SQL expression side as well as the Python value side.
- Replacing the previous ``CaseInsensitiveComparator`` class with a new
- ``CaseInsensitiveWord`` class::
-
- class CaseInsensitiveWord(Comparator):
- "Hybrid value representing a lower case representation of a word."
-
- def __init__(self, word):
- if isinstance(word, basestring):
- self.word = word.lower()
- elif isinstance(word, CaseInsensitiveWord):
- self.word = word.word
- else:
- self.word = func.lower(word)
-
- def operate(self, op, other):
- if not isinstance(other, CaseInsensitiveWord):
- other = CaseInsensitiveWord(other)
- return op(self.word, other.word)
-
- def __clause_element__(self):
- return self.word
-
- def __str__(self):
- return self.word
-
- key = 'word'
- "Label to apply to Query tuple results"
-
- Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may
- be a SQL function, or may be a Python native. By overriding ``operate()`` and
- ``__clause_element__()`` to work in terms of ``self.word``, all comparison
- operations will work against the "converted" form of ``word``, whether it be
- SQL side or Python side. Our ``SearchWord`` class can now deliver the
- ``CaseInsensitiveWord`` object unconditionally from a single hybrid call::
-
- class SearchWord(Base):
- __tablename__ = 'searchword'
- id = Column(Integer, primary_key=True)
- word = Column(String(255), nullable=False)
-
- @hybrid_property
- def word_insensitive(self):
- return CaseInsensitiveWord(self.word)
-
- The ``word_insensitive`` attribute now has case-insensitive comparison behavior
- universally, including SQL expression vs. Python expression (note the Python
- value is converted to lower case on the Python side here)::
-
- >>> print(Session().query(SearchWord).filter_by(word_insensitive="Trucks"))
- SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
- FROM searchword
- WHERE lower(searchword.word) = :lower_1
-
- SQL expression versus SQL expression::
-
- >>> sw1 = aliased(SearchWord)
- >>> sw2 = aliased(SearchWord)
- >>> print(Session().query(
- ... sw1.word_insensitive,
- ... sw2.word_insensitive).\
- ... filter(
- ... sw1.word_insensitive > sw2.word_insensitive
- ... ))
- SELECT lower(searchword_1.word) AS lower_1,
- lower(searchword_2.word) AS lower_2
- FROM searchword AS searchword_1, searchword AS searchword_2
- WHERE lower(searchword_1.word) > lower(searchword_2.word)
-
- Python only expression::
-
- >>> ws1 = SearchWord(word="SomeWord")
- >>> ws1.word_insensitive == "sOmEwOrD"
- True
- >>> ws1.word_insensitive == "XOmEwOrX"
- False
- >>> print(ws1.word_insensitive)
- someword
-
- The Hybrid Value pattern is very useful for any kind of value that may have
- multiple representations, such as timestamps, time deltas, units of
- measurement, currencies and encrypted passwords.
-
- .. seealso::
-
- `Hybrids and Value Agnostic Types
- <http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_
- - on the techspot.zzzeek.org blog
-
- `Value Agnostic Types, Part II
- <http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ -
- on the techspot.zzzeek.org blog
-
- .. _hybrid_transformers:
-
- Building Transformers
- ----------------------
-
- A *transformer* is an object which can receive a :class:`_query.Query`
- object and
- return a new one. The :class:`_query.Query` object includes a method
- :meth:`.with_transformation` that returns a new :class:`_query.Query`
- transformed by
- the given function.
-
- We can combine this with the :class:`.Comparator` class to produce one type
- of recipe which can both set up the FROM clause of a query as well as assign
- filtering criterion.
-
- Consider a mapped class ``Node``, which assembles using adjacency list into a
- hierarchical tree pattern::
-
- from sqlalchemy import Column, Integer, ForeignKey
- from sqlalchemy.orm import relationship
- from sqlalchemy.ext.declarative import declarative_base
- Base = declarative_base()
-
- class Node(Base):
- __tablename__ = 'node'
- id = Column(Integer, primary_key=True)
- parent_id = Column(Integer, ForeignKey('node.id'))
- parent = relationship("Node", remote_side=id)
-
- Suppose we wanted to add an accessor ``grandparent``. This would return the
- ``parent`` of ``Node.parent``. When we have an instance of ``Node``, this is
- simple::
-
- from sqlalchemy.ext.hybrid import hybrid_property
-
- class Node(Base):
- # ...
-
- @hybrid_property
- def grandparent(self):
- return self.parent.parent
-
- For the expression, things are not so clear. We'd need to construct a
- :class:`_query.Query` where we :meth:`_query.Query.join` twice along
- ``Node.parent`` to get to the ``grandparent``. We can instead return a
- transforming callable that we'll combine with the :class:`.Comparator` class to
- receive any :class:`_query.Query` object, and return a new one that's joined to
- the ``Node.parent`` attribute and filtered based on the given criterion::
-
- from sqlalchemy.ext.hybrid import Comparator
-
- class GrandparentTransformer(Comparator):
- def operate(self, op, other):
- def transform(q):
- cls = self.__clause_element__()
- parent_alias = aliased(cls)
- return q.join(parent_alias, cls.parent).\
- filter(op(parent_alias.parent, other))
- return transform
-
- Base = declarative_base()
-
- class Node(Base):
- __tablename__ = 'node'
- id = Column(Integer, primary_key=True)
- parent_id = Column(Integer, ForeignKey('node.id'))
- parent = relationship("Node", remote_side=id)
-
- @hybrid_property
- def grandparent(self):
- return self.parent.parent
-
- @grandparent.comparator
- def grandparent(cls):
- return GrandparentTransformer(cls)
-
- The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate`
- method at the base of the :class:`.Comparator` hierarchy to return a query-
- transforming callable, which then runs the given comparison operation in a
- particular context. Such as, in the example above, the ``operate`` method is
- called, given the :attr:`.Operators.eq` callable as well as the right side of
- the comparison ``Node(id=5)``. A function ``transform`` is then returned which
- will transform a :class:`_query.Query` first to join to ``Node.parent``,
- then to
- compare ``parent_alias`` using :attr:`.Operators.eq` against the left and right
- sides, passing into :meth:`_query.Query.filter`:
-
- .. sourcecode:: pycon+sql
-
- >>> from sqlalchemy.orm import Session
- >>> session = Session()
- {sql}>>> session.query(Node).\
- ... with_transformation(Node.grandparent==Node(id=5)).\
- ... all()
- SELECT node.id AS node_id, node.parent_id AS node_parent_id
- FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
- WHERE :param_1 = node_1.parent_id
- {stop}
-
- We can modify the pattern to be more verbose but flexible by separating the
- "join" step from the "filter" step. The tricky part here is ensuring that
- successive instances of ``GrandparentTransformer`` use the same
- :class:`.AliasedClass` object against ``Node``. Below we use a simple
- memoizing approach that associates a ``GrandparentTransformer`` with each
- class::
-
- class Node(Base):
-
- # ...
-
- @grandparent.comparator
- def grandparent(cls):
- # memoize a GrandparentTransformer
- # per class
- if '_gp' not in cls.__dict__:
- cls._gp = GrandparentTransformer(cls)
- return cls._gp
-
- class GrandparentTransformer(Comparator):
-
- def __init__(self, cls):
- self.parent_alias = aliased(cls)
-
- @property
- def join(self):
- def go(q):
- return q.join(self.parent_alias, Node.parent)
- return go
-
- def operate(self, op, other):
- return op(self.parent_alias.parent, other)
-
- .. sourcecode:: pycon+sql
-
- {sql}>>> session.query(Node).\
- ... with_transformation(Node.grandparent.join).\
- ... filter(Node.grandparent==Node(id=5))
- SELECT node.id AS node_id, node.parent_id AS node_parent_id
- FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
- WHERE :param_1 = node_1.parent_id
- {stop}
-
- The "transformer" pattern is an experimental pattern that starts to make usage
- of some functional programming paradigms. While it's only recommended for
- advanced and/or patient developers, there's probably a whole lot of amazing
- things it can be used for.
-
- """ # noqa
- from .. import util
- from ..orm import attributes
- from ..orm import interfaces
- from ..sql import elements
-
- HYBRID_METHOD = util.symbol("HYBRID_METHOD")
- """Symbol indicating an :class:`InspectionAttr` that's
- of type :class:`.hybrid_method`.
-
- Is assigned to the :attr:`.InspectionAttr.extension_type`
- attribute.
-
- .. seealso::
-
- :attr:`_orm.Mapper.all_orm_attributes`
-
- """
-
- HYBRID_PROPERTY = util.symbol("HYBRID_PROPERTY")
- """Symbol indicating an :class:`InspectionAttr` that's
- of type :class:`.hybrid_method`.
-
- Is assigned to the :attr:`.InspectionAttr.extension_type`
- attribute.
-
- .. seealso::
-
- :attr:`_orm.Mapper.all_orm_attributes`
-
- """
-
-
- class hybrid_method(interfaces.InspectionAttrInfo):
- """A decorator which allows definition of a Python object method with both
- instance-level and class-level behavior.
-
- """
-
- is_attribute = True
- extension_type = HYBRID_METHOD
-
- def __init__(self, func, expr=None):
- """Create a new :class:`.hybrid_method`.
-
- Usage is typically via decorator::
-
- from sqlalchemy.ext.hybrid import hybrid_method
-
- class SomeClass(object):
- @hybrid_method
- def value(self, x, y):
- return self._value + x + y
-
- @value.expression
- def value(self, x, y):
- return func.some_function(self._value, x, y)
-
- """
- self.func = func
- self.expression(expr or func)
-
- def __get__(self, instance, owner):
- if instance is None:
- return self.expr.__get__(owner, owner.__class__)
- else:
- return self.func.__get__(instance, owner)
-
- def expression(self, expr):
- """Provide a modifying decorator that defines a
- SQL-expression producing method."""
-
- self.expr = expr
- if not self.expr.__doc__:
- self.expr.__doc__ = self.func.__doc__
- return self
-
-
- class hybrid_property(interfaces.InspectionAttrInfo):
- """A decorator which allows definition of a Python descriptor with both
- instance-level and class-level behavior.
-
- """
-
- is_attribute = True
- extension_type = HYBRID_PROPERTY
-
- def __init__(
- self,
- fget,
- fset=None,
- fdel=None,
- expr=None,
- custom_comparator=None,
- update_expr=None,
- ):
- """Create a new :class:`.hybrid_property`.
-
- Usage is typically via decorator::
-
- from sqlalchemy.ext.hybrid import hybrid_property
-
- class SomeClass(object):
- @hybrid_property
- def value(self):
- return self._value
-
- @value.setter
- def value(self, value):
- self._value = value
-
- """
- self.fget = fget
- self.fset = fset
- self.fdel = fdel
- self.expr = expr
- self.custom_comparator = custom_comparator
- self.update_expr = update_expr
- util.update_wrapper(self, fget)
-
- def __get__(self, instance, owner):
- if instance is None:
- return self._expr_comparator(owner)
- else:
- return self.fget(instance)
-
- def __set__(self, instance, value):
- if self.fset is None:
- raise AttributeError("can't set attribute")
- self.fset(instance, value)
-
- def __delete__(self, instance):
- if self.fdel is None:
- raise AttributeError("can't delete attribute")
- self.fdel(instance)
-
- def _copy(self, **kw):
- defaults = {
- key: value
- for key, value in self.__dict__.items()
- if not key.startswith("_")
- }
- defaults.update(**kw)
- return type(self)(**defaults)
-
- @property
- def overrides(self):
- """Prefix for a method that is overriding an existing attribute.
-
- The :attr:`.hybrid_property.overrides` accessor just returns
- this hybrid object, which when called at the class level from
- a parent class, will de-reference the "instrumented attribute"
- normally returned at this level, and allow modifying decorators
- like :meth:`.hybrid_property.expression` and
- :meth:`.hybrid_property.comparator`
- to be used without conflicting with the same-named attributes
- normally present on the :class:`.QueryableAttribute`::
-
- class SuperClass(object):
- # ...
-
- @hybrid_property
- def foobar(self):
- return self._foobar
-
- class SubClass(SuperClass):
- # ...
-
- @SuperClass.foobar.overrides.expression
- def foobar(cls):
- return func.subfoobar(self._foobar)
-
- .. versionadded:: 1.2
-
- .. seealso::
-
- :ref:`hybrid_reuse_subclass`
-
- """
- return self
-
- def getter(self, fget):
- """Provide a modifying decorator that defines a getter method.
-
- .. versionadded:: 1.2
-
- """
-
- return self._copy(fget=fget)
-
- def setter(self, fset):
- """Provide a modifying decorator that defines a setter method."""
-
- return self._copy(fset=fset)
-
- def deleter(self, fdel):
- """Provide a modifying decorator that defines a deletion method."""
-
- return self._copy(fdel=fdel)
-
- def expression(self, expr):
- """Provide a modifying decorator that defines a SQL-expression
- producing method.
-
- When a hybrid is invoked at the class level, the SQL expression given
- here is wrapped inside of a specialized :class:`.QueryableAttribute`,
- which is the same kind of object used by the ORM to represent other
- mapped attributes. The reason for this is so that other class-level
- attributes such as docstrings and a reference to the hybrid itself may
- be maintained within the structure that's returned, without any
- modifications to the original SQL expression passed in.
-
- .. note::
-
- When referring to a hybrid property from an owning class (e.g.
- ``SomeClass.some_hybrid``), an instance of
- :class:`.QueryableAttribute` is returned, representing the
- expression or comparator object as well as this hybrid object.
- However, that object itself has accessors called ``expression`` and
- ``comparator``; so when attempting to override these decorators on a
- subclass, it may be necessary to qualify it using the
- :attr:`.hybrid_property.overrides` modifier first. See that
- modifier for details.
-
- .. seealso::
-
- :ref:`hybrid_distinct_expression`
-
- """
-
- return self._copy(expr=expr)
-
- def comparator(self, comparator):
- """Provide a modifying decorator that defines a custom
- comparator producing method.
-
- The return value of the decorated method should be an instance of
- :class:`~.hybrid.Comparator`.
-
- .. note:: The :meth:`.hybrid_property.comparator` decorator
- **replaces** the use of the :meth:`.hybrid_property.expression`
- decorator. They cannot be used together.
-
- When a hybrid is invoked at the class level, the
- :class:`~.hybrid.Comparator` object given here is wrapped inside of a
- specialized :class:`.QueryableAttribute`, which is the same kind of
- object used by the ORM to represent other mapped attributes. The
- reason for this is so that other class-level attributes such as
- docstrings and a reference to the hybrid itself may be maintained
- within the structure that's returned, without any modifications to the
- original comparator object passed in.
-
- .. note::
-
- When referring to a hybrid property from an owning class (e.g.
- ``SomeClass.some_hybrid``), an instance of
- :class:`.QueryableAttribute` is returned, representing the
- expression or comparator object as this hybrid object. However,
- that object itself has accessors called ``expression`` and
- ``comparator``; so when attempting to override these decorators on a
- subclass, it may be necessary to qualify it using the
- :attr:`.hybrid_property.overrides` modifier first. See that
- modifier for details.
-
- """
- return self._copy(custom_comparator=comparator)
-
- def update_expression(self, meth):
- """Provide a modifying decorator that defines an UPDATE tuple
- producing method.
-
- The method accepts a single value, which is the value to be
- rendered into the SET clause of an UPDATE statement. The method
- should then process this value into individual column expressions
- that fit into the ultimate SET clause, and return them as a
- sequence of 2-tuples. Each tuple
- contains a column expression as the key and a value to be rendered.
-
- E.g.::
-
- class Person(Base):
- # ...
-
- first_name = Column(String)
- last_name = Column(String)
-
- @hybrid_property
- def fullname(self):
- return first_name + " " + last_name
-
- @fullname.update_expression
- def fullname(cls, value):
- fname, lname = value.split(" ", 1)
- return [
- (cls.first_name, fname),
- (cls.last_name, lname)
- ]
-
- .. versionadded:: 1.2
-
- """
- return self._copy(update_expr=meth)
-
- @util.memoized_property
- def _expr_comparator(self):
- if self.custom_comparator is not None:
- return self._get_comparator(self.custom_comparator)
- elif self.expr is not None:
- return self._get_expr(self.expr)
- else:
- return self._get_expr(self.fget)
-
- def _get_expr(self, expr):
- def _expr(cls):
- return ExprComparator(cls, expr(cls), self)
-
- util.update_wrapper(_expr, expr)
-
- return self._get_comparator(_expr)
-
- def _get_comparator(self, comparator):
-
- proxy_attr = attributes.create_proxied_attribute(self)
-
- def expr_comparator(owner):
- # because this is the descriptor protocol, we don't really know
- # what our attribute name is. so search for it through the
- # MRO.
- for lookup in owner.__mro__:
- if self.__name__ in lookup.__dict__:
- if lookup.__dict__[self.__name__] is self:
- name = self.__name__
- break
- else:
- name = attributes.NO_KEY
-
- return proxy_attr(
- owner,
- name,
- self,
- comparator(owner),
- doc=comparator.__doc__ or self.__doc__,
- )
-
- return expr_comparator
-
-
- class Comparator(interfaces.PropComparator):
- """A helper class that allows easy construction of custom
- :class:`~.orm.interfaces.PropComparator`
- classes for usage with hybrids."""
-
- property = None
-
- def __init__(self, expression):
- self.expression = expression
-
- def __clause_element__(self):
- expr = self.expression
- if hasattr(expr, "__clause_element__"):
- expr = expr.__clause_element__()
- return expr
-
- def adapt_to_entity(self, adapt_to_entity):
- # interesting....
- return self
-
-
- class ExprComparator(Comparator):
- def __init__(self, cls, expression, hybrid):
- self.cls = cls
- self.expression = expression
- self.hybrid = hybrid
-
- def __getattr__(self, key):
- return getattr(self.expression, key)
-
- @property
- def info(self):
- return self.hybrid.info
-
- def _bulk_update_tuples(self, value):
- if isinstance(value, elements.BindParameter):
- value = value.value
-
- if isinstance(self.expression, attributes.QueryableAttribute):
- return self.expression._bulk_update_tuples(value)
- elif self.hybrid.update_expr is not None:
- return self.hybrid.update_expr(self.cls, value)
- else:
- return [(self.expression, value)]
-
- @property
- def property(self):
- return self.expression.property
-
- def operate(self, op, *other, **kwargs):
- return op(self.expression, *other, **kwargs)
-
- def reverse_operate(self, op, other, **kwargs):
- return op(other, self.expression, **kwargs)
|