|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 # |
|
17 |
|
18 """Support for polymorphic models and queries. |
|
19 |
|
20 The Model class on its own is only able to support functional polymorphism. |
|
21 It is possible to create a subclass of Model and then subclass that one as |
|
22 many generations as necessary and those classes will share all the same |
|
23 properties and behaviors. The problem is that subclassing Model in this way |
|
24 places each subclass in their own Kind. This means that it is not possible |
|
25 to do polymorphic queries. Building a query on a base class will only return |
|
26 instances of that class from the Datastore, while queries on a subclass will |
|
27 only return those instances. |
|
28 |
|
29 This module allows applications to specify class hierarchies that support |
|
30 polymorphic queries. |
|
31 """ |
|
32 |
|
33 |
|
34 from google.appengine.ext import db |
|
35 |
|
36 _class_map = {} |
|
37 |
|
38 _CLASS_KEY_PROPERTY = 'class' |
|
39 |
|
40 |
|
41 class _ClassKeyProperty(db.ListProperty): |
|
42 """Property representing class-key property of a polymorphic class. |
|
43 |
|
44 The class key is a list of strings describing an polymorphic instances |
|
45 place within its class hierarchy. This property is automatically calculated. |
|
46 For example: |
|
47 |
|
48 class Foo(PolyModel): ... |
|
49 class Bar(Foo): ... |
|
50 class Baz(Bar): ... |
|
51 |
|
52 Foo.class_key() == ['Foo'] |
|
53 Bar.class_key() == ['Foo', 'Bar'] |
|
54 Baz.class_key() == ['Foo', 'Bar', 'Baz'] |
|
55 """ |
|
56 |
|
57 def __init__(self, name): |
|
58 super(_ClassKeyProperty, self).__init__(name=name, |
|
59 item_type=str, |
|
60 default=None) |
|
61 |
|
62 def __set__(self, *args): |
|
63 raise db.DerivedPropertyError( |
|
64 'Class-key is a derived property and cannot be set.') |
|
65 |
|
66 def __get__(self, model_instance, model_class): |
|
67 if model_instance is None: |
|
68 return self |
|
69 return [cls.__name__ for cls in model_class.__class_hierarchy__] |
|
70 |
|
71 |
|
72 class PolymorphicClass(db.PropertiedClass): |
|
73 """Meta-class for initializing PolymorphicClasses. |
|
74 |
|
75 This class extends PropertiedClass to add a few static attributes to |
|
76 new polymorphic classes necessary for their correct functioning. |
|
77 |
|
78 """ |
|
79 |
|
80 def __init__(cls, name, bases, dct): |
|
81 """Initializes a class that belongs to a polymorphic hierarchy. |
|
82 |
|
83 This method configures a few built-in attributes of polymorphic |
|
84 models: |
|
85 |
|
86 __root_class__: If the new class is a root class, __root_class__ is set to |
|
87 itself so that it subclasses can quickly know what the root of |
|
88 their hierarchy is and what kind they are stored in. |
|
89 __class_hierarchy__: List of classes describing the new model's place |
|
90 in the class hierarchy. The first element is always the root |
|
91 element while the last element is the new class itself. For example: |
|
92 |
|
93 class Foo(PolymorphicClass): ... |
|
94 |
|
95 class Bar(Foo): ... |
|
96 |
|
97 class Baz(Bar): ... |
|
98 |
|
99 Foo.__class_hierarchy__ == [Foo] |
|
100 Bar.__class_hierarchy__ == [Foo, Bar] |
|
101 Baz.__class_hierarchy__ == [Foo, Bar, Baz] |
|
102 |
|
103 Unless the class is a root class or PolyModel itself, it is not |
|
104 inserted in to the kind-map like other models. However, all polymorphic |
|
105 classes, are inserted in to the class-map which maps the class-key to |
|
106 implementation. This class key is consulted using the polymorphic instances |
|
107 discriminator (the 'class' property of the entity) when loading from the |
|
108 datastore. |
|
109 """ |
|
110 if name == 'PolyModel' or PolyModel not in bases: |
|
111 db._initialize_properties(cls, name, bases, dct) |
|
112 super(db.PropertiedClass, cls).__init__(name, bases, dct) |
|
113 else: |
|
114 cls.__root_class__ = cls |
|
115 super(PolymorphicClass, cls).__init__(name, bases, dct) |
|
116 |
|
117 if name == 'PolyModel': |
|
118 return |
|
119 |
|
120 if cls is not cls.__root_class__: |
|
121 poly_class = None |
|
122 for base in cls.__bases__: |
|
123 if issubclass(base, PolyModel): |
|
124 poly_class = base |
|
125 break |
|
126 else: |
|
127 raise db.ConfigurationError( |
|
128 "Polymorphic class '%s' does not inherit from PolyModel." |
|
129 % cls.__name__) |
|
130 |
|
131 cls.__class_hierarchy__ = poly_class.__class_hierarchy__ + [cls] |
|
132 else: |
|
133 cls.__class_hierarchy__ = [cls] |
|
134 |
|
135 _class_map[cls.class_key()] = cls |
|
136 |
|
137 |
|
138 class PolyModel(db.Model): |
|
139 """Base-class for models that supports polymorphic queries. |
|
140 |
|
141 Use this class to build hierarchies that can be queried based |
|
142 on their types. |
|
143 |
|
144 Example: |
|
145 |
|
146 consider the following model hierarchy: |
|
147 |
|
148 +------+ |
|
149 |Animal| |
|
150 +------+ |
|
151 | |
|
152 +-----------------+ |
|
153 | | |
|
154 +------+ +------+ |
|
155 |Canine| |Feline| |
|
156 +------+ +------+ |
|
157 | | |
|
158 +-------+ +-------+ |
|
159 | | | | |
|
160 +---+ +----+ +---+ +-------+ |
|
161 |Dog| |Wolf| |Cat| |Panther| |
|
162 +---+ +----+ +---+ +-------+ |
|
163 |
|
164 This class hierarchy has three levels. The first is the "root class". |
|
165 All models in a single class hierarchy must inherit from this root. All |
|
166 models in the hierarchy are stored as the same kind as the root class. |
|
167 For example, Panther entities when stored to the datastore are of the kind |
|
168 'Animal'. Querying against the Animal kind will retrieve Cats, Dogs and |
|
169 Canines, for example, that match your query. Different classes stored |
|
170 in the root class' kind are identified by their class-key. When loaded |
|
171 from the datastore, it is mapped to the appropriate implementation class. |
|
172 |
|
173 Polymorphic properties: |
|
174 |
|
175 Properties that are defined in a given base-class within a hierarchy are |
|
176 stored in the datastore for all sub-casses only. So, if the Feline class |
|
177 had a property called 'whiskers', the Cat and Panther enties would also |
|
178 have whiskers, but not Animal, Canine, Dog or Wolf. |
|
179 |
|
180 Polymorphic queries: |
|
181 |
|
182 When written to the datastore, all polymorphic objects automatically have |
|
183 a property called 'class' that you can query against. Using this property |
|
184 it is possible to easily write a GQL query against any sub-hierarchy. For |
|
185 example, to fetch only Canine objects, including all Dogs and Wolves: |
|
186 |
|
187 db.GqlQuery("SELECT * FROM Animal WHERE class='Canine'") |
|
188 |
|
189 And alternate method is to use the 'all' or 'gql' methods of the Canine |
|
190 class: |
|
191 |
|
192 Canine.all() |
|
193 Canine.gql('') |
|
194 |
|
195 The 'class' property is not meant to be used by your code other than |
|
196 for queries. Since it is supposed to represents the real Python class |
|
197 it is intended to be hidden from view. |
|
198 |
|
199 Root class: |
|
200 |
|
201 The root class is the class from which all other classes of the hierarchy |
|
202 inherits from. Each hierarchy has a single root class. A class is a |
|
203 root class if it is an immediate child of PolyModel. The subclasses of |
|
204 the root class are all the same kind as the root class. In other words: |
|
205 |
|
206 Animal.kind() == Feline.kind() == Panther.kind() == 'Animal' |
|
207 """ |
|
208 |
|
209 __metaclass__ = PolymorphicClass |
|
210 |
|
211 _class = _ClassKeyProperty(name=_CLASS_KEY_PROPERTY) |
|
212 |
|
213 def __new__(cls, *args, **kwds): |
|
214 """Prevents direct instantiation of PolyModel.""" |
|
215 if cls is PolyModel: |
|
216 raise NotImplementedError() |
|
217 return super(PolyModel, cls).__new__(cls, *args, **kwds) |
|
218 |
|
219 @classmethod |
|
220 def kind(cls): |
|
221 """Get kind of polymorphic model. |
|
222 |
|
223 Overridden so that all subclasses of root classes are the same kind |
|
224 as the root. |
|
225 |
|
226 Returns: |
|
227 Kind of entity to write to datastore. |
|
228 """ |
|
229 if cls is cls.__root_class__: |
|
230 return super(PolyModel, cls).kind() |
|
231 else: |
|
232 return cls.__root_class__.kind() |
|
233 |
|
234 @classmethod |
|
235 def class_key(cls): |
|
236 """Caclulate the class-key for this class. |
|
237 |
|
238 Returns: |
|
239 Class key for class. By default this is a the list of classes |
|
240 of the hierarchy, starting with the root class and walking its way |
|
241 down to cls. |
|
242 """ |
|
243 if not hasattr(cls, '__class_hierarchy__'): |
|
244 raise NotImplementedError( |
|
245 'Cannot determine class key without class hierarchy') |
|
246 return tuple(cls.class_name() for cls in cls.__class_hierarchy__) |
|
247 |
|
248 @classmethod |
|
249 def class_name(cls): |
|
250 """Calculate class name for this class. |
|
251 |
|
252 Returns name to use for each classes element within its class-key. Used |
|
253 to discriminate between different classes within a class hierarchy's |
|
254 Datastore kind. |
|
255 |
|
256 The presence of this method allows developers to use a different class |
|
257 name in the datastore from what is used in Python code. This is useful, |
|
258 for example, for renaming classes without having to migrate instances |
|
259 already written to the datastore. For example, to rename a polymorphic |
|
260 class Contact to SimpleContact, you could convert: |
|
261 |
|
262 # Class key is ['Information'] |
|
263 class Information(PolyModel): ... |
|
264 |
|
265 # Class key is ['Information', 'Contact'] |
|
266 class Contact(Information): ... |
|
267 |
|
268 to: |
|
269 |
|
270 # Class key is still ['Information', 'Contact'] |
|
271 class SimpleContact(Information): |
|
272 ... |
|
273 @classmethod |
|
274 def class_name(cls): |
|
275 return 'Contact' |
|
276 |
|
277 # Class key is ['Information', 'Contact', 'ExtendedContact'] |
|
278 class ExtendedContact(SimpleContact): ... |
|
279 |
|
280 This would ensure that all objects written previously using the old class |
|
281 name would still be loaded. |
|
282 |
|
283 Returns: |
|
284 Name of this class. |
|
285 """ |
|
286 return cls.__name__ |
|
287 |
|
288 @classmethod |
|
289 def from_entity(cls, entity): |
|
290 """Load from entity to class based on discriminator. |
|
291 |
|
292 Rather than instantiating a new Model instance based on the kind |
|
293 mapping, this creates an instance of the correct model class based |
|
294 on the entities class-key. |
|
295 |
|
296 Args: |
|
297 entity: Entity loaded directly from datastore. |
|
298 |
|
299 Raises: |
|
300 KindError when there is no class mapping based on discriminator. |
|
301 """ |
|
302 if (_CLASS_KEY_PROPERTY in entity and |
|
303 tuple(entity[_CLASS_KEY_PROPERTY]) != cls.class_key()): |
|
304 key = tuple(entity[_CLASS_KEY_PROPERTY]) |
|
305 try: |
|
306 poly_class = _class_map[key] |
|
307 except KeyError: |
|
308 raise db.KindError('No implementation for class \'%s\'' % key) |
|
309 return poly_class.from_entity(entity) |
|
310 return super(PolyModel, cls).from_entity(entity) |
|
311 |
|
312 @classmethod |
|
313 def all(cls): |
|
314 """Get all instance of a class hierarchy. |
|
315 |
|
316 Returns: |
|
317 Query with filter set to match this class' discriminator. |
|
318 """ |
|
319 query = super(PolyModel, cls).all() |
|
320 if cls != cls.__root_class__: |
|
321 query.filter(_CLASS_KEY_PROPERTY + ' =', cls.class_name()) |
|
322 return query |
|
323 |
|
324 @classmethod |
|
325 def gql(cls, query_string, *args, **kwds): |
|
326 """Returns a polymorphic query using GQL query string. |
|
327 |
|
328 This query is polymorphic in that it has its filters configured in a way |
|
329 to retrieve instances of the model or an instance of a subclass of the |
|
330 model. |
|
331 |
|
332 Args: |
|
333 query_string: properly formatted GQL query string with the |
|
334 'SELECT * FROM <entity>' part omitted |
|
335 *args: rest of the positional arguments used to bind numeric references |
|
336 in the query. |
|
337 **kwds: dictionary-based arguments (for named parameters). |
|
338 """ |
|
339 if cls == cls.__root_class__: |
|
340 return super(PolyModel, cls).gql(query_string, *args, **kwds) |
|
341 else: |
|
342 from google.appengine.ext import gql |
|
343 |
|
344 query = db.GqlQuery('SELECT * FROM %s %s' % (cls.kind(), query_string)) |
|
345 |
|
346 query_filter = [('nop', |
|
347 [gql.Literal(cls.class_name())])] |
|
348 query._proto_query.filters()[('class', '=')] = query_filter |
|
349 query.bind(*args, **kwds) |
|
350 return query |