summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2010-08-02 15:29:31 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2010-08-02 15:29:31 -0400
commite616c2fb3cbc1f2fb7102f3fa666439c688e48b7 (patch)
tree7b635c217af1bf675956dbfd9ce27a679249060c
parentb2c0b50bbfa43f662afd16b7ca51bcfe17e4c351 (diff)
downloadsqlalchemy-e616c2fb3cbc1f2fb7102f3fa666439c688e48b7.tar.gz
- 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
-rw-r--r--CHANGES10
-rw-r--r--doc/build/mappers.rst13
-rwxr-xr-xlib/sqlalchemy/ext/declarative.py97
-rw-r--r--lib/sqlalchemy/sql/expression.py2
-rw-r--r--test/ext/test_declarative.py34
5 files changed, 136 insertions, 20 deletions
diff --git a/CHANGES b/CHANGES
index ae1763028..af4fdb8d8 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 701e95fac..a6e25e7d0 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 3370a764c..7f9dacbb0 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 050b5c05b..96147a94a 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 4da826d38..ef79b849a 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):