From: Mike Bayer Date: Mon, 26 Jun 2017 17:24:22 +0000 (-0400) Subject: Support state expiration for with_expression(); rename deferred_expression X-Git-Tag: rel_1_2_0b1~12^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9ac0f8119e34a696fbf711e00262e9c0851b749c;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Support state expiration for with_expression(); rename deferred_expression The attributeimpl for a deferred_expression does not support a scalar loader, add new configurability so that the impl can have this flag turned off. Document that the with_expression() system currently does not offer any deferred loading. To eliminate confusion over "deferred", which refers to lazy loading of column attributes, and "with_expression", which refers to an attribute that is explicitly at query time only, rename deferred_expression to query_expression. Change-Id: I07c4a050ed68c79ccbde9492e9de1630b7470d74 --- diff --git a/doc/build/changelog/migration_12.rst b/doc/build/changelog/migration_12.rst index add12a50c3..52990d3fbb 100644 --- a/doc/build/changelog/migration_12.rst +++ b/doc/build/changelog/migration_12.rst @@ -218,12 +218,12 @@ are loaded with additional SELECT statements: 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): @@ -233,7 +233,7 @@ if not specified, the attribute defaults to ``None``:: 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( @@ -242,7 +242,7 @@ if not specified, the attribute defaults to ``None``:: .. seealso:: - :ref:`mapper_deferred_expression` + :ref:`mapper_query_expression` :ticket:`3058` diff --git a/doc/build/orm/loading_columns.rst b/doc/build/orm/loading_columns.rst index 289ba5c332..8766dac1b4 100644 --- a/doc/build/orm/loading_columns.rst +++ b/doc/build/orm/loading_columns.rst @@ -136,7 +136,7 @@ Column Deferral API .. autofunction:: deferred -.. autofunction:: deferred_expression +.. autofunction:: query_expression .. autofunction:: load_only diff --git a/doc/build/orm/mapped_sql_expr.rst b/doc/build/orm/mapped_sql_expr.rst index 7d0481d9cc..425a4ece82 100644 --- a/doc/build/orm/mapped_sql_expr.rst +++ b/doc/build/orm/mapped_sql_expr.rst @@ -206,7 +206,7 @@ The plain descriptor approach is useful as a last resort, but is less performant 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 ----------------------------------------------- @@ -223,11 +223,11 @@ The above query returns tuples of the form ``(A object, integer)``. 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' @@ -235,7 +235,7 @@ may be applied:: 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``:: @@ -244,27 +244,37 @@ 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 diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 80c27e392d..846d2c2a75 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -178,18 +178,18 @@ def deferred(*columns, **kw): 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") diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index f3a5c47353..01cb0056ee 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -388,7 +388,7 @@ class AttributeImpl(object): 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. @@ -449,6 +449,11 @@ class AttributeImpl(object): 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] @@ -465,7 +470,7 @@ class AttributeImpl(object): __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): @@ -657,7 +662,7 @@ class AttributeImpl(object): 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 @@ -743,7 +748,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): """ - accepts_scalar_loader = False + default_accepts_scalar_loader = False uses_objects = True supports_population = True collection = False @@ -867,7 +872,7 @@ class CollectionAttributeImpl(AttributeImpl): 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 diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index 9f99740d9a..bb63067b05 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -44,7 +44,7 @@ class DynaLoader(strategies.AbstractRelationshipLoader): class DynamicAttributeImpl(attributes.AttributeImpl): uses_objects = True - accepts_scalar_loader = False + default_accepts_scalar_loader = False supports_population = False collection = False diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 4e7636cf83..4b9eb3b0fd 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -195,7 +195,7 @@ class ColumnLoader(LoaderStrategy): @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) @@ -235,6 +235,15 @@ class ExpressionColumnLoader(ColumnLoader): 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) diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 2cdf6ba957..796f859f81 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -1356,7 +1356,7 @@ from .util import _orm_full_deannotate 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. @@ -1375,7 +1375,7 @@ def with_expression(loadopt, key, expression): .. seealso:: - :ref:`mapper_deferred_expression` + :ref:`mapper_query_expression` """ @@ -1384,7 +1384,7 @@ def with_expression(loadopt, key, expression): return loadopt.set_column_strategy( (key, ), - {"deferred_expression": True}, + {"query_expression": True}, opts={"expression": expression} ) diff --git a/test/orm/test_deferred.py b/test/orm/test_deferred.py index 9c2dc4c8b7..9295399bcc 100644 --- a/test/orm/test_deferred.py +++ b/test/orm/test_deferred.py @@ -3,7 +3,7 @@ from sqlalchemy import testing, util 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 @@ -886,7 +886,7 @@ class WithExpressionTest(fixtures.DeclarativeMappedTest): x = Column(Integer) y = Column(Integer) - my_expr = deferred_expression() + my_expr = query_expression() bs = relationship("B", order_by="B.id") @@ -897,7 +897,7 @@ class WithExpressionTest(fixtures.DeclarativeMappedTest): p = Column(Integer) q = Column(Integer) - b_expr = deferred_expression() + b_expr = query_expression() @classmethod def insert_data(cls): @@ -972,4 +972,51 @@ class WithExpressionTest(fixtures.DeclarativeMappedTest): 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) +