~ubuntu-branches/debian/sid/sqlalchemy/sid

« back to all changes in this revision

Viewing changes to doc/build/orm/extensions/associationproxy.rst

  • Committer: Bazaar Package Importer
  • Author(s): Piotr Ożarowski
  • Date: 2011-03-27 14:22:50 UTC
  • mfrom: (16.1.9 experimental)
  • Revision ID: james.westby@ubuntu.com-20110327142250-aip46dv6a3r2jwvs
Tags: 0.6.6-2
Upload to unstable

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
.. _associationproxy:
 
2
 
 
3
Association Proxy
 
4
=================
 
5
 
 
6
.. module:: sqlalchemy.ext.associationproxy
 
7
 
 
8
``associationproxy`` is used to create a simplified, read/write view of a
 
9
relationship.  It can be used to cherry-pick fields from a collection of
 
10
related objects or to greatly simplify access to associated objects in an
 
11
association relationship.
 
12
 
 
13
Simplifying Relationships
 
14
-------------------------
 
15
 
 
16
Consider this "association object" mapping::
 
17
 
 
18
    users_table = Table('users', metadata,
 
19
        Column('id', Integer, primary_key=True),
 
20
        Column('name', String(64)),
 
21
    )
 
22
 
 
23
    keywords_table = Table('keywords', metadata,
 
24
        Column('id', Integer, primary_key=True),
 
25
        Column('keyword', String(64))
 
26
    )
 
27
 
 
28
    userkeywords_table = Table('userkeywords', metadata,
 
29
        Column('user_id', Integer, ForeignKey("users.id"),
 
30
               primary_key=True),
 
31
        Column('keyword_id', Integer, ForeignKey("keywords.id"),
 
32
               primary_key=True)
 
33
    )
 
34
 
 
35
    class User(object):
 
36
        def __init__(self, name):
 
37
            self.name = name
 
38
 
 
39
    class Keyword(object):
 
40
        def __init__(self, keyword):
 
41
            self.keyword = keyword
 
42
 
 
43
    mapper(User, users_table, properties={
 
44
        'kw': relationship(Keyword, secondary=userkeywords_table)
 
45
        })
 
46
    mapper(Keyword, keywords_table)
 
47
 
 
48
Above are three simple tables, modeling users, keywords and a many-to-many
 
49
relationship between the two.  These ``Keyword`` objects are little more
 
50
than a container for a name, and accessing them via the relationship is
 
51
awkward::
 
52
 
 
53
    user = User('jek')
 
54
    user.kw.append(Keyword('cheese inspector'))
 
55
    print user.kw
 
56
    # [<__main__.Keyword object at 0xb791ea0c>]
 
57
    print user.kw[0].keyword
 
58
    # 'cheese inspector'
 
59
    print [keyword.keyword for keyword in user.kw]
 
60
    # ['cheese inspector']
 
61
 
 
62
With ``association_proxy`` you have a "view" of the relationship that contains
 
63
just the ``.keyword`` of the related objects.  The proxy is a Python
 
64
property, and unlike the mapper relationship, is defined in your class::
 
65
 
 
66
    from sqlalchemy.ext.associationproxy import association_proxy
 
67
 
 
68
    class User(object):
 
69
        def __init__(self, name):
 
70
            self.name = name
 
71
 
 
72
        # proxy the 'keyword' attribute from the 'kw' relationship
 
73
        keywords = association_proxy('kw', 'keyword')
 
74
 
 
75
    # ...
 
76
    >>> user.kw
 
77
    [<__main__.Keyword object at 0xb791ea0c>]
 
78
    >>> user.keywords
 
79
    ['cheese inspector']
 
80
    >>> user.keywords.append('snack ninja')
 
81
    >>> user.keywords
 
82
    ['cheese inspector', 'snack ninja']
 
83
    >>> user.kw
 
84
    [<__main__.Keyword object at 0x9272a4c>, <__main__.Keyword object at 0xb7b396ec>]
 
85
 
 
86
The proxy is read/write.  New associated objects are created on demand when
 
87
values are added to the proxy, and modifying or removing an entry through
 
88
the proxy also affects the underlying collection.
 
89
 
 
90
 - The association proxy property is backed by a mapper-defined relationship,
 
91
   either a collection or scalar.
 
92
 
 
93
 - You can access and modify both the proxy and the backing
 
94
   relationship. Changes in one are immediate in the other.
 
95
 
 
96
 - The proxy acts like the type of the underlying collection.  A list gets a
 
