diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2011-01-17 20:05:09 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2011-01-17 20:05:09 -0500 |
commit | dc7d611182c9a0c364f334180c6c7a4ca99f79e6 (patch) | |
tree | d2933ea290156d4f6d47a3086907431c38f75edc /lib/sqlalchemy/ext/hybrid.py | |
parent | cbaa4c9d5d51308aeb5331b177e16d65c6dda163 (diff) | |
download | sqlalchemy-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.py | 321 |
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 |