]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- if @classproperty is used with a regular class-bound
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 2 Aug 2010 19:29:31 +0000 (15:29 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 2 Aug 2010 19:29:31 +0000 (15:29 -0400)
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

CHANGES
doc/build/mappers.rst
lib/sqlalchemy/ext/declarative.py
lib/sqlalchemy/sql/expression.py
test/ext/test_declarative.py

diff --git a/CHANGES b/CHANGES
index ae17630283759454002078658945b04ecb807920..af4fdb8d830560dc53fb145b28bce08d48004607 100644 (file)
--- 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
index 701e95fac5c18e51fd664c9e48b2235780224c74..a6e25e7d0df6e06fd2dd3e4632886e272e10e631 100644 (file)
@@ -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
 ----------------------------
 
index 3370a764ca5d41b0567712c2d1c0de5fec5a56ad..7f9dacbb04c212486223a90e5501ff4a99ba5c24 100755 (executable)
@@ -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 "
index 050b5c05b98641dcb47f6f9b92affe0a79c9420a..96147a94a0e8fc2589a0831d44d68cea7c675607 100644 (file)
@@ -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
index 4da826d389cd51f0f4add23e6645c42b2b06e7f8..ef79b849ad1ef20fe817e80bef870a3b07b9b981 100644 (file)
@@ -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):