]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- rewrite the docs for association proxy using declarative, add new examples, querying,
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 6 Aug 2011 01:29:21 +0000 (21:29 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 6 Aug 2011 01:29:21 +0000 (21:29 -0400)
etc., part of [ticket:2246]
- add some accessors to AssociationProxy for attributes, test in join(), [ticket:2236]
- update relationship docs to talk about callables, part of [ticket:2246]

doc/build/orm/examples.rst
doc/build/orm/extensions/associationproxy.rst
doc/build/orm/relationships.rst
lib/sqlalchemy/ext/associationproxy.py
lib/sqlalchemy/orm/__init__.py
test/ext/test_associationproxy.py

index 23140623249c917c257add48f855b037388208f4..20f74937fd9ffbefc09abff75bd4b9d7903a2f31 100644 (file)
@@ -16,6 +16,8 @@ Location: /examples/adjacency_list/
 
 .. automodule:: adjacency_list
 
+.. _examples_associations:
+
 Associations
 ------------
 
index c0b99cc5c590bc2895bb5ae4716b902613acfe6a..c78850cb7f0c4b6829f4ff8640bf1c41741cfcc3 100644 (file)
@@ -5,83 +5,100 @@ Association Proxy
 
 .. module:: sqlalchemy.ext.associationproxy
 
-``associationproxy`` is used to create a simplified, read/write view of a
-relationship.  It can be used to cherry-pick fields from a collection of
-related objects or to greatly simplify access to associated objects in an
-association relationship.
+``associationproxy`` is used to create a read/write view of a
+target attribute across a relationship.  It essentially conceals
+the usage of a "middle" attribute between two endpoints, and 
+can be used to cherry-pick fields from a collection of
+related objects or to reduce the verbosity of using the association
+object pattern.   Applied creatively, the association proxy allows
+the construction of sophisticated collections and dictionary 
+views of virtually any geometry, persisted to the database using
+standard, transparently configured relational patterns.
+
+Simplifying Scalar Collections
+------------------------------
+
+Consider a many-to-many mapping between two classes, ``User`` and ``Keyword``.
+Each ``User`` can have any number of ``Keyword`` objects, and vice-versa
+(the many-to-many pattern is described at :ref:`relationships_many_to_many`)::
+
+    from sqlalchemy import Column, Integer, String, ForeignKey, Table
+    from sqlalchemy.orm import relationship
+    from sqlalchemy.ext.declarative import declarative_base
+
+    Base = declarative_base()
+
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(64))
+        kw = relationship("Keyword", secondary=lambda: userkeywords_table)
 
-Simplifying Relationships
--------------------------
-
-Consider this "association object" mapping::
+        def __init__(self, name):
+            self.name = name
 
-    users_table = Table('users', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('name', String(64)),
-    )
+    class Keyword(Base):
+        __tablename__ = 'keyword'
+        id = Column(Integer, primary_key=True)
+        keyword = Column('keyword', String(64))
 
-    keywords_table = Table('keywords', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('keyword', String(64))
-    )
+        def __init__(self, keyword):
+            self.keyword = keyword
 
