thirdparty/google_appengine/google/appengine/ext/db/polymodel.py
changeset 828 f5fd65cc3bf3
child 2864 2e0b0af889be
equal deleted inserted replaced
827:88c186556a80 828:f5fd65cc3bf3
       
     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