1 import re
2 import pyperry.base
3 from pyperry import errors
4 from pyperry.relation import Relation
5
7 """
8 Associations
9 ============
10
11 Associations allow you to retrieve a model or collection of models that are
12 related to one another in some way. Here we are concerned with how
13 associations are defined and implemented. For documentation on how to use
14 associations in your models, please see the L{belongs_to
15 <pyperry.Base.belongs_to>}, L{has_one <pyperry.Base.has_one>}, and
16 L{has_many <pyperry.Base.has_many>} methods on L{pyperry.Base}.
17
18 Terminology
19 -----------
20
21 To understand how associations work, we must define the concepts of a
22 source class and a target class for an association.
23
24 - B{target class}: the class on which you define the association
25 - B{source class}: the class of the records returned by calling the
26 association method on the target class
27
28 An example showing the difference between source and target classes::
29
30 class Car(pyperry.Base): pass
31 class Part(pyperry.Base): pass
32
33 # Car is the target class and Part is the source class
34 Car.has_many('parts', class_name='Part')
35
36 # Part is the target class and Car is the source class
37 Part.belongs_to('car', class_name='Car')
38
39 c = Car.first()
40 # returns a collection of Part models (Part is the source class)
41 c.parts()
42
43 p = Part.first()
44 # returns a Car model (Car is the source class)
45 p.car()
46
47 What happens when you define an association
48 -------------------------------------------
49
50 Now let's look at what happens when you define an association on a model.
51 We will use the association C{Car.has_many('parts', class_name='Part')} as
52 an example because all associations work in the same general way. In this
53 example, a C{HasMany} class (an C{Association} subclass) is instantiated
54 where C{Car} is given as the C{target_klass} argument, and C{'parts'} is
55 given as the C{id} argument. Because we passed in C{'Part'} for the
56 C{class_name} option, it is used as the source class for this association.
57
58 The association id is used as the name of the association method that gets
59 defined on the target class. So in our example, all C{Car} instances now
60 have a C{parts()} method that can be called to retrieve a collection of
61 parts for a car. When you call the association method on the target class,
62 a relation (or scope) is constructed for the source class representing all
63 of the records related to the target class. For associations that represent
64 collections, such as C{has_many}, a relation is returned that you can
65 further modify. For associations that represent a single object, such as
66 C{belongs_to} or C{has_one}, an instance of that model is returned.
67
68 In summary, all an association does is create a scope on the source class
69 that represents records from the source class that are related to (or
70 associated with) the target class. Then a method on the target class is
71 created that returns this scope.
72
73 This means that calling C{car.parts()} is just returning a scope like::
74
75 Part.scoped().where({'car_id': car.id})
76
77 Similarly, calling C{part.car()} is just returning a scope like::
78
79 Car.scoped().where({'id': part.car_id}).first()
80
81 """
82
83 - def __init__(self, target_klass, id, **kwargs):
84 self.target_klass = target_klass
85 self.id = id
86 self.options = kwargs
87
92
94 raise NotImplementedError, 'You must define the type in subclasses.'
95
97 raise NotImplementedError, ('You must define how your association is'
98 'polymorphic in subclasses.')
99
101 raise NotImplementedError, 'You must define collection in subclasses.'
102
104 raise NotImplementedError, 'You must define scope in subclasses.'
105
116
117
119 return self.options.get('foreign_key')
120
122 self.options['foreign_key'] = value
123 foreign_key = property(get_foreign_key, set_foreign_key)
124
126 for option in self.finder_options():
127 if type(self.options.get(option)).__name__ == 'function':
128 return False
129 return not (self.type() == 'belongs_to' and self.polymorphic())
130
131
135
136
138 eager_loading = isinstance(obj, list)
139 poly_type = None
140 if (self.options.has_key('polymorphic') and
141 self.options['polymorphic'] and obj):
142 if type(obj.__class__) in [pyperry.base.Base, pyperry.base.BaseMeta]:
143 poly_type = getattr(obj, '%s_type' % self.id)
144 else:
145 poly_type = obj
146
147 if eager_loading and not self.eager_loadable():
148 raise errors.AssociationPreloadNotSupported(
149 "This association cannot be eager loaded. It either has "
150 "a config with callables, or it is a polymorphic belongs "
151 "to association.")
152
153 if poly_type:
154 type_list = [
155 self.options.get('namespace'),
156 self._sanitize_type_attribute(poly_type)
157 ]
158 type_string = '.'.join([arg for arg in type_list if arg])
159 return self._get_resolved_class(type_string)
160 else:
161 if not self.options.get('klass') and not self.options.get('class_name'):
162 raise errors.ArgumentError, ('klass or class_name option'
163 ' required for association declaration.')
164 if self.options.get('klass'):
165 if type(self.options.get('klass')).__name__ == 'function':
166 return self.options.get('klass')()
167 return self.options.get('klass')
168 elif self.options.get('class_name'):
169 type_list = [
170 self.options.get('namespace'),
171 self.options['class_name']
172 ]
173 type_string = '.'.join([arg for arg in type_list if arg])
174 return self._get_resolved_class(type_string)
175
177 class_name = self.target_klass.resolve_name(string)
178 if not class_name:
179 raise errors.ModelNotDefined, 'Model %s is not defined.' % (string)
180 elif len(class_name) > 1:
181 raise errors.AmbiguousClassName, ('Class name %s is'
182 ' ambiguous. Use the namespace option to get your'
183 ' specific class. Got classes %s' % (string, str(class_name)))
184 return class_name[0]
185
189
191 opts = {}
192 for option in self.finder_options():
193 value = self.options.get(option)
194 if value:
195 if type(self.options[option]).__name__ == 'function':
196 opts[option] = value()
197 else:
198 opts[option] = value
199 return opts
200
202 return re.sub('[^a-zA-z]\w*', '', string)
203
205 """
206 Builds the association scope for a C{belongs} association. See the
207 L{Association} class for more details on how associations work.
208
209 """
210
213
216
217
220
222 self.options['foreign_key'] = value
223 foreign_key = property(get_foreign_key, set_foreign_key)
224
226 return self.options.has_key('polymorphic') and self.options['polymorphic']
227
229 return '%s_type' % self.id
230
231 - def scope(self, obj_or_list):
232 """
233 Returns a scope on the source class containing this association
234
235 Builds conditions on top of the base_scope generated from any finder
236 options set with the association::
237
238 belongs_to('foo', foreign_key='foo_id')
239
240 In addition to any finder options included with the association options
241 the following scope will be added::
242
243 where('id = %s' % target['foo_id'])
244
245 """
246 if isinstance(obj_or_list, pyperry.Base):
247 keys = obj_or_list[self.foreign_key]
248 else:
249 keys = [o[self.foreign_key] for o in obj_or_list]
250
251 if keys is not None:
252 return self._base_scope(obj_or_list).where({
253 self.primary_key(obj_or_list): keys
254 })
255
256 -class Has(Association):
257 """
258 Builds the association scope for a C{has} association. This is the
259 superclass for L{HasOne} and L{HasMany} associations. The only difference
260 between a has one and has many relation, is that C{.first()} is called on
261 the has one association's scope but not on the has many association's
262 scope. See the L{Association} class for more details on how associations
263 work.
264
265 """
266
267
269 if super(Has, self).foreign_key:
270 return super(Has, self).foreign_key
271 elif self.polymorphic():
272 return '%s_id' % self.options['as_']
273 else:
274 return '%s_id' % self.target_klass.__name__.lower()
275
277 self.options['foreign_key'] = value
278 foreign_key = property(get_foreign_key, set_foreign_key)
279
281 return self.options.has_key('as_')
282
284 return '%s_type' % self.options['as_']
285
286 - def scope(self, obj_or_list):
287 """
288 Returns a scope on the source class containing this association
289
290 Builds conditions on top of the base_scope generated from any finder
291 options set with the association::
292
293 has_many('widgets', klass=Widget, foreign_key='widget_id')
294 has_many('comments', as_='parent')
295
296 In addition to any finder options included with the association options
297 the following will be added::
298
299 where('widget_id = %s ' % target['id'])
300
301 Or for the polymorphic :comments association::
302
303 where('parent_id = %s AND parent_type = %s' % (target['id'],
304 target.class))
305
306 """
307 pk_attr = self.primary_key()
308 if isinstance(obj_or_list, pyperry.Base):
309 keys = obj_or_list[pk_attr]
310 obj = obj_or_list
311 else:
312 keys = [o[pk_attr] for o in obj_or_list]
313 obj = obj_or_list[0]
314
315 if keys is not None:
316 scope = self._base_scope(obj_or_list).where({
317 self.foreign_key: keys
318 })
319 if self.polymorphic():
320 scope = scope.where({
321 self.polymorphic_type(): obj.__class__.__name__
322 })
323 return scope
324
326 """
327 The C{HasMany} class simply declares that the L{Has} association is a
328 collection of type C{'has_many'}.
329
330 """
331
334
337
339 """
340 The C{HasOne} class simply declares that the L{Has} association is a
341 not a collection and has a type of C{'has_one'}.
342
343 """
344
347
350
352 """
353 The C{HasManyThrough} class is used whenever a C{has_many} association is
354 created with the C{through} option. It works by using the association id
355 given with the through option as a I{proxy} association to another class
356 on which a I{source} association is defined. The source class for a has
357 many through association will be the source class of the source
358 association. This means that the scope for a has many through association
359 is actually two scopes chained together. The proxy scope is used to
360 retrieve the records on which to build the source association, which is
361 used to build a scope for the records represented by the has many through
362 association. The proxy and source associations may be any of the simple has
363 or belongs to association types.
364
365 B{Options specific to has many through associations}
366
367 - B{through}: the association id of an association defined on the
368 target class. This association will be used as the proxy association,
369 and it is an error if this association does not exist.
370
371 - B{source}: the id of the source association. If the source option is
372 not specified, we assume that the source association's id is the same
373 as the has many through association's id.
374
375 - B{source_type}: the name of the source class as a string. This option
376 may be required if the source class is ambiguous, such as when the
377 source association is a polymorphic belongs_to association.
378
379 B{Has many through example}
380
381 This example shows how to create a basic has many through relationship in
382 which the internet has many connected devices through its networks. The has
383 many through association is defined on the Internet class. Notice how the
384 proxy association, C{networks}, is defined on the target class,
385 C{Internet}, and the source association, C{devices}, is defined on the
386 proxy association's source class, C{Network}. Therefore, the source class
387 for the entire relation is the source association's source class,
388 C{Device}::
389
390 class Internet(pyperry.Base):
391 def _config(cls):
392 cls.attributes('id')
393 cls.has_many('connected_devices', through='networks', source='devices')
394 cls.has_many('networks', class_name='Network')
395
396 class Network(pyperry.Base):
397 def _config(cls):
398 cls.attributes('id', 'internet_id')
399 cls.belongs_to('internet', class_name='Internet')
400 cls.has_many('devices', class_name='Device')
401
402 class Device(pyperry.Base):
403 def _config(cls):
404 cls.attributes('id', 'network_id')
405 cls.belongs_to('network', class_name='Network')
406
407 """
408
410 return 'has_many_through'
411
414
417
419 if not hasattr(self, '_proxy_association'):
420 through = self.options.get('through')
421 proxy = self.target_klass.defined_associations.get(through)
422 if not proxy: raise errors.AssociationNotFound(
423 "has_many_through: '%s' is not an association on %s" % (
424 str(through), str(self.target_klass)))
425 self._proxy_association = proxy
426 return self._proxy_association
427
439
443
469