blob: ff7d1a091ee23edee50f3ccc9327f0a8d8f50075 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Support for polymorphic models and queries.
The Model class on its own is only able to support functional polymorphism.
It is possible to create a subclass of Model and then subclass that one as
many generations as necessary and those classes will share all the same
properties and behaviors. The problem is that subclassing Model in this way
places each subclass in their own Kind. This means that it is not possible
to do polymorphic queries. Building a query on a base class will only return
instances of that class from the Datastore, while queries on a subclass will
only return those instances.
This module allows applications to specify class hierarchies that support
polymorphic queries.
"""
from google.appengine.ext import db
_class_map = {}
_CLASS_KEY_PROPERTY = 'class'
class _ClassKeyProperty(db.ListProperty):
"""Property representing class-key property of a polymorphic class.
The class key is a list of strings describing an polymorphic instances
place within its class hierarchy. This property is automatically calculated.
For example:
class Foo(PolyModel): ...
class Bar(Foo): ...
class Baz(Bar): ...
Foo.class_key() == ['Foo']
Bar.class_key() == ['Foo', 'Bar']
Baz.class_key() == ['Foo', 'Bar', 'Baz']
"""
def __init__(self, name):
super(_ClassKeyProperty, self).__init__(name=name,
item_type=str,
default=None)
def __set__(self, *args):
raise db.DerivedPropertyError(
'Class-key is a derived property and cannot be set.')
def __get__(self, model_instance, model_class):
if model_instance is None:
return self
return [cls.__name__ for cls in model_class.__class_hierarchy__]
class PolymorphicClass(db.PropertiedClass):
"""Meta-class for initializing PolymorphicClasses.
This class extends PropertiedClass to add a few static attributes to
new polymorphic classes necessary for their correct functioning.
"""
def __init__(cls, name, bases, dct):
"""Initializes a class that belongs to a polymorphic hierarchy.
This method configures a few built-in attributes of polymorphic
models:
__root_class__: If the new class is a root class, __root_class__ is set to
itself so that it subclasses can quickly know what the root of
their hierarchy is and what kind they are stored in.
__class_hierarchy__: List of classes describing the new model's place
in the class hierarchy in reverse MRO order. The first element is
always the root class while the last element is always the new class.
MRO documentation: http://www.python.org/download/releases/2.3/mro/
For example:
class Foo(PolymorphicClass): ...
class Bar(Foo): ...
class Baz(Bar): ...
Foo.__class_hierarchy__ == [Foo]
Bar.__class_hierarchy__ == [Foo, Bar]
Baz.__class_hierarchy__ == [Foo, Bar, Baz]
Unless the class is a root class or PolyModel itself, it is not
inserted in to the kind-map like other models. However, all polymorphic
classes, are inserted in to the class-map which maps the class-key to
implementation. This class key is consulted using the polymorphic instances
discriminator (the 'class' property of the entity) when loading from the
datastore.
"""
if name == 'PolyModel':
super(PolymorphicClass, cls).__init__(name, bases, dct, map_kind=False)
return
elif PolyModel in bases:
if getattr(cls, '__class_hierarchy__', None):
raise db.ConfigurationError(('%s cannot derive from PolyModel as '
'__class_hierarchy__ is already defined.') % cls.__name__)
cls.__class_hierarchy__ = [cls]
cls.__root_class__ = cls
super(PolymorphicClass, cls).__init__(name, bases, dct)
else:
super(PolymorphicClass, cls).__init__(name, bases, dct, map_kind=False)
cls.__class_hierarchy__ = [c for c in reversed(cls.mro())
if issubclass(c, PolyModel) and c != PolyModel]
if cls.__class_hierarchy__[0] != cls.__root_class__:
raise db.ConfigurationError(
'%s cannot be derived from both root classes %s and %s' %
(cls.__name__,
cls.__class_hierarchy__[0].__name__,
cls.__root_class__.__name__))
_class_map[cls.class_key()] = cls
class PolyModel(db.Model):
"""Base-class for models that supports polymorphic queries.
Use this class to build hierarchies that can be queried based
on their types.
Example:
consider the following model hierarchy:
+------+
|Animal|
+------+
|
+-----------------+
| |
+------+ +------+
|Canine| |Feline|
+------+ +------+
| |
+-------+ +-------+
| | | |
+---+ +----+ +---+ +-------+
|Dog| |Wolf| |Cat| |Panther|
+---+ +----+ +---+ +-------+
This class hierarchy has three levels. The first is the "root class".
All models in a single class hierarchy must inherit from this root. All
models in the hierarchy are stored as the same kind as the root class.
For example, Panther entities when stored to the datastore are of the kind
'Animal'. Querying against the Animal kind will retrieve Cats, Dogs and
Canines, for example, that match your query. Different classes stored
in the root class' kind are identified by their class-key. When loaded
from the datastore, it is mapped to the appropriate implementation class.
Polymorphic properties:
Properties that are defined in a given base-class within a hierarchy are
stored in the datastore for all sub-casses only. So, if the Feline class
had a property called 'whiskers', the Cat and Panther enties would also
have whiskers, but not Animal, Canine, Dog or Wolf.
Polymorphic queries:
When written to the datastore, all polymorphic objects automatically have
a property called 'class' that you can query against. Using this property
it is possible to easily write a GQL query against any sub-hierarchy. For
example, to fetch only Canine objects, including all Dogs and Wolves:
db.GqlQuery("SELECT * FROM Animal WHERE class='Canine'")
And alternate method is to use the 'all' or 'gql' methods of the Canine
class:
Canine.all()
Canine.gql('')
The 'class' property is not meant to be used by your code other than
for queries. Since it is supposed to represents the real Python class
it is intended to be hidden from view.
Root class:
The root class is the class from which all other classes of the hierarchy
inherits from. Each hierarchy has a single root class. A class is a
root class if it is an immediate child of PolyModel. The subclasses of
the root class are all the same kind as the root class. In other words:
Animal.kind() == Feline.kind() == Panther.kind() == 'Animal'
"""
__metaclass__ = PolymorphicClass
_class = _ClassKeyProperty(name=_CLASS_KEY_PROPERTY)
def __new__(*args, **kwds):
"""Prevents direct instantiation of PolyModel.
Allow subclasses to call __new__() with arguments.
Do NOT list 'cls' as the first argument, or in the case when
the 'kwds' dictionary contains the key 'cls', the function
will complain about multiple argument values for 'cls'.
Raises:
TypeError if there are no positional arguments.
"""
if args:
cls = args[0]
else:
raise TypeError('object.__new__(): not enough arguments')
if cls is PolyModel:
raise NotImplementedError()
return super(PolyModel, cls).__new__(cls, *args, **kwds)
@classmethod
def kind(cls):
"""Get kind of polymorphic model.
Overridden so that all subclasses of root classes are the same kind
as the root.
Returns:
Kind of entity to write to datastore.
"""
if cls is cls.__root_class__:
return super(PolyModel, cls).kind()
else:
return cls.__root_class__.kind()
@classmethod
def class_key(cls):
"""Caclulate the class-key for this class.
Returns:
Class key for class. By default this is a the list of classes
of the hierarchy, starting with the root class and walking its way
down to cls.
"""
if not hasattr(cls, '__class_hierarchy__'):
raise NotImplementedError(
'Cannot determine class key without class hierarchy')
return tuple(cls.class_name() for cls in cls.__class_hierarchy__)
@classmethod
def class_name(cls):
"""Calculate class name for this class.
Returns name to use for each classes element within its class-key. Used
to discriminate between different classes within a class hierarchy's
Datastore kind.
The presence of this method allows developers to use a different class
name in the datastore from what is used in Python code. This is useful,
for example, for renaming classes without having to migrate instances
already written to the datastore. For example, to rename a polymorphic
class Contact to SimpleContact, you could convert:
# Class key is ['Information']
class Information(PolyModel): ...
# Class key is ['Information', 'Contact']
class Contact(Information): ...
to:
# Class key is still ['Information', 'Contact']
class SimpleContact(Information):
...
@classmethod
def class_name(cls):
return 'Contact'
# Class key is ['Information', 'Contact', 'ExtendedContact']
class ExtendedContact(SimpleContact): ...
This would ensure that all objects written previously using the old class
name would still be loaded.
Returns:
Name of this class.
"""
return cls.__name__
@classmethod
def from_entity(cls, entity):
"""Load from entity to class based on discriminator.
Rather than instantiating a new Model instance based on the kind
mapping, this creates an instance of the correct model class based
on the entities class-key.
Args:
entity: Entity loaded directly from datastore.
Raises:
KindError when there is no class mapping based on discriminator.
"""
if (_CLASS_KEY_PROPERTY in entity and
tuple(entity[_CLASS_KEY_PROPERTY]) != cls.class_key()):
key = tuple(entity[_CLASS_KEY_PROPERTY])
try:
poly_class = _class_map[key]
except KeyError:
raise db.KindError('No implementation for class \'%s\'' % (key,))
return poly_class.from_entity(entity)
return super(PolyModel, cls).from_entity(entity)
@classmethod
def all(cls, **kwds):
"""Get all instance of a class hierarchy.
Args:
kwds: Keyword parameters passed on to Model.all.
Returns:
Query with filter set to match this class' discriminator.
"""
query = super(PolyModel, cls).all(**kwds)
if cls != cls.__root_class__:
query.filter(_CLASS_KEY_PROPERTY + ' =', cls.class_name())
return query
@classmethod
def gql(cls, query_string, *args, **kwds):
"""Returns a polymorphic query using GQL query string.
This query is polymorphic in that it has its filters configured in a way
to retrieve instances of the model or an instance of a subclass of the
model.
Args:
query_string: properly formatted GQL query string with the
'SELECT * FROM <entity>' part omitted
*args: rest of the positional arguments used to bind numeric references
in the query.
**kwds: dictionary-based arguments (for named parameters).
"""
if cls == cls.__root_class__:
return super(PolyModel, cls).gql(query_string, *args, **kwds)
else:
from google.appengine.ext import gql
query = db.GqlQuery('SELECT * FROM %s %s' % (cls.kind(), query_string))
query_filter = [('nop',
[gql.Literal(cls.class_name())])]
query._proto_query.filters()[('class', '=')] = query_filter
query.bind(*args, **kwds)
return query