summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext/hybrid.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2011-01-17 20:05:09 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2011-01-17 20:05:09 -0500
commitdc7d611182c9a0c364f334180c6c7a4ca99f79e6 (patch)
treed2933ea290156d4f6d47a3086907431c38f75edc /lib/sqlalchemy/ext/hybrid.py
parentcbaa4c9d5d51308aeb5331b177e16d65c6dda163 (diff)
downloadsqlalchemy-dc7d611182c9a0c364f334180c6c7a4ca99f79e6.tar.gz
- tests for hybrid
- documentation for hybrid - rewrite descriptor, synonym, comparable_property documentation
Diffstat (limited to 'lib/sqlalchemy/ext/hybrid.py')
-rw-r--r--lib/sqlalchemy/ext/hybrid.py321
1 files changed, 309 insertions, 12 deletions
diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py
index 9fb8a27d8..5c747400d 100644
--- a/lib/sqlalchemy/ext/hybrid.py
+++ b/lib/sqlalchemy/ext/hybrid.py
@@ -4,34 +4,35 @@
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Define attributes on ORM-mapped classes that have 'hybrid' behavior.
+"""Define attributes on ORM-mapped classes that have "hybrid" behavior.
-'hybrid' means the attribute has distinct behaviors defined at the
+"hybrid" means the attribute has distinct behaviors defined at the
class level and at the instance level.
-Consider a table `interval` as below::
+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 class-level expression generator.
+
+Consider a table ``interval`` as below::
from sqlalchemy import MetaData, Table, Column, Integer
- from sqlalchemy.orm import mapper, create_session
- engine = create_engine('sqlite://')
metadata = MetaData()
interval_table = Table('interval', metadata,
Column('id', Integer, primary_key=True),
Column('start', Integer, nullable=False),
- Column('end', Integer, nullable=False))
- metadata.create_all(engine)
+ Column('end', Integer, nullable=False)
+ )
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 :func:`hybrid.method`
-or :func:`hybrid.property` may receive ``self`` as an instance of the class,
+instance level. Below, each function decorated with :func:`.hybrid_method`
+or :func:`.hybrid_property` may receive ``self`` as an instance of the class,
or as the class itself::
- # A base class for intervals
-
- from sqlalchemy.orm.hybrid import hybrid_property, hybrid_method
+ from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
+ from sqlalchemy.orm import mapper, Session, aliased
class Interval(object):
def __init__(self, start, end):
@@ -49,15 +50,271 @@ or as the class itself::
@hybrid_method
def intersects(self, other):
return self.contains(other.start) | self.contains(other.end)
+
+ mapper(Interval, interval_table)
+
+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
+
+At the class level, the usual descriptor behavior of returning the descriptor
+itself is modified by :class:`.hybrid_property`, to instead evaluate the function
+body given the ``Interval`` class as the argument::
+
+ >>> print Interval.length
+ 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.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 ``contains()`` and ``intersects()`` methods are decorated with :class:`.hybrid_method`.
+This decorator applies the same idea to methods which accept
+zero or more arguments. The above 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"
+
+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 :attr:`.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
+
+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
+
+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.
+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 = balance
+
+ @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, we can't travel along relationships to column attributes
+directly since SQLAlchemy is explicit about joins. So here, 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
+
+.. _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.
+
+The example class below allows case-insensitive comparisons on the attribute
+named ``word_insensitive``::
+
+ from sqlalchemy.ext.hybrid import Comparator
+
+ 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)
"""
from sqlalchemy import util
from sqlalchemy.orm import attributes, interfaces
+import new
class hybrid_method(object):
+ """A decorator which allows definition of a Python object method with both
+ instance-level and class-level behavior.
+
+ """
+
+
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
+
+ @hybrid_property.expression
+ def value(self, x, y):
+ return func.some_function(self._value, x, y)
+
+ """
self.func = func
self.expr = expr or func
@@ -68,11 +325,34 @@ class hybrid_method(object):
return new.instancemethod(self.func, instance, owner)
def expression(self, expr):
+ """Provide a modifying decorator that defines a SQL-expression producing method."""
+
self.expr = expr
return self
class hybrid_property(object):
+ """A decorator which allows definition of a Python descriptor with both
+ instance-level and class-level behavior.
+
+ """
+
def __init__(self, fget, fset=None, fdel=None, 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
+
+ @hybrid_property.setter
+ def value(self, value):
+ self._value = value
+
+ """
self.fget = fget
self.fset = fset
self.fdel = fdel
@@ -92,18 +372,31 @@ class hybrid_property(object):
self.fdel(instance)
def setter(self, fset):
+ """Provide a modifying decorator that defines a value-setter method."""
+
self.fset = fset
return self
def deleter(self, fdel):
+ """Provide a modifying decorator that defines a value-deletion method."""
+
self.fdel = fdel
return self
def expression(self, expr):
+ """Provide a modifying decorator that defines a SQL-expression producing method."""
+
self.expr = expr
return self
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`.
+
+ """
+
proxy_attr = attributes.\
create_proxied_attribute(self)
def expr(owner):
@@ -113,6 +406,10 @@ class hybrid_property(object):
class Comparator(interfaces.PropComparator):
+ """A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator`
+ classes for usage with hybrids."""
+
+
def __init__(self, expression):
self.expression = expression