97
   list-like proxy, a dict a dict-like proxy, and so on.
 
98
 
 
99
 - Multiple proxies for the same relationship are fine.
 
100
 
 
101
 - Proxies are lazy, and won't trigger a load of the backing relationship until
 
102
   they are accessed.
 
103
 
 
104
 - The relationship is inspected to determine the type of the related objects.
 
105
 
 
106
 - To construct new instances, the type is called with the value being
 
107
   assigned, or key and value for dicts.
 
108
 
 
109
 - A ````creator```` function can be used to create instances instead.
 
110
 
 
111
Above, the ``Keyword.__init__`` takes a single argument ``keyword``, which
 
112
maps conveniently to the value being set through the proxy.  A ``creator``
 
113
function could have been used instead if more flexibility was required.
 
114
 
 
115
Because the proxies are backed by a regular relationship collection, all of the
 
116
usual hooks and patterns for using collections are still in effect.  The
 
117
most convenient behavior is the automatic setting of "parent"-type
 
118
relationships on assignment.  In the example above, nothing special had to
 
119
be done to associate the Keyword to the User.  Simply adding it to the
 
120
collection is sufficient.
 
121
 
 
122
Simplifying Association Object Relationships
 
123
--------------------------------------------
 
124
 
 
125
Association proxies are also useful for keeping ``association objects`` out
 
126
the way during regular use.  For example, the ``userkeywords`` table
 
127
might have a bunch of auditing columns that need to get updated when changes
 
128
are made- columns that are updated but seldom, if ever, accessed in your
 
129
application.  A proxy can provide a very natural access pattern for the
 
130
relationship.
 
131
 
 
132
.. sourcecode:: python
 
133
 
 
134
    from sqlalchemy.ext.associationproxy import association_proxy
 
135
 
 
136
    # users_table and keywords_table tables as above, then:
 
137
 
 
138
    def get_current_uid():
 
139
        """Return the uid of the current user."""
 
140
        return 1  # hardcoded for this example
 
141
 
 
142
    userkeywords_table = Table('userkeywords', metadata,
 
143
        Column('user_id', Integer, ForeignKey("users.id"), primary_key=True),
 
144
        Column('keyword_id', Integer, ForeignKey("keywords.id"), primary_key=True),
 
145
        # add some auditing columns
 
146
        Column('updated_at', DateTime, default=datetime.now),
 
147
        Column('updated_by', Integer, default=get_current_uid, onupdate=get_current_uid),
 
148
    )
 
149
 
 
150
    def _create_uk_by_keyword(keyword):
 
151
        """A creator function."""
 
152
        return UserKeyword(keyword=keyword)
 
153
 
 
154
    class User(object):
 
155
        def __init__(self, name):
 
156
            self.name = name
 
157
        keywords = association_proxy('user_keywords', 'keyword', creator=_create_uk_by_keyword)
 
158
 
 
159
    class Keyword(object):
 
160
        def __init__(self, keyword):
 
161
            self.keyword = keyword
 
162
        def __repr__(self):
 
163
            return 'Keyword(%s)' % repr(self.keyword)
 
164
 
 
165
    class UserKeyword(object):
 
166
        def __init__(self, user=None, keyword=None):
 
167
            self.user = user
 
168
            self.keyword = keyword
 
169
 
 
170
    mapper(User, users_table)
 
171
    mapper(Keyword, keywords_table)
 
172
    mapper(UserKeyword, userkeywords_table, properties={
 
173
        'user': relationship(User, backref='user_keywords'),
 
174
        'keyword': relationship(Keyword),
 
175
    })
 
176
 
 
177
    user = User('log')
 
178
    kw1  = Keyword('new_from_blammo')
 
179
 
 
180
    # Creating a UserKeyword association object will add a Keyword.
 
181
    # the "user" reference assignment in the UserKeyword() constructor
 
182
    # populates "user_keywords" via backref.
 
183
    UserKeyword(user, kw1)
 
184
 
 
185
    # Accessing Keywords requires traversing UserKeywords
 
186
    print user.user_keywords[0]
 
187
    # <__main__.UserKeyword object at 0xb79bbbec>
 
188
 
 
189
    print user.user_keywords[0].keyword
 
190
    # Keyword('new_from_blammo')
 
191
 
 
192
    # Lots of work.
 
193
 
 
194
    # It's much easier to go through the association proxy!
 
