From: Mike Bayer Date: Sat, 6 Aug 2011 16:43:09 +0000 (-0400) Subject: - second pass through association proxy docs, some more links on any()/has(), MapperP... X-Git-Tag: rel_0_7_3~98 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=669d0c47f8806173261a365b66cae6edf73287cb;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - second pass through association proxy docs, some more links on any()/has(), MapperProperty --- diff --git a/CHANGES b/CHANGES index 77f041c86e..9960e72dea 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,12 @@ CHANGES the database regardless of whether C extensions are in use or not. +- ext + - Added local_attr, remote_attr, attr accessors + to AssociationProxy, providing quick access + to the proxied attributes at the class + level. [ticket:2236] + 0.7.2 ===== - orm diff --git a/doc/build/orm/collections.rst b/doc/build/orm/collections.rst index 4d768e8c4a..fbe734e7c8 100644 --- a/doc/build/orm/collections.rst +++ b/doc/build/orm/collections.rst @@ -284,6 +284,8 @@ interface marked for SQLAlchemy's use. Append and remove methods will be called with a mapped entity as the single argument, and iterator methods are called with no arguments and must return an iterator. +.. _dictionary_collections: + Dictionary-Based Collections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -365,6 +367,9 @@ must decorate appender and remover methods, however- there are no compatible methods in the basic dictionary interface for SQLAlchemy to use by default. Iteration will go through ``itervalues()`` unless otherwise decorated. +See also :ref:`proxying_dictionaries` for details on how to use association +proxies to create flexible dictionary views. + Instrumentation and Custom Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/build/orm/extensions/associationproxy.rst b/doc/build/orm/extensions/associationproxy.rst index c78850cb7f..3e00080097 100644 --- a/doc/build/orm/extensions/associationproxy.rst +++ b/doc/build/orm/extensions/associationproxy.rst @@ -52,23 +52,22 @@ Each ``User`` can have any number of ``Keyword`` objects, and vice-versa primary_key=True) ) -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:: +Reading and manipulating the collection of "keyword" strings associated +with ``User`` requires traversal from each collection element to the ``.keyword`` +attribute, which can be awkward:: >>> user = User('jek') >>> user.kw.append(Keyword('cheese inspector')) - >>> print user.kw + >>> print(user.kw) [<__main__.Keyword object at 0x12bf830>] - >>> print user.kw[0].keyword + >>> print(user.kw[0].keyword) cheese inspector - >>> print [keyword.keyword for keyword in user.kw] + >>> 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. Below we illustrate -how to bridge the gap between the ``kw`` collection and the ``keyword`` -attribute of each ``Keyword``:: +The ``association_proxy`` is applied to the ``User`` class to produce +a "view" of the ``kw`` relationship, which only exposes the string +value of ``.keyword`` associated with each ``Keyword`` object:: from sqlalchemy.ext.associationproxy import association_proxy @@ -85,7 +84,8 @@ attribute of each ``Keyword``:: keywords = association_proxy('kw', 'keyword') We can now reference the ``.keywords`` collection as a listing of strings, -which is both readable and writeable:: +which is both readable and writable. New ``Keyword`` objects are created +for us transparently:: >>> user = User('jek') >>> user.keywords.append('cheese inspector') @@ -95,64 +95,74 @@ which is both readable and writeable:: >>> user.kw [<__main__.Keyword object at 0x12cdd30>, <__main__.Keyword object at 0x12cde30>] -The association proxy is nothing more than a Python `descriptor `_, -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 :class:`.AssociationProxy` object produced by the :func:`.association_proxy` function +is an instance of a `Python descriptor `_. +It is always declared with the user-defined class being mapped, regardless of +whether Declarative or classical mappings via the :func:`.mapper` function 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 -the proxy also affects the underlying collection. +The proxy functions by operating upon the underlying mapped attribute +or collection in response to operations, and changes made via the proxy are immediately +apparent in the mapped attribute, as well as vice versa. The underlying +attribute remains fully accessible. - - The association proxy property is backed by a mapper-defined relationship, - either a collection or scalar. +When first accessed, the association proxy performs introspection +operations on the target collection so that its behavior corresponds correctly. +Details such as if the locally proxied attribute is a collection (as is typical) +or a scalar reference, as well as if the collection acts like a set, list, +or dictionary is taken into account, so that the proxy should act just like +the underlying collection or attribute does. - - You can access and modify both the proxy and the backing - relationship. Changes in one are immediate in the other. +Creation of New Values +----------------------- - - The proxy acts like the type of the underlying collection. A list gets a - list-like proxy, a dict a dict-like proxy, and so on. +When a list append() event (or set add(), dictionary __setitem__(), or scalar +assignment event) is intercepted by the association proxy, it instantiates a +new instance of the "intermediary" object using its constructor, passing as a +single argument the given value. In our example above, an operation like:: - - Multiple proxies for the same relationship are fine. + user.keywords.append('cheese inspector') - - Proxies are lazy, and won't trigger a load of the backing relationship until - they are accessed. +Is translated by the association proxy into the operation:: - - The relationship is inspected to determine the type of the related objects. + user.kw.append(Keyword('cheese inspector')) - - To construct new instances, the type is called with the value being - assigned, or key and value for dicts. +The example works here because we have designed the constructor for ``Keyword`` +to accept a single positional argument, ``keyword``. For those cases where a +single-argument constructor isn't feasible, the association proxy's creational +behavior can be customized using the ``creator`` argument, which references a +callable (i.e. Python function) that will produce a new object instance given the +singular argument. Below we illustrate this using a lambda as is typical:: - - A "creator" function can be used to create instances instead. + class User(Base): + # ... -Above, the ``Keyword.__init__`` takes a single argument ``keyword``, which -maps conveniently to the value being set through the proxy. A ``creator`` -function can be used if more flexibility is required. + # use Keyword(keyword=kw) on append() events + keywords = association_proxy('kw', 'keyword', + creator=lambda kw: Keyword(keyword=kw)) -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 -collection is sufficient. +The ``creator`` function accepts a single argument in the case of a list- +or set- based collection, or a scalar attribute. In the case of a dictionary-based +collection, it accepts two arguments, "key" and "value". An example +of this is below in :ref:`proxying_dictionaries`. -Simplifying Association Proxies +Simplifying Association Objects ------------------------------- -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`. +The "association object" pattern is an extended form of a many-to-many +relationship, and is described at :ref:`association_pattern`. Association +proxies are useful for keeping "association objects" out the way during +regular use. 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 +This class adds an additional column ``special_key``, a value which +we occasionally want to access, but not in the usual case. 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 +collection of ``User`` to the ``.keyword`` attribute present on each ``UserKeyword``:: from sqlalchemy import Column, Integer, String, ForeignKey @@ -167,99 +177,103 @@ collection to the ``Keyword`` object referenced by each __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String(64)) + + # association proxy of "user_keywords" collection + # to "keyword" attribute keywords = association_proxy('user_keywords', 'keyword') def __init__(self, name): self.name = name - 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(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) + + # bidirectional attribute/collection of "user"/"user_keywords" + user = relationship(User, + backref=backref("user_keywords", + cascade="all, delete-orphan") + ) + + # reference to the "Keyword" object + 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> + class Keyword(Base): + __tablename__ = 'keyword' + id = Column(Integer, primary_key=True) + keyword = Column('keyword', String(64)) - >>> # Accessing Keywords requires traversing UserKeywords - ... print user.user_keywords[0] - <__main__.UserKeyword object at 0x12d9a50> + def __init__(self, keyword): + self.keyword = keyword - >>> print user.user_keywords[0].keyword - Keyword('new_from_blammo') + def __repr__(self): + return 'Keyword(%s)' % repr(self.keyword) -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:: +With the above configuration, we can operate upon the ``.keywords`` +collection of each ``User`` object, and the usage of ``UserKeyword`` +is concealed:: - >>> for kw in (Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')): + >>> user = User('log') + >>> for kw in (Keyword('new_from_blammo'), Keyword('its_big')): ... user.keywords.append(kw) ... - >>> print user.keywords - [Keyword('new_from_blammo'), Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')] + >>> print(user.keywords) + [Keyword('new_from_blammo'), Keyword('its_big')] -In each call to ``keywords.append()``, the association proxy performs the -operation as:: +Where above, each ``.keywords.append()`` operation is equivalent to:: - user.user_keywords.append(UserKeyword(kw)) + >>> user.user_keywords.append(UserKeyword(Keyword('its_heavy'))) -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. +The ``UserKeyword`` association object has two attributes here which are populated; +the ``.keyword`` attribute is populated directly as a result of passing +the ``Keyword`` object as the first argument. The ``.user`` argument is then +assigned as the ``UserKeyword`` object is appended to the ``User.user_keywords`` +collection, where the bidirectional relationship configured between ``User.user_keywords`` +and ``UserKeyword.user`` results in a population of the ``UserKeyword.user`` attribute. +The ``special_key`` argument above is left at its default value of ``None``. -Using the creator argument -^^^^^^^^^^^^^^^^^^^^^^^^^^ +For those cases where we do want ``special_key`` to have a value, we +create the ``UserKeyword`` object explicitly. Below we assign all three +attributes, where the assignment of ``.user`` has the effect of the ``UserKeyword`` +being appended to the ``User.user_keywords`` collection:: -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:: + >>> UserKeyword(Keyword('its_wood'), user, special_key='my special key') - class User(Base): - # ... +The association proxy returns to us a collection of ``Keyword`` objects represented +by all these operations:: - keywords = association_proxy('user_keywords', 'keyword', - creator=lambda k:UserKeyword(keyword=k, special_key='special')) + >>> user.keywords + [Keyword('new_from_blammo'), Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')] + +.. _proxying_dictionaries: -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:: +Proxying to Dictionary Based Collections +----------------------------------------- + +The association proxy can proxy to dictionary based collections as well. SQLAlchemy +mappings usually use the :func:`.attribute_mapped_collection` collection type to +create dictionary collections, as well as the extended techniques described in +:ref:`dictionary_collections`. + +The association proxy adjusts its behavior when it detects the usage of a +dictionary-based collection. When new values are added to the dictionary, the +association proxy instantiates the intermediary object by passing two +arguments to the creation function instead of one, the key and the value. As +always, this creation function defaults to the constructor of the intermediary +class, and can be customized using the ``creator`` argument. + +Below, we modify our ``UserKeyword`` example such that the ``User.user_keywords`` +collection will now be mapped using a dictionary, where the ``UserKeyword.special_key`` +argument will be used as the key for the dictionary. We then apply a ``creator`` +argument to the ``User.keywords`` proxy so that these values are assigned appropriately +when new elements are added to the dictionary:: from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, backref @@ -273,13 +287,34 @@ linked to ``Keyword`` objects:: __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String(64)) + + # proxy to 'user_keywords', instantiating UserKeyword + # assigning the new key to 'special_key', values to + # 'keyword'. keywords = association_proxy('user_keywords', 'keyword', - creator=lambda k, v:UserKeyword(special_key=k, keyword=v) + creator=lambda k, v: + UserKeyword(special_key=k, keyword=v) ) def __init__(self, name): self.name = name + 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) + + # bidirectional user/user_keywords relationships, mapping + # user_keywords with a dictionary against "special_key" as key. + user = relationship(User, backref=backref( + "user_keywords", + collection_class=attribute_mapped_collection("special_key"), + cascade="all, delete-orphan" + ) + ) + keyword = relationship("Keyword") + class Keyword(Base): __tablename__ = 'keyword' id = Column(Integer, primary_key=True) @@ -291,38 +326,19 @@ linked to ``Keyword`` objects:: 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 - -The ``.keywords`` collection is now a dictionary of string keys to ``Keyword`` -objects:: +We illustrate the ``.keywords`` collection as a dictionary, mapping the +``UserKeyword.string_key`` value to ``Keyword`` objects:: >>> user = User('log') - >>> kw1 = Keyword('new_from_blammo') >>> user.keywords['sk1'] = Keyword('kw1') >>> user.keywords['sk2'] = Keyword('kw2') - >>> print user.keywords + >>> print(user.keywords) {'sk1': Keyword('kw1'), 'sk2': Keyword('kw2')} -Building Complex Views ----------------------- +Composite Association Proxies +----------------------------- Given our previous examples of proxying from relationship to scalar attribute, proxying across an association object, and proxying dictionaries, @@ -346,15 +362,40 @@ present on ``UserKeyword``:: __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String(64)) + + # the same 'user_keywords'->'keyword' proxy as in + # the basic dictionary example keywords = association_proxy( 'user_keywords', 'keyword', - creator=lambda k, v:UserKeyword(special_key=k, keyword=v) + creator=lambda k, v: + UserKeyword(special_key=k, keyword=v) ) def __init__(self, name): self.name = name + 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" + ) + ) + + # the relationship to Keyword is now called + # 'kw' + kw = relationship("Keyword") + + # 'keyword' is changed to be a proxy to the + # 'keyword' attribute of 'Keyword' + keyword = association_proxy('kw', 'keyword') + class Keyword(Base): __tablename__ = 'keyword' id = Column(Integer, primary_key=True) @@ -363,50 +404,50 @@ present on ``UserKeyword``:: def __init__(self, keyword): self.keyword = 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" - ) - ) - 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.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. In the example below, we illustrate +usage of the assignment operator, also appropriately handled by the +association proxy, to apply a dictionary value to the collection at once:: >>> user = User('log') >>> user.keywords = { ... 'sk1':'kw1', ... 'sk2':'kw2' ... } - >>> print user.keywords + >>> print(user.keywords) {'sk1': 'kw1', 'sk2': 'kw2'} >>> user.keywords['sk3'] = 'kw3' >>> del user.keywords['sk2'] - >>> print user.keywords + >>> print(user.keywords) {'sk1': 'kw1', 'sk3': 'kw3'} - >>> print user.user_keywords['sk3'].kw + >>> # illustrate un-proxied usage + ... print(user.user_keywords['sk3'].kw) <__main__.Keyword object at 0x12ceb90> +One caveat with our example above is that because ``Keyword`` objects are created +for each dictionary set operation, the example fails to maintain uniqueness for +the ``Keyword`` objects on their string name, which is a typical requirement for +a tagging scenario such as this one. For this use case the recipe +`UniqueObject `_, or +a comparable creational strategy, is +recommended, which will apply a "lookup first, then create" strategy to the constructor +of the ``Keyword`` class, so that an already existing ``Keyword`` is returned if the +given name is already present. + 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 +as the target attribute. For example, the :meth:`.RelationshipProperty.Comparator.any` +and :meth:`.RelationshipProperty.Comparator.has` operations are available, 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')) + >>> 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 @@ -417,16 +458,16 @@ a "nested" EXISTS clause, such as in our basic association object example:: For a proxy to a scalar attribute, ``__eq__()`` is supported:: - >>> print session.query(UserKeyword).filter(UserKeyword.keyword == 'jek') + >>> 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:: +and ``.contains()`` is available for a proxy to a scalar collection:: - >>> print session.query(User).filter(User.keywords.contains('jek')) + >>> print(session.query(User).filter(User.keywords.contains('jek'))) SELECT user.* FROM user WHERE EXISTS (SELECT 1 diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst index bb55fe60eb..29525cc635 100644 --- a/doc/build/orm/tutorial.rst +++ b/doc/build/orm/tutorial.rst @@ -1301,7 +1301,7 @@ There is an explicit EXISTS construct, which looks like this: The :class:`~sqlalchemy.orm.query.Query` features several operators which make usage of EXISTS automatically. Above, the statement can be expressed along the -``User.addresses`` relationship using ``any()``: +``User.addresses`` relationship using :meth:`~.RelationshipProperty.Comparator.any`: .. sourcecode:: python+sql @@ -1315,7 +1315,7 @@ usage of EXISTS automatically. Above, the statement can be expressed along the () {stop}jack -``any()`` takes criterion as well, to limit the rows matched: +:meth:`~.RelationshipProperty.Comparator.any` takes criterion as well, to limit the rows matched: .. sourcecode:: python+sql @@ -1330,7 +1330,7 @@ usage of EXISTS automatically. Above, the statement can be expressed along the ('%google%',) {stop}jack -``has()`` is the same operator as ``any()`` for many-to-one relationships (note the ``~`` operator here too, which means "NOT"): +:meth:`~.RelationshipProperty.Comparator.has` is the same operator as :meth:`~.RelationshipProperty.Comparator.any` for many-to-one relationships (note the ``~`` operator here too, which means "NOT"): .. sourcecode:: python+sql diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index 55237686c0..47b21ab5b0 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -26,21 +26,9 @@ def association_proxy(target_collection, attr, **kw): """Return a Python property implementing a view of a target attribute which references an attribute on members of the target. - - Implements a read/write view over an instance's *target_collection*, - extracting *attr* from each member of the collection. The property acts - somewhat like this list comprehension:: - - [getattr(member, *attr*) - for member in getattr(instance, *target_collection*)] - - 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. - + + The returned value is an instance of :class:`.AssociationProxy`. + Implements a Python property representing a relationship as a collection of simpler values, or a scalar value. The proxied property will mimic the collection type of the target (list, dict or set), or, in the case of a one to one relationship, @@ -95,9 +83,13 @@ class AssociationProxy(object): getset_factory=None, proxy_factory=None, proxy_bulk_set=None): """Construct a new :class:`.AssociationProxy`. + + The :func:`.association_proxy` function is provided as the usual + entrypoint here, though :class:`.AssociationProxy` can be instantiated + and/or subclassed directly. :param target_collection: Name of the collection we'll proxy to, - usually created with 'relationship()' in a mapper setup. + usually created with :func:`.relationship`. :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 @@ -202,12 +194,17 @@ class AssociationProxy(object): @util.memoized_property def target_class(self): - """The class the proxy is attached to.""" + """The intermediary class handled by this :class:`.AssociationProxy`. + + Intercepted append/set/assignment events will result + in the generation of new instances of this class. + + """ return self._get_property().mapper.class_ @util.memoized_property def scalar(self): - """Return true if this :class:`.AssociationProxy` proxies a scalar + """Return ``True`` if this :class:`.AssociationProxy` proxies a scalar relationship on the local side.""" scalar = not self._get_property().uselist @@ -335,7 +332,14 @@ class AssociationProxy(object): return self._get_property().comparator def any(self, criterion=None, **kwargs): - """Produce a proxied 'any' expression using EXISTS.""" + """Produce a proxied 'any' expression using EXISTS. + + This expression will be a composed product + using the :meth:`.RelationshipProperty.Comparator.any` + and/or :meth:`.RelationshipProperty.Comparator.has` + operators of the underlying proxied attributes. + + """ if self._value_is_scalar: value_expr = getattr(self.target_class, self.value_attr).has(criterion, **kwargs) @@ -355,14 +359,29 @@ class AssociationProxy(object): ) def has(self, criterion=None, **kwargs): - """Produce a proxied 'has' expression using EXISTS.""" + """Produce a proxied 'has' expression using EXISTS. + + This expression will be a composed product + using the :meth:`.RelationshipProperty.Comparator.any` + and/or :meth:`.RelationshipProperty.Comparator.has` + operators of the underlying proxied attributes. + + """ return self._comparator.has( - getattr(self.target_class, self.value_attr).has(criterion, **kwargs) + getattr(self.target_class, self.value_attr).\ + has(criterion, **kwargs) ) def contains(self, obj): - """Produce a proxied 'contains' expression using EXISTS.""" + """Produce a proxied 'contains' expression using EXISTS. + + This expression will be a composed product + using the :meth:`.RelationshipProperty.Comparator.any` + , :meth:`.RelationshipProperty.Comparator.has`, + and/or :meth:`.RelationshipProperty.Comparator.contains` + operators of the underlying proxied attributes. + """ if self.scalar and not self._value_is_scalar: return self._comparator.has( diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 4f8e6dbfda..24aebcc7bd 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -61,6 +61,13 @@ class MapperProperty(object): attribute, as well as that attribute as it appears on individual instances of the class, including attribute instrumentation, attribute access, loading behavior, and dependency calculations. + + The most common occurrences of :class:`.MapperProperty` are the + mapped :class:`.Column`, which is represented in a mapping as + an instance of :class:`.ColumnProperty`, + and a reference to another class produced by :func:`.relationship`, + represented in the mapping as an instance of :class:`.RelationshipProperty`. + """ cascade = () @@ -244,8 +251,7 @@ class PropComparator(operators.ColumnOperators): query.join(Company.employees.of_type(Engineer)).\\ filter(Engineer.name=='foo') - \class_ - a class or mapper indicating that criterion will be against + :param \class_: a class or mapper indicating that criterion will be against this specific subclass. @@ -257,13 +263,16 @@ class PropComparator(operators.ColumnOperators): """Return true if this collection contains any member that meets the given criterion. - criterion - an optional ClauseElement formulated against the member class' table - or attributes. + The usual implementation of ``any()`` is + :meth:`.RelationshipProperty.Comparator.any`. + + :param criterion: an optional ClauseElement formulated against the + member class' table or attributes. + + :param \**kwargs: key/value pairs corresponding to member class attribute + names which will be compared via equality to the corresponding + values. - \**kwargs - key/value pairs corresponding to member class attribute names which - will be compared via equality to the corresponding values. """ return self.operate(PropComparator.any_op, criterion, **kwargs) @@ -272,13 +281,16 @@ class PropComparator(operators.ColumnOperators): """Return true if this element references a member which meets the given criterion. - criterion - an optional ClauseElement formulated against the member class' table - or attributes. + The usual implementation of ``has()`` is + :meth:`.RelationshipProperty.Comparator.has`. + + :param criterion: an optional ClauseElement formulated against the + member class' table or attributes. + + :param \**kwargs: key/value pairs corresponding to member class attribute + names which will be compared via equality to the corresponding + values. - \**kwargs - key/value pairs corresponding to member class attribute names which - will be compared via equality to the corresponding values. """ return self.operate(PropComparator.has_op, criterion, **kwargs)