| """Polymorphic models and queries. |
| |
| The standard NDB Model class only supports 'functional polymorphism'. |
| That is, you can create a subclass of Model, and then subclass that |
| class, as many generations as necessary, and those classes will share |
| all the same properties and behaviors of their base classes. However, |
| subclassing Model in this way gives each subclass its own kind. This |
| means that it is not possible to do 'polymorphic queries'. Building a |
| query on a base class will only return entities whose kind matches |
| that base class's kind, and exclude entities that are instances of |
| some subclass of that base class. |
| |
| The PolyModel class defined here lets you create class hierarchies |
| that support polymorphic queries. Simply subclass PolyModel instead |
| of Model. |
| """ |
| |
| from . import model |
| |
| __all__ = ['PolyModel'] |
| |
| _CLASS_KEY_PROPERTY = 'class' |
| |
| |
| class _ClassKeyProperty(model.StringProperty): |
| """Property to store the 'class key' of a polymorphic class. |
| |
| The class key is a list of strings describing a polymorphic entity's |
| place within its class hierarchy. This property is automatically |
| calculated. For example: |
| |
| class Foo(PolyModel): ... |
| class Bar(Foo): ... |
| class Baz(Bar): ... |
| |
| Foo().class_ == ['Foo'] |
| Bar().class_ == ['Foo', 'Bar'] |
| Baz().class_ == ['Foo', 'Bar', 'Baz'] |
| """ |
| |
| def __init__(self, name=_CLASS_KEY_PROPERTY, indexed=True): |
| """Constructor. |
| |
| If you really want to you can give this a different datastore name |
| or make it unindexed. For example: |
| |
| class Foo(PolyModel): |
| class_ = _ClassKeyProperty(indexed=False) |
| """ |
| super(_ClassKeyProperty, self).__init__(name=name, indexed=indexed, |
| repeated=True) |
| |
| def _set_value(self, entity, value): |
| """The class_ property is read-only from the user's perspective.""" |
| raise TypeError('%s is a read-only property' % self._code_name) |
| |
| def _get_value(self, entity): |
| """Compute and store a default value if necessary.""" |
| value = super(_ClassKeyProperty, self)._get_value(entity) |
| if not value: |
| value = entity._class_key() |
| self._store_value(entity, value) |
| return value |
| |
| def _prepare_for_put(self, entity): |
| """Ensure the class_ property is initialized before it is serialized.""" |
| self._get_value(entity) # For its side effects. |
| |
| |
| class PolyModel(model.Model): |
| """Base class for class hierarchies supporting 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 subclasses 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 |
| query against any sub-hierarchy. For example, to fetch only |
| Canine objects, including all Dogs and Wolves: |
| |
| Canine.query() |
| |
| 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. (Although if |
| you feel the need, it is accessible as the 'class_' attribute.) |
| |
| 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' |
| |
| Note: |
| |
| All classes in a given hierarchy must have unique names, since |
| the class name is used to identify the appropriate subclass. |
| """ |
| |
| class_ = _ClassKeyProperty() |
| |
| _class_map = {} # Map class key -> suitable subclass. |
| |
| @classmethod |
| def _update_kind_map(cls): |
| """Override; called by Model._fix_up_properties(). |
| |
| Update the kind map as well as the class map, except for PolyModel |
| itself (its class key is empty). Note that the kind map will |
| contain entries for all classes in a PolyModel hierarchy; they all |
| have the same kind, but different class names. PolyModel class |
| names, like regular Model class names, must be globally unique. |
| """ |
| cls._kind_map[cls._class_name()] = cls |
| class_key = cls._class_key() |
| if class_key: |
| cls._class_map[tuple(class_key)] = cls |
| |
| @classmethod |
| def _from_pb(cls, pb, set_key=True, ent=None, key=None): |
| """Override. |
| |
| Use the class map to give the entity the correct subclass. |
| """ |
| prop_name = cls.class_._name |
| class_name = [] |
| for plist in [pb.property_list(), pb.raw_property_list()]: |
| for p in plist: |
| if p.name() == prop_name: |
| class_name.append(p.value().stringvalue()) |
| cls = cls._class_map.get(tuple(class_name), cls) |
| return super(PolyModel, cls)._from_pb(pb, set_key, ent, key) |
| |
| @classmethod |
| def _class_key(cls): |
| """Return the class key. |
| |
| This is a list of class names, e.g. ['Animal', 'Feline', 'Cat']. |
| """ |
| return [c._class_name() for c in cls._get_hierarchy()] |
| |
| @classmethod |
| def _get_kind(cls): |
| """Override. |
| |
| Make sure that the kind returned is the root class of the |
| polymorphic hierarchy. |
| """ |
| bases = cls._get_hierarchy() |
| if not bases: |
| # We have to jump through some hoops to call the superclass' |
| # _get_kind() method. First, this is called by the metaclass |
| # before the PolyModel name is defined, so it can't use |
| # super(PolyModel, cls)._get_kind(). Second, we can't just call |
| # Model._get_kind() because that always returns 'Model'. Hence |
| # the 'im_func' hack. |
| return model.Model._get_kind.im_func(cls) |
| else: |
| return bases[0]._class_name() |
| |
| @classmethod |
| def _class_name(cls): |
| """Return the class name. |
| |
| This overrides Model._class_name() which is an alias for _get_kind(). |
| |
| This is overridable in case you want to use a different class |
| name. The main use case is probably to maintain backwards |
| compatibility with datastore contents after renaming a class. |
| |
| NOTE: When overriding this for an intermediate class in your |
| hierarchy (as opposed to a leaf class), make sure to test |
| cls.__name__, or else all subclasses will appear to have the |
| same class name. |
| """ |
| return cls.__name__ |
| |
| @classmethod |
| def _get_hierarchy(cls): |
| """Internal helper to return the list of polymorphic base classes. |
| |
| This returns a list of class objects, e.g. [Animal, Feline, Cat]. |
| """ |
| bases = [] |
| for base in cls.mro(): # pragma: no branch |
| if hasattr(base, '_get_hierarchy'): |
| bases.append(base) |
| del bases[-1] # Delete PolyModel itself |
| bases.reverse() |
| return bases |
| |
| @classmethod |
| def _default_filters(cls): |
| if len(cls._get_hierarchy()) <= 1: |
| return () |
| return (cls.class_ == cls._class_name(),) |