195
    for kw in (Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')):
 
196
        user.keywords.append(kw)
 
197
 
 
198
    print user.keywords
 
199
    # [Keyword('new_from_blammo'), Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')]
 
200
 
 
201
 
 
202
Building Complex Views
 
203
----------------------
 
204
 
 
205
.. sourcecode:: python
 
206
 
 
207
    stocks_table = Table("stocks", meta,
 
208
       Column('symbol', String(10), primary_key=True),
 
209
       Column('last_price', Numeric)
 
210
    )
 
211
 
 
212
    brokers_table = Table("brokers", meta,
 
213
       Column('id', Integer,primary_key=True),
 
214
       Column('name', String(100), nullable=False)
 
215
    )
 
216
 
 
217
    holdings_table = Table("holdings", meta,
 
218
      Column('broker_id', Integer, ForeignKey('brokers.id'), primary_key=True),
 
219
      Column('symbol', String(10), ForeignKey('stocks.symbol'), primary_key=True),
 
220
      Column('shares', Integer)
 
221
    )
 
222
 
 
223
Above are three tables, modeling stocks, their brokers and the number of
 
224
shares of a stock held by each broker.  This situation is quite different
 
225
from the association example above.  ``shares`` is a *property of the
 
226
relationship*, an important one that we need to use all the time.
 
227
 
 
228
For this example, it would be very convenient if ``Broker`` objects had a
 
229
dictionary collection that mapped ``Stock`` instances to the shares held for
 
230
each.  That's easy::
 
231
 
 
232
    from sqlalchemy.ext.associationproxy import association_proxy
 
233
    from sqlalchemy.orm.collections import attribute_mapped_collection
 
234
 
 
235
    def _create_holding(stock, shares):
 
236
        """A creator function, constructs Holdings from Stock and share quantity."""
 
237
        return Holding(stock=stock, shares=shares)
 
238
 
 
239
    class Broker(object):
 
240
        def __init__(self, name):
 
241
            self.name = name
 
242
 
 
243
        holdings = association_proxy('by_stock', 'shares', creator=_create_holding)
 
244
 
 
245
    class Stock(object):
 
246
        def __init__(self, symbol):
 
247
            self.symbol = symbol
 
248
            self.last_price = 0
 
249
 
 
250
    class Holding(object):
 
251
        def __init__(self, broker=None, stock=None, shares=0):
 
252
            self.broker = broker
 
253
            self.stock = stock
 
254
            self.shares = shares
 
255
 
 
256
    mapper(Stock, stocks_table)
 
257
    mapper(Broker, brokers_table, properties={
 
258
        'by_stock': relationship(Holding,
 
259
            collection_class=attribute_mapped_collection('stock'))
 
260
    })
 
261
    mapper(Holding, holdings_table, properties={
 
262
        'stock': relationship(Stock),
 
263
        'broker': relationship(Broker)
 
264
    })
 
265
 
 
266
Above, we've set up the ``by_stock`` relationship collection to act as a
 
267
dictionary, using the ``.stock`` property of each Holding as a key.
 
268
 
 
269
Populating and accessing that dictionary manually is slightly inconvenient
 
270
because of the complexity of the Holdings association object::
 
271
 
 
272
    stock = Stock('ZZK')
 
273
    broker = Broker('paj')
 
274
 
 
275
    broker.by_stock[stock] = Holding(broker, stock, 10)
 
276
    print broker.by_stock[stock].shares
 
277
    # 10
 
278
 
 
279
The ``holdings`` proxy we've added to the ``Broker`` class hides the details
 
280
of the ``Holding`` while also giving access to ``.shares``::
 
281
 
 
282
    for stock in (Stock('JEK'), Stock('STPZ')):
 
283
        broker.holdings[stock] = 123
 
284
 
 
285
    for stock, shares in broker.holdings.items():
 
286
        print stock, shares
 
287
 
 
288
    session.add(broker)
 
289
    session.commit()
 
290
 
 
291
    # lets take a peek at that holdings_table after committing changes to the db
 
292
    print list(holdings_table.select().execute())
 
293
    # [(1, 'ZZK', 10), (1, 'JEK', 123), (1, 'STEPZ', 123)]
 
294
 
 
295
Further examples can be found in the ``examples/`` directory in the
 
296
SQLAlchemy distribution.
 
297
 
 
298
API
 
299
---
 
300
 
 
301
.. autofunction:: association_proxy
 
302
 
 
303
.. autoclass:: AssociationProxy
 
304
   :members:
 
305
   :undoc-members:
 
 
b'\\ No newline at end of file'