ORM attributes that can receive ad-hoc SQL expressions
------------------------------------------------------
-A new ORM attribute type :func:`.orm.deferred_expression` is added which
+A new ORM attribute type :func:`.orm.query_expression` is added which
is similar to :func:`.orm.deferred`, except its SQL expression
is determined at query time using a new option :func:`.orm.with_expression`;
if not specified, the attribute defaults to ``None``::
- from sqlalchemy.orm import deferred_expression
+ from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression
class A(Base):
y = Column(Integer)
# will be None normally...
- expr = deferred_expression()
+ expr = query_expression()
# but let's give it x + y
a1 = session.query(A).options(
.. seealso::
- :ref:`mapper_deferred_expression`
+ :ref:`mapper_query_expression`
:ticket:`3058`
.. autofunction:: deferred
-.. autofunction:: deferred_expression
+.. autofunction:: query_expression
.. autofunction:: load_only
in the usual case than both the hybrid and column property approaches, in that
it needs to emit a SQL query upon each access.
-.. _mapper_deferred_expression:
+.. _mapper_querytime_expression:
Query-time SQL expressions as mapped attributes
-----------------------------------------------
An option exists which can apply the ad-hoc ``A.x + A.y`` expression to the
returned ``A`` objects instead of as a separate tuple entry; this is the
:func:`.with_expression` query option in conjunction with the
-:func:`.deferred_expression` attribute mapping. The class is mapped
+:func:`.query_expression` attribute mapping. The class is mapped
to include a placeholder attribute where any particular SQL expression
may be applied::
- from sqlalchemy.orm import deferred_expression
+ from sqlalchemy.orm import query_expression
class A(Base):
__tablename__ = 'a'
x = Column(Integer)
y = Column(Integer)
- expr = deferred_expression()
+ expr = query_expression()
We can then query for objects of type ``A``, applying an arbitrary
SQL expression to be populated into ``A.expr``::
q = session.query(A).options(
with_expression(A.expr, A.x + A.y))
-The :func:`.deferred_expression` mapping has these caveats:
+The :func:`.query_expression` mapping has these caveats:
-* On an object where :func:`.deferred_expression` were not used to populate
+* On an object where :func:`.query_expression` were not used to populate
the attribute, the attribute on an object instance will have the value
``None``.
+* The query_expression value **does not refresh when the object is
+ expired**. Once the object is expired, either via :meth:`.Session.expire`
+ or via the expire_on_commit behavior of :meth:`.Session.commit`, the value is
+ removed from the attribute and will return ``None`` on subsequent access.
+ Only by running a new :class:`.Query` that touches the object which includes
+ a new :func:`.with_expression` directive will the attribute be set to a
+ non-None value.
+
* The mapped attribute currently **cannot** be applied to other parts of the
- query and make use of the ad-hoc expression; that is, this won't work::
+ query, such as the WHERE clause, the ORDER BY clause, and make use of the
+ ad-hoc expression; that is, this won't work::
+ # wont work
q = session.query(A).options(
with_expression(A.expr, A.x + A.y)
- ).filter(A.expr > 5)
+ ).filter(A.expr > 5).order_by(A.expr)
- The ``A.expr`` expression will resolve to NULL in the above WHERE clause.
- To use the expression throughout the query, assign to a variable and
- use that::
+ The ``A.expr`` expression will resolve to NULL in the above WHERE clause
+ and ORDER BY clause. To use the expression throughout the query, assign to a
+ variable and use that::
a_expr = A.x + A.y
q = session.query(A).options(
with_expression(A.expr, a_expr)
- ).filter(a_expr > 5)
+ ).filter(a_expr > 5).order_by(a_expr)
.. versionadded:: 1.2
return ColumnProperty(deferred=True, *columns, **kw)
-def deferred_expression():
+def query_expression():
"""Indicate an attribute that populates from a query-time SQL expression.
.. versionadded:: 1.2
.. seealso::
- :ref:`mapper_deferred_expression`
+ :ref:`mapper_query_expression`
"""
prop = ColumnProperty(_sql.null())
- prop.strategy_key = (("deferred_expression", True),)
+ prop.strategy_key = (("query_expression", True),)
return prop
mapper = public_factory(Mapper, ".orm.mapper")
callable_, dispatch, trackparent=False, extension=None,
compare_function=None, active_history=False,
parent_token=None, expire_missing=True,
- send_modified_events=True,
+ send_modified_events=True, accepts_scalar_loader=None,
**kwargs):
r"""Construct an AttributeImpl.
else:
self.is_equal = compare_function
+ if accepts_scalar_loader is not None:
+ self.accepts_scalar_loader = accepts_scalar_loader
+ else:
+ self.accepts_scalar_loader = self.default_accepts_scalar_loader
+
# TODO: pass in the manager here
# instead of doing a lookup
attr = manager_of_class(class_)[key]
__slots__ = (
'class_', 'key', 'callable_', 'dispatch', 'trackparent',
'parent_token', 'send_modified_events', 'is_equal', 'expire_missing',
- '_modified_token'
+ '_modified_token', 'accepts_scalar_loader'
)
def _init_modified_token(self):
class ScalarAttributeImpl(AttributeImpl):
"""represents a scalar value-holding InstrumentedAttribute."""
- accepts_scalar_loader = True
+ default_accepts_scalar_loader = True
uses_objects = False
supports_population = True
collection = False
"""
- accepts_scalar_loader = False
+ default_accepts_scalar_loader = False
uses_objects = True
supports_population = True
collection = False
semantics to the orm layer independent of the user data implementation.
"""
- accepts_scalar_loader = False
+ default_accepts_scalar_loader = False
uses_objects = True
supports_population = True
collection = True
class DynamicAttributeImpl(attributes.AttributeImpl):
uses_objects = True
- accepts_scalar_loader = False
+ default_accepts_scalar_loader = False
supports_population = False
collection = False
@log.class_logger
-@properties.ColumnProperty.strategy_for(deferred_expression=True)
+@properties.ColumnProperty.strategy_for(query_expression=True)
class ExpressionColumnLoader(ColumnLoader):
def __init__(self, parent, strategy_key):
super(ExpressionColumnLoader, self).__init__(parent, strategy_key)
else:
populators["expire"].append((self.key, True))
+ def init_class_attribute(self, mapper):
+ self.is_class_level = True
+
+ _register_attribute(
+ self.parent_property, mapper, useobject=False,
+ compare_function=self.columns[0].type.compare_values,
+ accepts_scalar_loader=False
+ )
+
@log.class_logger
@properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
def with_expression(loadopt, key, expression):
r"""Apply an ad-hoc SQL expression to a "deferred expression" attribute.
- This option is used in conjunction with the :func:`.orm.deferred_expression`
+ This option is used in conjunction with the :func:`.orm.query_expression`
mapper-level construct that indicates an attribute which should be the
target of an ad-hoc SQL expression.
.. seealso::
- :ref:`mapper_deferred_expression`
+ :ref:`mapper_query_expression`
"""
return loadopt.set_column_strategy(
(key, ),
- {"deferred_expression": True},
+ {"query_expression": True},
opts={"expression": expression}
)
from sqlalchemy.orm import mapper, deferred, defer, undefer, Load, \
load_only, undefer_group, create_session, synonym, relationship, Session,\
joinedload, defaultload, aliased, contains_eager, with_polymorphic, \
- deferred_expression, with_expression
+ query_expression, with_expression
from sqlalchemy.testing import eq_, AssertsCompiledSQL, assert_raises_message
from test.orm import _fixtures
from sqlalchemy.testing.schema import Column
x = Column(Integer)
y = Column(Integer)
- my_expr = deferred_expression()
+ my_expr = query_expression()
bs = relationship("B", order_by="B.id")
p = Column(Integer)
q = Column(Integer)
- b_expr = deferred_expression()
+ b_expr = query_expression()
@classmethod
def insert_data(cls):
def go():
eq_(a1.my_expr, None)
- self.assert_sql_count(testing.db, go, 0)
\ No newline at end of file
+ self.assert_sql_count(testing.db, go, 0)
+
+ def test_dont_explode_on_expire_individual(self):
+ A = self.classes.A
+
+ s = Session()
+ q = s.query(A).options(
+ with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+ order_by(A.id)
+
+ a1 = q.first()
+
+ eq_(a1.my_expr, 5)
+
+ s.expire(a1, ['my_expr'])
+
+ eq_(a1.my_expr, None)
+
+ # comes back
+ q = s.query(A).options(
+ with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+ order_by(A.id)
+ q.first()
+ eq_(a1.my_expr, 5)
+
+ def test_dont_explode_on_expire_whole(self):
+ A = self.classes.A
+
+ s = Session()
+ q = s.query(A).options(
+ with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+ order_by(A.id)
+
+ a1 = q.first()
+
+ eq_(a1.my_expr, 5)
+
+ s.expire(a1)
+
+ eq_(a1.my_expr, None)
+
+ # comes back
+ q = s.query(A).options(
+ with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+ order_by(A.id)
+ q.first()
+ eq_(a1.my_expr, 5)
+