From: Mike Bayer Date: Mon, 2 Aug 2010 19:29:31 +0000 (-0400) Subject: - if @classproperty is used with a regular class-bound X-Git-Tag: rel_0_6_4~64 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e616c2fb3cbc1f2fb7102f3fa666439c688e48b7;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - if @classproperty is used with a regular class-bound mapper property attribute, it will be called to get the actual attribute value during initialization. Currently, there's no advantage to using @classproperty on a column or relationship attribute of a declarative class that isn't a mixin - evaluation is at the same time as if @classproperty weren't used. But here we at least allow it to function as expected. - docs for column_property() with declarative - mixin docs in declarative made more clear - mixins are optional - each subsection starts with, "in *declarative mixins*", to reduce confusion --- diff --git a/CHANGES b/CHANGES index ae17630283..af4fdb8d83 100644 --- a/CHANGES +++ b/CHANGES @@ -66,6 +66,16 @@ CHANGES this to appease MySQL who has a max length of 64 for index names, separate from their overall max length of 255. [ticket:1412] + +- declarative + - if @classproperty is used with a regular class-bound + mapper property attribute, it will be called to get the + actual attribute value during initialization. Currently, + there's no advantage to using @classproperty on a column + or relationship attribute of a declarative class that + isn't a mixin - evaluation is at the same time as if + @classproperty weren't used. But here we at least allow + it to function as expected. - mssql - Fixed "default schema" query to work with diff --git a/doc/build/mappers.rst b/doc/build/mappers.rst index 701e95fac5..a6e25e7d0d 100644 --- a/doc/build/mappers.rst +++ b/doc/build/mappers.rst @@ -204,11 +204,16 @@ And an entire "deferred group", i.e. which uses the ``group`` keyword argument t SQL Expressions as Mapped Attributes ------------------------------------- -To add a SQL clause composed of local or external columns as a read-only, mapped column attribute, use the :func:`~sqlalchemy.orm.column_property()` function. Any scalar-returning :class:`~sqlalchemy.sql.expression.ClauseElement` may be used, as long as it has a ``name`` attribute; usually, you'll want to call ``label()`` to give it a specific name:: +To add a SQL clause composed of local or external columns as +a read-only, mapped column attribute, use the +:func:`~sqlalchemy.orm.column_property()` function. Any +scalar-returning +:class:`~sqlalchemy.sql.expression.ClauseElement` may be +used. Unlike older versions of SQLAlchemy, there is no :func:`~.sql.expression.label` requirement:: mapper(User, users_table, properties={ 'fullname': column_property( - (users_table.c.firstname + " " + users_table.c.lastname).label('fullname') + users_table.c.firstname + " " + users_table.c.lastname ) }) @@ -221,10 +226,12 @@ Correlated subqueries may be used as well: select( [func.count(addresses_table.c.address_id)], addresses_table.c.user_id==users_table.c.user_id - ).label('address_count') + ) ) }) +The declarative form of the above is described in :ref:`declarative_sql_expressions`. + Changing Attribute Behavior ---------------------------- diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 3370a764ca..7f9dacbb04 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -233,6 +233,61 @@ Similarly, :func:`comparable_using` is a front end for the def uc_name(self): return self.name.upper() +.. _declarative_sql_expressions: + +Defining SQL Expressions +======================== + +The usage of :func:`.column_property` with Declarative is +pretty much the same as that described in +:ref:`mapper_sql_expressions`. Local columns within the same +class declaration can be referenced directly:: + + class User(Base): + __tablename__ = 'user' + id = Column(Integer, primary_key=True) + firstname = Column(String) + lastname = Column(String) + fullname = column_property( + firstname + " " + lastname + ) + +Correlated subqueries reference the :class:`Column` objects they +need either from the local class definition or from remote +classes:: + + from sqlalchemy.sql import func + + class Address(Base): + __tablename__ = 'address' + + id = Column('id', Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('user.id')) + + class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + name = Column(String) + + address_count = column_property( + select([func.count(Address.id)]).\\ + where(Address.user_id==id) + ) + +In the case that the ``address_count`` attribute above doesn't have access to +``Address`` when ``User`` is defined, the ``address_count`` attribute should +be added to ``User`` when both ``User`` and ``Address`` are available (i.e. +there is no string based "late compilation" feature like there is with +:func:`.relationship` at this time). Note we reference the ``id`` column +attribute of ``User`` with its class when we are no longer in the declaration +of the ``User`` class:: + + User.address_count = column_property( + select([func.count(Address.id)]).\\ + where(Address.user_id==User.id) + ) + Table Configuration =================== @@ -429,6 +484,11 @@ share some functionality, often a set of columns, across many classes. The normal Python idiom would be to put this common code into a base class and have all the other classes subclass this class. +.. note:: Mixins are an entirely optional feature when using declarative, + and are not required for any configuration. Users who don't need + to define sets of attributes common among many classes can + skip this section. + When using :mod:`~sqlalchemy.ext.declarative`, this need is met by using a "mixin class". A mixin class is one that isn't mapped to a table and doesn't subclass the declarative :class:`Base`. For example:: @@ -531,12 +591,12 @@ Mixing in Relationships ~~~~~~~~~~~~~~~~~~~~~~~ Relationships created by :func:`~sqlalchemy.orm.relationship` are provided -exclusively using the :func:`~sqlalchemy.util.classproperty` approach, -eliminating any ambiguity which could arise when copying a relationship -and its possibly column-bound contents. Below is an example which -combines a foreign key column and a relationship so that two classes -``Foo`` and ``Bar`` can both be configured to reference a common -target class via many-to-one:: +with declarative mixin classes exclusively using the +:func:`~sqlalchemy.util.classproperty` approach, eliminating any ambiguity +which could arise when copying a relationship and its possibly column-bound +contents. Below is an example which combines a foreign key column and a +relationship so that two classes ``Foo`` and ``Bar`` can both be configured to +reference a common target class via many-to-one:: class RefTargetMixin(object): @classproperty @@ -586,9 +646,9 @@ Mixing in deferred(), column_property(), etc. Like :func:`~sqlalchemy.orm.relationship`, all :class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as :func:`~sqlalchemy.orm.deferred`, :func:`~sqlalchemy.orm.column_property`, -etc. ultimately involve references to columns, and therefore have the -:func:`~sqlalchemy.util.classproperty` requirement so that no reliance on -copying is needed:: +etc. ultimately involve references to columns, and therefore, when +used with declarative mixins, have the :func:`~sqlalchemy.util.classproperty` +requirement so that no reliance on copying is needed:: class SomethingMixin(object): @@ -607,7 +667,8 @@ Controlling table inheritance with mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``__tablename__`` attribute in conjunction with the hierarchy of -the classes involved controls what type of table inheritance, if any, +classes involved in a declarative mixin scenario controls what type of +table inheritance, if any, is configured by the declarative extension. If the ``__tablename__`` is computed by a mixin, you may need to @@ -700,12 +761,13 @@ classes:: Combining Table/Mapper Arguments from Multiple Mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the case of ``__table_args__`` or ``__mapper_args__``, you may want -to combine some parameters from several mixins with those you wish to -define on the class iteself. The -:func:`~sqlalchemy.util.classproperty` decorator can be used here -to create user-defined collation routines that pull from multiple -collections:: +In the case of ``__table_args__`` or ``__mapper_args__`` +specified with declarative mixins, you may want to combine +some parameters from several mixins with those you wish to +define on the class iteself. The +:func:`~sqlalchemy.util.classproperty` decorator can be used +here to create user-defined collation routines that pull +from multiple collections:: from sqlalchemy.util import classproperty @@ -868,6 +930,9 @@ def _as_declarative(cls, classname, dict_): our_stuff = util.OrderedDict() for k in dict_: value = dict_[k] + if isinstance(value, util.classproperty): + value = getattr(cls, k) + if (isinstance(value, tuple) and len(value) == 1 and isinstance(value[0], (Column, MapperProperty))): util.warn("Ignoring declarative-like tuple value of attribute " diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 050b5c05b9..96147a94a0 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1813,7 +1813,7 @@ class ColumnElement(ClauseElement, _CompareMixin): else: name = str(self) co = ColumnClause(self.anon_label, selectable, type_=getattr(self, 'type', None)) - + co.proxies = [self] selectable.columns[name] = co return co diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py index 4da826d389..ef79b849ad 100644 --- a/test/ext/test_declarative.py +++ b/test/ext/test_declarative.py @@ -690,6 +690,40 @@ class DeclarativeTest(DeclarativeTestBase): eq_(sess.query(User).all(), [User(name='u1', address_count=2, addresses=[Address(email='one'), Address(email='two')])]) + def test_useless_classproperty(self): + class Address(Base, ComparableEntity): + + __tablename__ = 'addresses' + id = Column('id', Integer, primary_key=True, + test_needs_autoincrement=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + + class User(Base, ComparableEntity): + + __tablename__ = 'users' + id = Column('id', Integer, primary_key=True, + test_needs_autoincrement=True) + name = Column('name', String(50)) + addresses = relationship('Address', backref='user') + + @classproperty + def address_count(cls): + # this doesn't really gain us anything. but if + # one is used, lets have it function as expected... + return sa.orm.column_property(sa.select([sa.func.count(Address.id)]). + where(Address.user_id == cls.id)) + + Base.metadata.create_all() + u1 = User(name='u1', addresses=[Address(email='one'), + Address(email='two')]) + sess = create_session() + sess.add(u1) + sess.flush() + sess.expunge_all() + eq_(sess.query(User).all(), [User(name='u1', address_count=2, + addresses=[Address(email='one'), Address(email='two')])]) + def test_column(self): class User(Base, ComparableEntity):