-    userkeywords_table = Table('userkeywords', metadata,
-        Column('user_id', Integer, ForeignKey("users.id"),
+    userkeywords_table = Table('userkeywords', Base.metadata,
+        Column('user_id', Integer, ForeignKey("user.id"),
                primary_key=True),
-        Column('keyword_id', Integer, ForeignKey("keywords.id"),
+        Column('keyword_id', Integer, ForeignKey("keyword.id"),
                primary_key=True)
     )
 
-    class User(object):
-        def __init__(self, name):
-            self.name = name
-
-    class Keyword(object):
-        def __init__(self, keyword):
-            self.keyword = keyword
-
-    mapper(User, users_table, properties={
-        'kw': relationship(Keyword, secondary=userkeywords_table)
-        })
-    mapper(Keyword, keywords_table)
-
-Above are three simple tables, modeling users, keywords and a many-to-many
-relationship between the two.  These ``Keyword`` objects are little more
-than a container for a name, and accessing them via the relationship is
-awkward::
-
-    user = User('jek')
-    user.kw.append(Keyword('cheese inspector'))
-    print user.kw
-    # [<__main__.Keyword object at 0xb791ea0c>]
-    print user.kw[0].keyword
-    # 'cheese inspector'
-    print [keyword.keyword for keyword in user.kw]
-    # ['cheese inspector']
+We can append ``Keyword`` objects to a target ``User``, and access the
+``.keyword`` attribute of each ``Keyword`` in order to see the value, but
+the extra hop introduced by ``.kw`` can be awkward::
+
+    >>> user = User('jek')
+    >>> user.kw.append(Keyword('cheese inspector'))
+    >>> print user.kw
+    [<__main__.Keyword object at 0x12bf830>]
+    >>> print user.kw[0].keyword
+    cheese inspector
+    >>> print [keyword.keyword for keyword in user.kw]
+    ['cheese inspector']
 
 With ``association_proxy`` you have a "view" of the relationship that contains
-just the ``.keyword`` of the related objects.  The proxy is a Python
-property, and unlike the mapper relationship, is defined in your class::
+just the ``.keyword`` of the related objects.  Below we illustrate
+how to bridge the gap between the ``kw`` collection and the ``keyword``
+attribute of each ``Keyword``::
 
     from sqlalchemy.ext.associationproxy import association_proxy
 
-    class User(object):
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(64))
+        kw = relationship("Keyword", secondary=lambda: userkeywords_table)
+
         def __init__(self, name):
             self.name = name
 
         # proxy the 'keyword' attribute from the 'kw' relationship
         keywords = association_proxy('kw', 'keyword')
 
-    # ...
-    >>> user.kw
-    [<__main__.Keyword object at 0xb791ea0c>]
+We can now reference the ``.keywords`` collection as a listing of strings,
+which is both readable and writeable::
+
+    >>> user = User('jek')
+    >>> user.keywords.append('cheese inspector')
     >>> user.keywords
     ['cheese inspector']
     >>> user.keywords.append('snack ninja')
-    >>> user.keywords
-    ['cheese inspector', 'snack ninja']
     >>> user.kw
-    [<__main__.Keyword object at 0x9272a4c>, <__main__.Keyword object at 0xb7b396ec>]
+    [<__main__.Keyword object at 0x12cdd30>, <__main__.Keyword object at 0x12cde30>]
+
+The association proxy is nothing more than a Python `descriptor <http://docs.python.org/howto/descriptor.html>`_, 
+as opposed to a :class:`.MapperProperty`-based construct such as :func:`.relationship` or :func:`.column_property`.  
+It is always defined in a declarative fashion along with its parent class, regardless of 
+whether or not Declarative or classical mappings are used.
 
 The proxy is read/write.  New associated objects are created on demand when
 values are added to the proxy, and modifying or removing an entry through
@@ -106,197 +123,335 @@ the proxy also affects the underlying collection.
  - To construct new instances, the type is called with the value being
    assigned, or key and value for dicts.
 
- - A ````creator```` function can be used to create instances instead.
+ - A "creator" function can be used to create instances instead.
 
 Above, the ``Keyword.__init__`` takes a single argument ``keyword``, which
 maps conveniently to the value being set through the proxy.  A ``creator``
-function could have been used instead if more flexibility was required.
+function can be used if more flexibility is required.
 
 Because the proxies are backed by a regular relationship collection, all of the
 usual hooks and patterns for using collections are still in effect.  The
 most convenient behavior is the automatic setting of "parent"-type
 relationships on assignment.  In the example above, nothing special had to
-be done to associate the Keyword to the User.  Simply adding it to the
+be done to associate the ``Keyword`` to the ``User``.  Simply adding it to the
 collection is sufficient.
 
-Simplifying Association Object Relationships
---------------------------------------------
+Simplifying Association Proxies
+-------------------------------
 
-Association proxies are also useful for keeping ``association objects`` out
-the way during regular use.  For example, the ``userkeywords`` table
-might have a bunch of auditing columns that need to get updated when changes
-are made- columns that are updated but seldom, if ever, accessed in your
-application.  A proxy can provide a very natural access pattern for the
-relationship.
+Association proxies are useful for keeping "association objects" out
+the way during regular use.  The "association object" pattern is an
+extended form of a many-to-many relationship, and is described at
+:ref:`association_pattern`.
 
-.. sourcecode:: python
+Suppose our ``userkeywords`` table above had additional columns
+which we'd like to map explicitly, but in most cases we don't 
+require direct access to these attributes.  Below, we illustrate
+a new mapping which introduces the ``UserKeyword`` class, which 
+is mapped to the ``userkeywords`` table illustrated earlier.
+This class adds an additional column ``special_key``.   We
+create an association proxy on the ``User`` class called
+``keywords``, which will bridge the gap from the ``user_keywords``
+collection to the ``Keyword`` object referenced by each 
+``UserKeyword``::
 
-    from sqlalchemy.ext.associationproxy import association_proxy
-
-    # users_table and keywords_table tables as above, then:
+    from sqlalchemy import Column, Integer, String, ForeignKey
+    from sqlalchemy.orm import relationship, backref
 
-    def get_current_uid():
-        """Return the uid of the current user."""
-        return 1  # hardcoded for this example
+    from sqlalchemy.ext.associationproxy import association_proxy
+    from sqlalchemy.ext.declarative import declarative_base
 
-    userkeywords_table = Table('userkeywords', metadata,
-        Column('user_id', Integer, ForeignKey("users.id"), primary_key=True),
-        Column('keyword_id', Integer, ForeignKey("keywords.id"), primary_key=True),
-        # add some auditing columns
-        Column('updated_at', DateTime, default=datetime.now),
-        Column('updated_by', Integer, default=get_current_uid, onupdate=get_current_uid),
-    )
+    Base = declarative_base()
 
-    def _create_uk_by_keyword(keyword):
-        """A creator function."""
-        return UserKeyword(keyword=keyword)
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(64))
+        keywords = association_proxy('user_keywords', 'keyword')
 
-    class User(object):
         def __init__(self, name):
             self.name = name
-        keywords = association_proxy('user_keywords', 'keyword', creator=_create_uk_by_keyword)
 
-    class Keyword(object):
+    class Keyword(Base):
+        __tablename__ = 'keyword'
+        id = Column(Integer, primary_key=True)
+        keyword = Column('keyword', String(64))
+
         def __init__(self, keyword):
             self.keyword = keyword
+
         def __repr__(self):
             return 'Keyword(%s)' % repr(self.keyword)
 
-    class UserKeyword(object):
-        def __init__(self, user=None, keyword=None):
+    class UserKeyword(Base):
+        __tablename__ = 'user_keyword'
+        user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
+        keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
+        special_key = Column(String(50))
+        user = relationship(User, backref=backref("user_keywords", cascade="all, delete-orphan"))
+        keyword = relationship(Keyword)
+
+        def __init__(self, keyword=None, user=None, special_key=None):
             self.user = user
             self.keyword = keyword
+            self.special_key = special_key
+
+With the above mapping, we first illustrate usage of the ``UserKeyword`` class
+explicitly, creating a ``User``, ``Keyword``, and the association::
+
+    >>> user = User('log')
+    >>> kw1  = Keyword('new_from_blammo')
+
+    >>> # Creating a UserKeyword association object will add a Keyword.
+    ... # the "user" reference assignment in the UserKeyword() constructor
+    ... # populates "user_keywords" via backref.
+    ... UserKeyword(kw1, user)
+    <__main__.UserKeyword object at 0x12d9a50>
+
+    >>> # Accessing Keywords requires traversing UserKeywords
+    ... print user.user_keywords[0]
+    <__main__.UserKeyword object at 0x12d9a50>
+
+    >>> print user.user_keywords[0].keyword
+    Keyword('new_from_blammo')
+
+Next we illustrate using the association proxy, which is accessible via
+the ``keywords`` attribute on each ``User`` object.  Using the proxy,
+the ``UserKeyword`` object is created for us automatically, passing in
+the given ``Keyword`` object as the first positional argument by default::
+
+    >>> for kw in (Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')):
+    ...     user.keywords.append(kw)
+    ... 
+    >>> print user.keywords
+    [Keyword('new_from_blammo'), Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')]
+
+In each call to ``keywords.append()``, the association proxy performs the
+operation as::
+
+    user.user_keywords.append(UserKeyword(kw))
+
+As each ``UserKeyword`` is added to the ``.user_keywords`` collection, the bidirectional
+relationship established between ``User.user_keywords`` and ``UserKeyword.user`` establishes
+the parent ``User`` as the current value of ``UserKeyword.user``, and the new ``UserKeyword``
+object is fully populated.
+
+Using the creator argument
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The above example featured usage of the default "creation" function for the association proxy,
+which is to call the constructor of the class mapped by the first attribute, in this case
+that of ``UserKeyword``.  It is often necessary to supply a custom construction function
+specific to the context in which the association proxy is used.   For example, if
+we wanted the ``special_key`` argument to be populated specifically when the 
+association proxy collection were used.    The ``creator`` argument is given a single-argument
+function to achieve this, often using a lambda for succinctness::
+
+    class User(Base):
+        # ...
+
+        keywords = association_proxy('user_keywords', 'keyword', 
+                        creator=lambda k:UserKeyword(keyword=k, special_key='special'))
+
+When the proxied collection is based on a Python mapping (e.g. a ``dict``-like object),
+the ``creator`` argument accepts a **two** argument callable, passing in the key and value
+used in the collection population.  Below we map our ``UserKeyword`` association object
+to the ``User`` object using a dictionary interface, where the ``special_key`` attribute
+of ``UserKeyword`` is used as the key in this dictionary, and the ``UserKeyword`` as 
+the value.  The association proxy with ``creator`` can give us a dictionary of ``special_key``
+linked to ``Keyword`` objects::
+
+    from sqlalchemy import Column, Integer, String, ForeignKey
+    from sqlalchemy.orm import relationship, backref
+    from sqlalchemy.ext.associationproxy import association_proxy
+    from sqlalchemy.ext.declarative import declarative_base
+    from sqlalchemy.orm.collections import attribute_mapped_collection
 
-    mapper(User, users_table)
-    mapper(Keyword, keywords_table)
-    mapper(UserKeyword, userkeywords_table, properties={
-        'user': relationship(User, backref='user_keywords'),
-        'keyword': relationship(Keyword),
-    })
-
-    user = User('log')
-    kw1  = Keyword('new_from_blammo')
-
-    # Creating a UserKeyword association object will add a Keyword.
-    # the "user" reference assignment in the UserKeyword() constructor
-    # populates "user_keywords" via backref.
-    UserKeyword(user, kw1)
+    Base = declarative_base()
 
-    # Accessing Keywords requires traversing UserKeywords
-    print user.user_keywords[0]
-    # <__main__.UserKeyword object at 0xb79bbbec>
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(64))
+        keywords = association_proxy('user_keywords', 'keyword', 
+                        creator=lambda k, v:UserKeyword(special_key=k, keyword=v)
+                    )
 
-    print user.user_keywords[0].keyword
-    # Keyword('new_from_blammo')
+        def __init__(self, name):
+            self.name = name
 
-    # Lots of work.
+    class Keyword(Base):
+        __tablename__ = 'keyword'
+        id = Column(Integer, primary_key=True)
+        keyword = Column('keyword', String(64))
 
-    # It's much easier to go through the association proxy!
-    for kw in (Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')):
-        user.keywords.append(kw)
+        def __init__(self, keyword):
+            self.keyword = keyword
 
-    print user.keywords
-    # [Keyword('new_from_blammo'), Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')]
+        def __repr__(self):
+            return 'Keyword(%s)' % repr(self.keyword)
 
+    class UserKeyword(Base):
+        __tablename__ = 'user_keyword'
+        user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
+        keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
+        special_key = Column(String)
+        user = relationship(User, backref=backref(
+                                "user_keywords", 
+                                collection_class=attribute_mapped_collection("special_key"),
+                                cascade="all, delete-orphan"
+                                )
+                            )
+        keyword = relationship(Keyword)
+
+        def __init__(self, keyword=None, user=None, special_key=None):
+            self.user = user
+            self.keyword = keyword
+            self.special_key = special_key
 
-Building Complex Views
-----------------------
+The ``.keywords`` collection is now a dictionary of string keys to ``Keyword`` 
+objects::
 
-.. sourcecode:: python
+    >>> user = User('log')
+    >>> kw1  = Keyword('new_from_blammo')
 
-    stocks_table = Table("stocks", meta,
-       Column('symbol', String(10), primary_key=True),
-       Column('last_price', Numeric)
-    )
+    >>> user.keywords['sk1'] = Keyword('kw1')
+    >>> user.keywords['sk2'] = Keyword('kw2')
 
-    brokers_table = Table("brokers", meta,
-       Column('id', Integer,primary_key=True),
-       Column('name', String(100), nullable=False)
-    )
+    >>> print user.keywords
+    {'sk1': Keyword('kw1'), 'sk2': Keyword('kw2')}
 
-    holdings_table = Table("holdings", meta,
-      Column('broker_id', Integer, ForeignKey('brokers.id'), primary_key=True),
-      Column('symbol', String(10), ForeignKey('stocks.symbol'), primary_key=True),
-      Column('shares', Integer)
-    )
+Building Complex Views
+----------------------
 
-Above are three tables, modeling stocks, their brokers and the number of
-shares of a stock held by each broker.  This situation is quite different
-from the association example above.  ``shares`` is a *property of the
-relationship*, an important one that we need to use all the time.
+Given our previous examples of proxying from relationship to scalar
+attribute, proxying across an association object, and proxying dictionaries,
+we can combine all three techniques together to give ``User`` 
+a ``keywords`` dictionary that deals strictly with the string value 
+of ``special_key`` mapped to the string ``keyword``.  Both the ``UserKeyword``
+and ``Keyword`` classes are entirely concealed.  This is achieved by building
+an association proxy on ``User`` that refers to an association proxy
+present on ``UserKeyword``::
 
-For this example, it would be very convenient if ``Broker`` objects had a
-dictionary collection that mapped ``Stock`` instances to the shares held for
-each.  That's easy::
+    from sqlalchemy import Column, Integer, String, ForeignKey
+    from sqlalchemy.orm import relationship, backref
 
     from sqlalchemy.ext.associationproxy import association_proxy
+    from sqlalchemy.ext.declarative import declarative_base
     from sqlalchemy.orm.collections import attribute_mapped_collection
 
-    def _create_holding(stock, shares):
-        """A creator function, constructs Holdings from Stock and share quantity."""
-        return Holding(stock=stock, shares=shares)
+    Base = declarative_base()
+
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(64))
+        keywords = association_proxy(
+                    'user_keywords', 
+                    'keyword', 
+                    creator=lambda k, v:UserKeyword(special_key=k, keyword=v)
+                    )
 
-    class Broker(object):
         def __init__(self, name):
             self.name = name
 
-        holdings = association_proxy('by_stock', 'shares', creator=_create_holding)
-
-    class Stock(object):
-        def __init__(self, symbol):
-            self.symbol = symbol
-            self.last_price = 0
-
-    class Holding(object):
-        def __init__(self, broker=None, stock=None, shares=0):
-            self.broker = broker
-            self.stock = stock
-            self.shares = shares
-
-    mapper(Stock, stocks_table)
-    mapper(Broker, brokers_table, properties={
-        'by_stock': relationship(Holding,
-            collection_class=attribute_mapped_collection('stock'))
-    })
-    mapper(Holding, holdings_table, properties={
-        'stock': relationship(Stock),
-        'broker': relationship(Broker)
-    })
-
-Above, we've set up the ``by_stock`` relationship collection to act as a
-dictionary, using the ``.stock`` property of each Holding as a key.
-
-Populating and accessing that dictionary manually is slightly inconvenient
-because of the complexity of the Holdings association object::
-
-    stock = Stock('ZZK')
-    broker = Broker('paj')
-
-    broker.by_stock[stock] = Holding(broker, stock, 10)
-    print broker.by_stock[stock].shares
-    # 10
+    class Keyword(Base):
+        __tablename__ = 'keyword'
+        id = Column(Integer, primary_key=True)
+        keyword = Column('keyword', String(64))
 
-The ``holdings`` proxy we've added to the ``Broker`` class hides the details
-of the ``Holding`` while also giving access to ``.shares``::
-
-    for stock in (Stock('JEK'), Stock('STPZ')):
-        broker.holdings[stock] = 123
-
-    for stock, shares in broker.holdings.items():
-        print stock, shares
-
-    session.add(broker)
-    session.commit()
-
-    # lets take a peek at that holdings_table after committing changes to the db
-    print list(holdings_table.select().execute())
-    # [(1, 'ZZK', 10), (1, 'JEK', 123), (1, 'STEPZ', 123)]
-
-Further examples can be found in the ``examples/`` directory in the
-SQLAlchemy distribution.
+        def __init__(self, keyword):
+            self.keyword = keyword
 
-API
----
+    class UserKeyword(Base):
+        __tablename__ = 'user_keyword'
+        user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
+        keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
+        special_key = Column(String)
+        user = relationship(User, backref=backref(
+                    "user_keywords", 
+                    collection_class=attribute_mapped_collection("special_key"),
+                    cascade="all, delete-orphan"
+                    )
+                )
+        kw = relationship(Keyword)
+        keyword = association_proxy('kw', 'keyword')
+
+``.keywords`` is now a dictionary of string to string, where ``UserKeyword`` and ``Keyword`` objects are created and removed
+for us transparently using the association proxy, persisted and loaded using the ORM::
+
+    >>> user = User('log')
+    >>> user.keywords = {
+    ...     'sk1':'kw1',
+    ...     'sk2':'kw2'
+    ... }
+    >>> print user.keywords
+    {'sk1': 'kw1', 'sk2': 'kw2'}
+
+    >>> user.keywords['sk3'] = 'kw3'
+    >>> del user.keywords['sk2']
+    >>> print user.keywords
+    {'sk1': 'kw1', 'sk3': 'kw3'}
+
+    >>> print user.user_keywords['sk3'].kw
+    <__main__.Keyword object at 0x12ceb90>
+
+Querying with Association Proxies
+---------------------------------
+
+The :class:`.AssociationProxy` features simple SQL construction capabilities
+which relate down to the underlying :func:`.relationship` in use as well
+as the target attribute.  For example, the :meth:`.PropComparator.any`
+and :meth:`.PropComparator.has` operations are available for an association
+proxy that is specifically proxying two relationships, and will produce
+a "nested" EXISTS clause, such as in our basic association object example::
+
+    >>> print session.query(User).filter(User.keywords.any(keyword='jek'))
+    SELECT user.id AS user_id, user.name AS user_name 
+    FROM user 
+    WHERE EXISTS (SELECT 1 
+    FROM user_keyword 
+    WHERE user.id = user_keyword.user_id AND (EXISTS (SELECT 1 
+    FROM keyword 
+    WHERE keyword.id = user_keyword.keyword_id AND keyword.keyword = :keyword_1)))
+
+For a proxy to a scalar attribute, ``__eq__()`` is supported::
+
+    >>> print session.query(UserKeyword).filter(UserKeyword.keyword == 'jek')
+    SELECT user_keyword.*
+    FROM user_keyword 
+    WHERE EXISTS (SELECT 1 
+        FROM keyword 
+        WHERE keyword.id = user_keyword.keyword_id AND keyword.keyword = :keyword_1)
+
+and :meth:`.PropComparator.contains` for proxy to scalar collection::
+
+    >>> print session.query(User).filter(User.keywords.contains('jek'))
+    SELECT user.*
+    FROM user 
+    WHERE EXISTS (SELECT 1 
+    FROM userkeywords, keyword 
+    WHERE user.id = userkeywords.user_id 
+        AND keyword.id = userkeywords.keyword_id 
+        AND keyword.keyword = :keyword_1)
+
+:class:`.AssociationProxy` can be used with :meth:`.Query.join` somewhat manually
+using the :attr:`~.AssociationProxy.attr` attribute in a star-args context (new in 0.7.3)::
+
+    q = session.query(User).join(*User.keywords)
+
+:attr:`~.AssociationProxy.attr` is composed of :attr:`.AssociationProxy.local_attr` and :attr:`.AssociationProxy.remote_attr`,
+which are just synonyms for the actual proxied attributes, and can also
+be used for querying (also new in 0.7.3)::
+
+    uka = aliased(UserKeyword)
+    ka = aliased(Keyword)
+    q = session.query(User).\
+            join(uka, User.keywords.local_attr).\
+            join(ka, User.keywords.remote_attr)
+
+API Documentation
+-----------------
 
 .. autofunction:: association_proxy
 
index 3fbafd23c59c2b38cb97ba7c0b9ec28d126f93bc..6dd32fac5e6dc7461be01f5759b6d890c330132c 100644 (file)
@@ -188,6 +188,8 @@ The second example above as declarative::
         __tablename__ = 'child'
         id = Column(Integer, primary_key=True)
 
+.. _relationships_many_to_many:
+
 Many To Many
 ~~~~~~~~~~~~~
 
@@ -250,6 +252,19 @@ plain schematic form::
         __tablename__ = 'right'
         id = Column(Integer, primary_key=True)
 
+The ``secondary`` argument of :func:`.relationship` also accepts a callable
+that returns the ultimate argument, which is evaluated only when mappers are 
+first used.   Using this, we can define the ``association_table`` at a later
+point, as long as it's available to the callable after all module initialization
+is complete::
+
+    class Parent(Base):
+        __tablename__ = 'left'
+        id = Column(Integer, primary_key=True)
+        children = relationship("Child", 
+                        secondary=lambda: association_table, 
+                        backref="parents")
+
 .. _association_pattern:
 
 Association Object
index 31bfa90ff72fef768054eab8ea0bab8e6d6ac894..55237686c0143bc8f6762c66b87d2f670a365081 100644 (file)
@@ -18,7 +18,7 @@ import weakref
 from sqlalchemy import exceptions
 from sqlalchemy import orm
 from sqlalchemy import util
-from sqlalchemy.orm import collections
+from sqlalchemy.orm import collections, ColumnProperty
 from sqlalchemy.sql import not_
 
 
@@ -37,7 +37,7 @@ def association_proxy(target_collection, attr, **kw):
     Unlike the list comprehension, the collection returned by the property is
     always in sync with *target_collection*, and mutations made to either
     collection will be reflected in both.
-    
+
     The association proxy also works with scalar attributes, which in
     turn reference scalar attributes or collections.
 
@@ -46,8 +46,10 @@ def association_proxy(target_collection, attr, **kw):
     the target (list, dict or set), or, in the case of a one to one relationship,
     a simple scalar value.
 
-    :param target_collection: Name of the relationship attribute we'll proxy to,
-      usually created with :func:`~sqlalchemy.orm.relationship`.
+    :param target_collection: Name of the attribute we'll proxy to.  
+      This attribute is typically mapped by
+      :func:`~sqlalchemy.orm.relationship` to link to a target collection, but
+      can also be a many-to-one or non-scalar relationship.
 
     :param attr: Attribute on the associated instance or instances we'll proxy for.
 
@@ -92,20 +94,17 @@ class AssociationProxy(object):
     def __init__(self, target_collection, attr, creator=None,
                  getset_factory=None, proxy_factory=None, 
                  proxy_bulk_set=None):
-        """Arguments are:
+        """Construct a new :class:`.AssociationProxy`.
 
-        target_collection
-          Name of the collection we'll proxy to, usually created with
-          'relationship()' in a mapper setup.
+        :param target_collection: Name of the collection we'll proxy to, 
+          usually created with 'relationship()' in a mapper setup.
 
-        attr
-          Attribute on the collected instances we'll proxy for.  For example,
+        :param attr: Attribute on the collected instances we'll proxy for.  For example,
           given a target collection of [obj1, obj2], a list created by this
           proxy property would look like [getattr(obj1, attr), getattr(obj2,
           attr)]
 
-        creator
-          Optional. When new items are added to this proxied collection, new
+        :param creator: Optional. When new items are added to this proxied collection, new
           instances of the class collected by the target collection will be
           created.  For list and set collections, the target class constructor
           will be called with the 'value' for the new instance.  For dict
@@ -114,8 +113,7 @@ class AssociationProxy(object):
           If you want to construct instances differently, supply a 'creator'
           function that takes arguments as above and returns instances.
 
-        getset_factory
-          Optional.  Proxied attribute access is automatically handled by
+        :param getset_factory: Optional.  Proxied attribute access is automatically handled by
           routines that get and set values based on the `attr` argument for
           this proxy.
 
@@ -124,16 +122,14 @@ class AssociationProxy(object):
           `setter` functions.  The factory is called with two arguments, the
           abstract type of the underlying collection and this proxy instance.
 
-        proxy_factory
-          Optional.  The type of collection to emulate is determined by
+        :param proxy_factory: Optional.  The type of collection to emulate is determined by
           sniffing the target collection.  If your collection type can't be
           determined by duck typing or you'd like to use a different
           collection implementation, you may supply a factory function to
           produce those collections.  Only applicable to non-scalar relationships.
 
-        proxy_bulk_set
-          Optional, use with proxy_factory.  See the _set() method for
-          details.
+        :param proxy_bulk_set: Optional, use with proxy_factory.  See 
+          the _set() method for details.
 
         """
         self.target_collection = target_collection
@@ -148,6 +144,58 @@ class AssociationProxy(object):
             type(self).__name__, target_collection, id(self))
         self.collection_class = None
 
+    @property
+    def remote_attr(self):
+        """The 'remote' :class:`.MapperProperty` referenced by this
+        :class:`.AssociationProxy`.
+        
+        New in 0.7.3.
+        
+        See also:
+        
+        :attr:`.AssociationProxy.attr`
+
+        :attr:`.AssociationProxy.local_attr`
+
+        """
+        return getattr(self.target_class, self.value_attr)
+
+    @property
+    def local_attr(self):
+        """The 'local' :class:`.MapperProperty` referenced by this
+        :class:`.AssociationProxy`.
+
+        New in 0.7.3.
+        
+        See also:
+        
+        :attr:`.AssociationProxy.attr`
+
+        :attr:`.AssociationProxy.remote_attr`
+
+        """
+        return getattr(self.owning_class, self.target_collection)
+
+    @property
+    def attr(self):
+        """Return a tuple of ``(local_attr, remote_attr)``.
+        
+        This attribute is convenient when specifying a join 
+        using :meth:`.Query.join` across two relationships::
+        
+            sess.query(Parent).join(*Parent.proxied.attr)
+
+        New in 0.7.3.
+        
+        See also:
+        
+        :attr:`.AssociationProxy.local_attr`
+
+        :attr:`.AssociationProxy.remote_attr`
+        
+        """
+        return (self.local_attr, self.remote_attr)
+
     def _get_property(self):
         return (orm.class_mapper(self.owning_class).
                 get_property(self.target_collection))
@@ -159,6 +207,9 @@ class AssociationProxy(object):
 
     @util.memoized_property
     def scalar(self):
+        """Return true if this :class:`.AssociationProxy` proxies a scalar
+        relationship on the local side."""
+
         scalar = not self._get_property().uselist
         if scalar:
             self._initialize_scalar_accessors()
@@ -284,6 +335,8 @@ class AssociationProxy(object):
         return self._get_property().comparator
 
     def any(self, criterion=None, **kwargs):
+        """Produce a proxied 'any' expression using EXISTS."""
+
         if self._value_is_scalar:
             value_expr = getattr(self.target_class, self.value_attr).has(criterion, **kwargs)
         else:
@@ -302,11 +355,15 @@ class AssociationProxy(object):
                 )
 
     def has(self, criterion=None, **kwargs):
+        """Produce a proxied 'has' expression using EXISTS."""
+
         return self._comparator.has(
                     getattr(self.target_class, self.value_attr).has(criterion, **kwargs)
                 )
 
     def contains(self, obj):
+        """Produce a proxied 'contains' expression using EXISTS."""
+
         if self.scalar and not self._value_is_scalar:
             return self._comparator.has(
                 getattr(self.target_class, self.value_attr).contains(obj)
index 717176ad76b1f3d014109034cd3aabb993de9748..36dd1e6fb14f88d940c9214f5f885e29b2322c04 100644 (file)
@@ -186,23 +186,64 @@ def relationship(argument, secondary=None, **kwargs):
     This corresponds to a parent-child or associative table relationship.  The
     constructed class is an instance of :class:`.RelationshipProperty`.
 
-    A typical :func:`relationship`::
+    A typical :func:`.relationship`, used in a classical mapping::
 
        mapper(Parent, properties={
-         'children': relationship(Children)
+         'children': relationship(Child)
        })
 
+    Some arguments accepted by :func:`.relationship` optionally accept a 
+    callable function, which when called produces the desired value.
+    The callable is invoked by the parent :class:`.Mapper` at "mapper initialization"
+    time, which happens only when mappers are first used, and is assumed
+    to be after all mappings have been constructed.  This can be used
+    to resolve order-of-declaration and other dependency issues, such as 
+    if ``Child`` is declared below ``Parent`` in the same file::
+    
+        mapper(Parent, properties={
+            "children":relationship(lambda: Child, 
+                                order_by=lambda: Child.id)
+        })
+    
+    When using the :ref:`declarative_toplevel` extension, the Declarative
+    initializer allows string arguments to be passed to :func:`.relationship`.
+    These string arguments are converted into callables that evaluate 
+    the string as Python code, using the Declarative
+    class-registry as a namespace.  This allows the lookup of related
+    classes to be automatic via their string name, and removes the need to import
+    related classes at all into the local module space::
+    
+        from sqlalchemy.ext.declarative import declarative_base
+
+        Base = declarative_base()
+        
+        class Parent(Base):
+            __tablename__ = 'parent'
+            id = Column(Integer, primary_key=True)
+            children = relationship("Child", order_by="Child.id")
+    
+    A full array of examples and reference documentation regarding
+    :func:`.relationship` is at :ref:`relationship_config_toplevel`.
+    
     :param argument:
-      a class or :class:`.Mapper` instance, representing the target of
-      the relationship.
+      a mapped class, or actual :class:`.Mapper` instance, representing the target of
+      the relationship.  
+      
+      ``argument`` may also be passed as a callable function
+      which is evaluated at mapper initialization time, and may be passed as a 
+      Python-evaluable string when using Declarative.
 
     :param secondary:
       for a many-to-many relationship, specifies the intermediary
-      table. The *secondary* keyword argument should generally only
+      table, and is an instance of :class:`.Table`.  The ``secondary`` keyword 
+      argument should generally only
       be used for a table that is not otherwise expressed in any class
-      mapping. In particular, using the Association Object Pattern is
-      generally mutually exclusive with the use of the *secondary*
-      keyword argument.
+      mapping, unless this relationship is declared as view only, otherwise
+      conflicting persistence operations can occur.   
+      
+      ``secondary`` may
+      also be passed as a callable function which is evaluated at 
+      mapper initialization time.
 
     :param active_history=False:
       When ``True``, indicates that the "previous" value for a
@@ -212,7 +253,7 @@ def relationship(argument, secondary=None, **kwargs):
       value in order to perform a flush. This flag is available
       for applications that make use of
       :func:`.attributes.get_history` which also need to know
-      the "previous" value of the attribute. (New in 0.6.6)
+      the "previous" value of the attribute.
 
     :param backref:
       indicates the string name of a property to be placed on the related
@@ -333,6 +374,10 @@ def relationship(argument, secondary=None, **kwargs):
       rare and exotic composite foreign key setups where some columns
       should artificially not be considered as foreign.
 
+      ``foreign_keys`` may also be passed as a callable function
+      which is evaluated at mapper initialization time, and may be passed as a 
+      Python-evaluable string when using Declarative.
+      
     :param innerjoin=False:
       when ``True``, joined eager loads will use an inner join to join
       against related tables instead of an outer join.  The purpose
@@ -423,8 +468,15 @@ def relationship(argument, secondary=None, **kwargs):
 
     :param order_by:
       indicates the ordering that should be applied when loading these
-      items.
-
+      items.  ``order_by`` is expected to refer to one of the :class:`.Column`
+      objects to which the target class is mapped, or 
+      the attribute itself bound to the target class which refers
+      to the column.
+
+      ``order_by`` may also be passed as a callable function
+      which is evaluated at mapper initialization time, and may be passed as a 
+      Python-evaluable string when using Declarative.
+      
     :param passive_deletes=False:
        Indicates loading behavior during delete operations.
 
@@ -496,17 +548,25 @@ def relationship(argument, secondary=None, **kwargs):
       use ``post_update`` to "break" the cycle.
 
     :param primaryjoin:
-      a ColumnElement (i.e. WHERE criterion) that will be used as the primary
+      a SQL expression that will be used as the primary
       join of this child object against the parent object, or in a
       many-to-many relationship the join of the primary object to the
       association table. By default, this value is computed based on the
       foreign key relationships of the parent and child tables (or association
       table).
 
+      ``primaryjoin`` may also be passed as a callable function
+      which is evaluated at mapper initialization time, and may be passed as a 
+      Python-evaluable string when using Declarative.
+
     :param remote_side:
       used for self-referential relationships, indicates the column or
       list of columns that form the "remote side" of the relationship.
 
+      ``remote_side`` may also be passed as a callable function
+      which is evaluated at mapper initialization time, and may be passed as a 
+      Python-evaluable string when using Declarative.
+
     :param query_class:
       a :class:`.Query` subclass that will be used as the base of the
       "appender query" returned by a "dynamic" relationship, that
@@ -515,18 +575,22 @@ def relationship(argument, secondary=None, **kwargs):
       function.
       
     :param secondaryjoin:
-      a ColumnElement (i.e. WHERE criterion) that will be used as the join of
+      a SQL expression that will be used as the join of
       an association table to the child object. By default, this value is
       computed based on the foreign key relationships of the association and
       child tables.
 
+      ``secondaryjoin`` may also be passed as a callable function
+      which is evaluated at mapper initialization time, and may be passed as a 
+      Python-evaluable string when using Declarative.
+
     :param single_parent=(True|False):
       when True, installs a validator which will prevent objects
       from being associated with more than one parent at a time.
       This is used for many-to-one or many-to-many relationships that
       should be treated either as one-to-one or one-to-many.  Its
       usage is optional unless delete-orphan cascade is also 
-      set on this relationship(), in which case its required (new in 0.5.2).
+      set on this relationship(), in which case its required.
 
     :param uselist=(True|False):
       a boolean that indicates if this property should be loaded as a
index 718a5c0981a902223a29e272bb7ec4eaae7b6989..ddd0bd8f12766ce7f1ee1abd4943063a39d12d02 100644 (file)
@@ -1006,7 +1006,8 @@ class PickleKeyFunc(object):
     def __call__(self, obj):
         return getattr(obj, self.name)
 
-class ComparatorTest(fixtures.MappedTest):
+class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
+    __dialect__ = 'default'
 
     run_inserts = 'once'
     run_deletes = None
@@ -1070,7 +1071,8 @@ class ComparatorTest(fixtures.MappedTest):
 
     @classmethod
     def setup_mappers(cls):
-        users, Keyword, UserKeyword, singular, userkeywords, User, keywords, Singular = (cls.tables.users,
+        users, Keyword, UserKeyword, singular, \
+            userkeywords, User, keywords, Singular = (cls.tables.users,
                                 cls.classes.Keyword,
                                 cls.classes.UserKeyword,
                                 cls.tables.singular,
@@ -1280,3 +1282,28 @@ class ComparatorTest(fixtures.MappedTest):
 
         assert_raises(exceptions.InvalidRequestError, lambda : \
                       User.keywords != self.kw)
+
+    def test_join_separate_attr(self):
+        User = self.classes.User
+        self.assert_compile(
+            self.session.query(User).join(
+                        User.keywords.local_attr, 
+                        User.keywords.remote_attr),
+            "SELECT users.id AS users_id, users.name AS users_name, "
+            "users.singular_id AS users_singular_id "
+            "FROM users JOIN userkeywords ON users.id = "
+            "userkeywords.user_id JOIN keywords ON keywords.id = "
+            "userkeywords.keyword_id"
+        )
+
+    def test_join_single_attr(self):
+        User = self.classes.User
+        self.assert_compile(
+            self.session.query(User).join(
+                        *User.keywords.attr),
+            "SELECT users.id AS users_id, users.name AS users_name, "
+            "users.singular_id AS users_singular_id "
+            "FROM users JOIN userkeywords ON users.id = "
+            "userkeywords.user_id JOIN keywords ON keywords.id = "
+            "userkeywords.keyword_id"
+        )
\ No newline at end of file