diff --git a/cassandra/cqlengine/columns.py b/cassandra/cqlengine/columns.py index 3be8bfef..8548355b 100644 --- a/cassandra/cqlengine/columns.py +++ b/cassandra/cqlengine/columns.py @@ -13,16 +13,19 @@ # limitations under the License. from copy import deepcopy, copy -from datetime import datetime -from datetime import date +from datetime import date, datetime +import logging import re import six import sys +import warnings from cassandra.cqltypes import DateType from cassandra.encoder import cql_quote from cassandra.cqlengine.exceptions import ValidationError +log = logging.getLogger(__name__) + # move to central spot class UnicodeMixin(object): @@ -147,8 +150,20 @@ class Column(object): polymorphic_key = False """ - boolean, if set to True, this column will be used for saving and loading instances - of polymorphic tables + *Deprecated* + + see :attr:`~.discriminator_column` + """ + + discriminator_column = False + """ + boolean, if set to True, this column will be used for discriminating records + of inherited models. + + Should only be set on a column of an abstract model being used for inheritance. + + There may only be one discriminator column per model. See :attr:`~.__discriminator_value__` + for how to specify the value of this column on specialized models. """ static = False @@ -165,6 +180,7 @@ class Column(object): required=False, clustering_order=None, polymorphic_key=False, + discriminator_column=False, static=False): self.partition_key = partition_key self.primary_key = partition_key or primary_key @@ -173,7 +189,15 @@ class Column(object): self.default = default self.required = required self.clustering_order = clustering_order - self.polymorphic_key = polymorphic_key + + if polymorphic_key: + msg = "polymorphic_key is deprecated. Use discriminator_column instead." + warnings.warn(msg, DeprecationWarning) + log.warn(msg) + + self.discriminator_column = discriminator_column or polymorphic_key + self.polymorphic_key = self.discriminator_column + # the column name in the model definition self.column_name = None self.static = static diff --git a/cassandra/cqlengine/models.py b/cassandra/cqlengine/models.py index 980dffc5..f04abfa9 100644 --- a/cassandra/cqlengine/models.py +++ b/cassandra/cqlengine/models.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import re import six +import warnings from cassandra.cqlengine import columns from cassandra.cqlengine.exceptions import ModelException, CQLEngineException, ValidationError @@ -22,6 +24,8 @@ from cassandra.cqlengine.query import DoesNotExist as _DoesNotExist from cassandra.cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned from cassandra.util import OrderedDict +log = logging.getLogger(__name__) + class ModelDefinitionException(ModelException): pass @@ -71,14 +75,14 @@ class QuerySetDescriptor(object): raise CQLEngineException('cannot execute queries against abstract models') queryset = model.__queryset__(model) - # if this is a concrete polymorphic model, and the polymorphic + # if this is a concrete polymorphic model, and the discriminator # key is an indexed column, add a filter clause to only return # logical rows of the proper type if model._is_polymorphic and not model._is_polymorphic_base: - name, column = model._polymorphic_column_name, model._polymorphic_column + name, column = model._discriminator_column_name, model._discriminator_column if column.partition_key or column.index: # look for existing poly types - return queryset.filter(**{name: model.__polymorphic_key__}) + return queryset.filter(**{name: model.__discriminator_value__}) return queryset @@ -302,7 +306,8 @@ class BaseModel(object): __default_ttl__ = None - __polymorphic_key__ = None + __polymorphic_key__ = None # DEPRECATED + __discriminator_value__ = None # compaction options __compaction__ = None @@ -378,17 +383,17 @@ class BaseModel(object): raise ModelException('_discover_polymorphic_submodels can only be called on polymorphic base classes') def _discover(klass): - if not klass._is_polymorphic_base and klass.__polymorphic_key__ is not None: - cls._polymorphic_map[klass.__polymorphic_key__] = klass + if not klass._is_polymorphic_base and klass.__discriminator_value__ is not None: + cls._discriminator_map[klass.__discriminator_value__] = klass for subklass in klass.__subclasses__(): _discover(subklass) _discover(cls) @classmethod - def _get_model_by_polymorphic_key(cls, key): + def _get_model_by_discriminator_value(cls, key): if not cls._is_polymorphic_base: - raise ModelException('_get_model_by_polymorphic_key can only be called on polymorphic base classes') - return cls._polymorphic_map.get(key) + raise ModelException('_get_model_by_discriminator_value can only be called on polymorphic base classes') + return cls._discriminator_map.get(key) @classmethod def _construct_instance(cls, values): @@ -403,20 +408,20 @@ class BaseModel(object): field_dict = dict([(cls._db_map.get(k, k), v) for k, v in items]) if cls._is_polymorphic: - poly_key = field_dict.get(cls._polymorphic_column_name) + disc_key = field_dict.get(cls._discriminator_column_name) - if poly_key is None: - raise PolyMorphicModelException('polymorphic key was not found in values') + if disc_key is None: + raise PolyMorphicModelException('discriminator value was not found in values') poly_base = cls if cls._is_polymorphic_base else cls._polymorphic_base - klass = poly_base._get_model_by_polymorphic_key(poly_key) + klass = poly_base._get_model_by_discriminator_value(disc_key) if klass is None: poly_base._discover_polymorphic_submodels() - klass = poly_base._get_model_by_polymorphic_key(poly_key) + klass = poly_base._get_model_by_discriminator_value(disc_key) if klass is None: raise PolyMorphicModelException( - 'unrecognized polymorphic key {} for class {}'.format(poly_key, poly_base.__name__) + 'unrecognized discriminator column {} for class {}'.format(disc_key, poly_base.__name__) ) if not issubclass(klass, cls): @@ -640,7 +645,7 @@ class BaseModel(object): if self._is_polymorphic_base: raise PolyMorphicModelException('cannot save polymorphic base model') else: - setattr(self, self._polymorphic_column_name, self.__polymorphic_key__) + setattr(self, self._discriminator_column_name, self.__discriminator_value__) self.validate() self.__dmlquery__(self.__class__, self, @@ -690,7 +695,7 @@ class BaseModel(object): if self._is_polymorphic_base: raise PolyMorphicModelException('cannot update polymorphic base model') else: - setattr(self, self._polymorphic_column_name, self.__polymorphic_key__) + setattr(self, self._discriminator_column_name, self.__disciminator_value__) self.validate() self.__dmlquery__(self.__class__, self, @@ -757,8 +762,15 @@ class ModelMetaClass(type): # short circuit __abstract__ inheritance is_abstract = attrs['__abstract__'] = attrs.get('__abstract__', False) - # short circuit __polymorphic_key__ inheritance - attrs['__polymorphic_key__'] = attrs.get('__polymorphic_key__', None) + # short circuit __discriminator_value__ inheritance + # __polymorphic_key__ is deprecated + poly_key = attrs.get('__polymorphic_key__', None) + if poly_key: + msg = '__polymorphic_key__ is deprecated. Use __discriminator_value__ instead' + warnings.warn(msg, DeprecationWarning) + log.warn(msg) + attrs['__discriminator_value__'] = attrs.get('__discriminator_value__', poly_key) + attrs['__polymorphic_key__'] = attrs['__discriminator_value__'] def _transform_column(col_name, col_obj): column_dict[col_name] = col_obj @@ -771,18 +783,18 @@ class ModelMetaClass(type): column_definitions = [(k, v) for k, v in attrs.items() if isinstance(v, columns.Column)] column_definitions = sorted(column_definitions, key=lambda x: x[1].position) - is_polymorphic_base = any([c[1].polymorphic_key for c in column_definitions]) + is_polymorphic_base = any([c[1].discriminator_column for c in column_definitions]) column_definitions = [x for x in inherited_columns.items()] + column_definitions - polymorphic_columns = [c for c in column_definitions if c[1].polymorphic_key] - is_polymorphic = len(polymorphic_columns) > 0 - if len(polymorphic_columns) > 1: - raise ModelDefinitionException('only one polymorphic_key can be defined in a model, {} found'.format(len(polymorphic_columns))) + discriminator_columns = [c for c in column_definitions if c[1].discriminator_column] + is_polymorphic = len(discriminator_columns) > 0 + if len(discriminator_columns) > 1: + raise ModelDefinitionException('only one discriminator_column (polymorphic_key (deprecated)) can be defined in a model, {} found'.format(len(discriminator_columns))) - polymorphic_column_name, polymorphic_column = polymorphic_columns[0] if polymorphic_columns else (None, None) + discriminator_column_name, discriminator_column = discriminator_columns[0] if discriminator_columns else (None, None) - if isinstance(polymorphic_column, (columns.BaseContainerColumn, columns.Counter)): - raise ModelDefinitionException('counter and container columns cannot be used for polymorphic keys') + if isinstance(discriminator_column, (columns.BaseContainerColumn, columns.Counter)): + raise ModelDefinitionException('counter and container columns cannot be used as discriminator columns (polymorphic_key (deprecated)) ') # find polymorphic base class polymorphic_base = None @@ -877,9 +889,9 @@ class ModelMetaClass(type): attrs['_is_polymorphic_base'] = is_polymorphic_base attrs['_is_polymorphic'] = is_polymorphic attrs['_polymorphic_base'] = polymorphic_base - attrs['_polymorphic_column'] = polymorphic_column - attrs['_polymorphic_column_name'] = polymorphic_column_name - attrs['_polymorphic_map'] = {} if is_polymorphic_base else None + attrs['_discriminator_column'] = discriminator_column + attrs['_discriminator_column_name'] = discriminator_column_name + attrs['_discriminator_map'] = {} if is_polymorphic_base else None # setup class exceptions DoesNotExistBase = None @@ -933,5 +945,12 @@ class Model(BaseModel): __polymorphic_key__ = None """ - *Optional* Specifies a value for the polymorphic key when using model inheritance. + *Deprecated.* + + see :attr:`~.__discriminator_value__` + """ + + __discriminator_value__ = None + """ + *Optional* Specifies a value for the discriminator column when using model inheritance. """ diff --git a/docs/api/cassandra/cqlengine/columns.rst b/docs/api/cassandra/cqlengine/columns.rst index 23a6157b..36256bd3 100644 --- a/docs/api/cassandra/cqlengine/columns.rst +++ b/docs/api/cassandra/cqlengine/columns.rst @@ -31,6 +31,8 @@ Columns .. autoattribute:: polymorphic_key + .. autoattribute:: discriminator_column + .. autoattribute:: static Column Types diff --git a/docs/api/cassandra/cqlengine/models.rst b/docs/api/cassandra/cqlengine/models.rst index 6dd255f9..f29dacbf 100644 --- a/docs/api/cassandra/cqlengine/models.rst +++ b/docs/api/cassandra/cqlengine/models.rst @@ -35,7 +35,9 @@ Model .. autoattribute:: __polymorphic_key__ - See :ref:`table_polymorphism` for usage examples. + .. autoattribute:: __discriminator_value__ + + See :ref:`model_inheritance` for usage examples. *Each table can have its own set of configuration options. These can be specified on a model with the following attributes:* diff --git a/docs/cqlengine/models.rst b/docs/cqlengine/models.rst index 8ee30c36..aef77f50 100644 --- a/docs/cqlengine/models.rst +++ b/docs/cqlengine/models.rst @@ -122,10 +122,10 @@ extend the model's validation method: *Note*: while not required, the convention is to raise a ``ValidationError`` (``from cqlengine import ValidationError``) if validation fails. -.. _table_polymorphism: +.. _model_inheritance: -Table Polymorphism -================== +Model Inheritance +================= It is possible to save and load different model classes using a single CQL table. This is useful in situations where you have different object types that you want to store in a single cassandra row. @@ -137,7 +137,7 @@ For instance, suppose you want a table that stores rows of pets owned by an owne __table_name__ = 'pet' owner_id = UUID(primary_key=True) pet_id = UUID(primary_key=True) - pet_type = Text(polymorphic_key=True) + pet_type = Text(discriminator_column=True) name = Text() def eat(self, food): @@ -147,14 +147,14 @@ For instance, suppose you want a table that stores rows of pets owned by an owne pass class Cat(Pet): - __polymorphic_key__ = 'cat' + __discriminator_value__ = 'cat' cuteness = Float() def tear_up_couch(self): pass class Dog(Pet): - __polymorphic_key__ = 'dog' + __discriminator_value__ = 'dog' fierceness = Float() def bark_all_night(self): @@ -164,19 +164,17 @@ After calling ``sync_table`` on each of these tables, the columns defined in eac ``pet`` table. Additionally, saving ``Cat`` and ``Dog`` models will save the meta data needed to identify each row as either a cat or dog. -To setup a polymorphic model structure, follow these steps +To setup a model structure with inheritance, follow these steps -1. Create a base model with a column set as the polymorphic_key (set ``polymorphic_key=True`` in the column definition) -2. Create subclass models, and define a unique ``__polymorphic_key__`` value on each +1. Create a base model with a column set as the distriminator (``distriminator_column=True`` in the column definition) +2. Create subclass models, and define a unique ``__discriminator_value__`` value on each 3. Run ``sync_table`` on each of the sub tables -**About the polymorphic key** +**About the discriminator value** -The polymorphic key is what cqlengine uses under the covers to map logical cql rows to the appropriate model type. The -base model maintains a map of polymorphic keys to subclasses. When a polymorphic model is saved, this value is automatically -saved into the polymorphic key column. You can set the polymorphic key column to any column type that you like, with -the exception of container and counter columns, although ``Integer`` columns make the most sense. Additionally, if you -set ``index=True`` on your polymorphic key column, you can execute queries against polymorphic subclasses, and a +The discriminator value is what cqlengine uses under the covers to map logical cql rows to the appropriate model type. The +base model maintains a map of discriminator values to subclasses. When a specialized model is saved, its discriminator value is +automatically saved into the discriminator column. The discriminator column may be any column type except counter and container types. +Additionally, if you set ``index=True`` on your discriminator column, you can execute queries against specialized subclasses, and a ``WHERE`` clause will be automatically added to your query, returning only rows of that type. Note that you must -define a unique ``__polymorphic_key__`` value to each subclass, and that you can only assign a single polymorphic -key column per model +define a unique ``__discriminator_value__`` to each subclass, and that you can only assign a single discriminator column per model. diff --git a/tests/integration/cqlengine/model/test_polymorphism.py b/tests/integration/cqlengine/model/test_polymorphism.py index f272e42f..f40f008e 100644 --- a/tests/integration/cqlengine/model/test_polymorphism.py +++ b/tests/integration/cqlengine/model/test_polymorphism.py @@ -64,11 +64,11 @@ class TestPolymorphicClassConstruction(BaseCassEngTestCase): assert Base._is_polymorphic_base assert not M1._is_polymorphic_base - assert Base._polymorphic_column is Base._columns['type1'] - assert M1._polymorphic_column is M1._columns['type1'] + assert Base._discriminator_column is Base._columns['type1'] + assert M1._discriminator_column is M1._columns['type1'] - assert Base._polymorphic_column_name == 'type1' - assert M1._polymorphic_column_name == 'type1' + assert Base._discriminator_column_name == 'type1' + assert M1._discriminator_column_name == 'type1' def test_table_names_are_inherited_from_poly_base(self): class Base(models.Model):