def _cascade_operation(self, reference, cascading_type): entity = reference if isinstance(reference, ProxyObject): entity = reference._actual if not EntityMetadataHelper.hasMetadata(entity): return entity_meta = EntityMetadataHelper.extract(entity) relational_map = entity_meta.relational_map for property_name in relational_map: guide = relational_map[property_name] if guide.inverted_by: continue actual_data = entity.__getattribute__(property_name) reference = self.hydrate_entity(actual_data) if not guide.cascading_options or cascading_type not in guide.cascading_options or not reference: continue if isinstance(reference, list): for sub_reference in actual_data: self._forward_operation(self.hydrate_entity(sub_reference), cascading_type, guide.target_class) continue self._forward_operation(reference, cascading_type, guide.target_class)
def repository(self, reference): """ Retrieve the collection :param reference: the entity class or entity metadata of the target repository / collection :rtype: tori.db.repository.Repository """ key = None if isinstance(reference, EntityMetadata): key = reference.collection_name elif EntityMetadataHelper.hasMetadata(reference): is_registerable_reference = True metadata = EntityMetadataHelper.extract(reference) key = metadata.collection_name self.register_class(reference) if not key: raise UnsupportedRepositoryReferenceError('Either a class with metadata or an entity metadata is supported.') if key not in self._repository_map: repository = Repository( session = self, representing_class = reference ) repository.setup_index() self._repository_map[key] = repository return self._repository_map[key]
def collection_name(self): """ Auto-generated Collection Name :rtype: str .. note:: This is a read-only property. """ if not self.__collection_name: self.__collection_name = self.collection_name_tmpl.format( origin = EntityMetadataHelper.extract(self.origin).collection_name, destination = EntityMetadataHelper.extract(self.destination).collection_name ) return self.__collection_name
def _load_extra_associations(self, record, change_set): origin_id = record.entity.id relational_map = EntityMetadataHelper.extract(record.entity).relational_map for property_name in relational_map: if property_name not in change_set: continue property_change_set = change_set[property_name] guide = relational_map[property_name] repository = self._em.collection(guide.association_class.cls) if property_change_set["action"] == "update": for unlinked_destination_id in property_change_set["deleted"]: association = repository.filter_one({"origin": origin_id, "destination": unlinked_destination_id}) if not association: continue self._register_deleted(association) for new_destination_id in property_change_set["new"]: association = repository.new(origin=origin_id, destination=new_destination_id) self._register_new(association) return elif property_change_set["action"] == "purge": for association in repository.filter({"origin": origin_id}): self._register_deleted(association) return raise RuntimeError("Unknown changes on external associations for {}".format(origin_id))
def _sub_query(self, query, alias_to_query_map, iteration): is_join_query = True alias = iteration.alias if alias not in alias_to_query_map: return False join_config = query.join_map[alias] joined_type = join_config['class'] joined_meta = EntityMetadataHelper.extract(joined_type) native_query = alias_to_query_map[alias] local_constrains = {} if not iteration.parent_alias: is_root = False constrains = self.driver.dialect.get_iterating_constrains(query) result_list = self.driver.query(joined_meta, native_query, local_constrains) # No result in a sub-query means no result in the main query. if not result_list: return False join_config['result_list'] = result_list alias_to_query_map.update(self.driver.dialect.get_alias_to_native_query_map(query)) return True
def extra_associations(self, data, stack_depth=0): if not isinstance(data, object): raise TypeError('The provided data must be an object') returnee = {} relational_map = EntityMetadataHelper.extract(data).relational_map if self._is_entity(data) else {} extra_associations = {} for name in dir(data): # Skip all protected/private/reserved properties. if self._is_preserved_property(name): continue guide = self._retrieve_guide(relational_map, name) # Skip all properties without an associative guide or with reverse mapping or without pseudo association class. if not guide or guide.inverted_by or not guide.association_class: continue property_reference = data.__getattribute__(name) # Skip all callable properties and non-list properties if callable(property_reference) or not isinstance(property_reference, list): continue # With a valid association class, this property has the many-to-many relationship with the other entity. extra_associations[name] = [] for destination in property_reference: extra_associations[name].append(destination.id) return extra_associations
def __prevent_duplicated_mapping(cls, property_name): if not cls: raise ValueError('Expecting a valid type') metadata = EntityMetadataHelper.extract(cls) if property_name in metadata.relational_map: raise DuplicatedRelationalMapping('The property is already mapped.')
def name(self): """ Collection name :rtype: str """ metadata = EntityMetadataHelper.extract(self._class) return metadata.collection_name
def _convert_object_id_to_str(self, object_id, entity=None, cls=None): class_hash = "generic" if not cls and entity: cls = entity.__class__ if cls: metadata = EntityMetadataHelper.extract(cls) class_hash = metadata.collection_name object_key = "{}/{}".format(class_hash, str(object_id)) return object_key
def _construct_dependency_graph(self): self._dependency_map = {} for uid in self._record_map: record = self._record_map[uid] object_id = self._convert_object_id_to_str(record.entity.id, record.entity) current_set = Record.serializer.encode(record.entity) extra_association = Record.serializer.extra_associations(record.entity) # Register the current entity into the dependency map if it's never # been registered or eventually has no dependencies. if object_id not in self._dependency_map: self._dependency_map[object_id] = DependencyNode(record) relational_map = EntityMetadataHelper.extract(record.entity).relational_map if not relational_map: continue # Go through the relational map to establish relationship between dependency nodes. for property_name in relational_map: guide = relational_map[property_name] # Ignore a property from reverse mapping. if guide.inverted_by: continue # ``data`` can be either an object ID or list. data = current_set[property_name] if not data: # Ignore anything evaluated as False. continue elif not isinstance(data, list): other_uid = self._retrieve_entity_guid_by_id(data, guide.target_class) other_record = self._record_map[other_uid] self._register_dependency(record, other_record) continue for dependency_object_id in data: other_uid = self._retrieve_entity_guid_by_id(dependency_object_id, guide.target_class) other_record = self._record_map[other_uid] self._register_dependency(record, other_record) return self._dependency_map
def encode(self, data, stack_depth=0, convert_object_id_to_str=False): """ Encode data into dictionary and list. :param data: the data to encode :param stack_depth: traversal depth limit :param convert_object_id_to_str: flag to convert object ID into string """ if not isinstance(data, object): raise TypeError('The provided data must be an object') returnee = {} relational_map = EntityMetadataHelper.extract(data).relational_map if self._is_entity(data) else {} for name in dir(data): # Skip all protected/private/reserved properties. if self._is_preserved_property(name): continue guide = self._retrieve_guide(relational_map, name) # Skip all pseudo properties used for reverse mapping. if guide and guide.inverted_by: continue property_reference = data.__getattribute__(name) is_list = isinstance(property_reference, list) value = None # Skip all callable properties if callable(property_reference): continue # For one-to-many relationship, this property relies on the built-in list type. if is_list: value = [] for item in property_reference: value.append(self._process_value(data, item, stack_depth, convert_object_id_to_str)) else: value = self._process_value(data, property_reference, stack_depth, convert_object_id_to_str) returnee[name] = value # If this is not a pseudo object ID, add the reserved key '_id' with the property 'id' . if data.id and not isinstance(data.id, PseudoObjectId): returnee['_id'] = self._process_value(data, data, stack_depth, convert_object_id_to_str) return returnee
def _update_join_map(self, origin_metadata, join_map, origin_alias): link_map = origin_metadata.relational_map iterating_sequence = [] # Compute the (local) iterating sequence for updating the join map. # Note: this is not the query iterating sequence. for alias in join_map: join_config = join_map[alias] if join_config['class']: continue parent_alias, property_path = join_config['path'].split('.', 2) join_config['alias'] = alias join_config['property_path'] = property_path join_config['parent_alias'] = parent_alias join_config['result_list'] = [] iterating_sequence.append((join_config, alias, parent_alias, property_path)) # Update the immediate properties. for join_config, current_alias, parent_alias, property_path in iterating_sequence: if parent_alias != origin_alias: continue if property_path not in link_map: continue mapper = link_map[property_path] join_config['class'] = mapper.target_class join_config['mapper'] = mapper # Update the joined properties. for join_config, current_alias, parent_alias, property_path in iterating_sequence: if current_alias not in join_map: continue if not join_map[current_alias]['class']: continue next_origin_class = join_map[current_alias]['class'] next_metadata = EntityMetadataHelper.extract(next_origin_class) self._update_join_map(next_metadata, join_map, current_alias)
def setup_index(self): """ Set up index for the entity based on the ``entity`` and ``link`` decorators """ metadata = EntityMetadataHelper.extract(self._class) # Apply the relational indexes. for field in metadata.relational_map: guide = metadata.relational_map[field] if guide.inverted_by or guide.association != AssociationType.ONE_TO_ONE: continue self.index(field) # Apply the manual indexes. for index in metadata.index_list: self.index(index)
def has_cascading(self): if self._has_cascading is not None: return self._has_cascading self._has_cascading = False relational_map = EntityMetadataHelper.extract(self._class).relational_map for property_name in relational_map: cascading_options = relational_map[property_name].cascading_options if cascading_options \ and ( CascadingType.DELETE in cascading_options \ or CascadingType.PERSIST in cascading_options ): self._has_cascading = True break return self._has_cascading
def new(self, **attributes): """ Create a new document/entity :param attributes: attribute map :return: object .. note:: This method deal with data mapping. """ spec = inspect.getargspec(self._class.__init__) # constructor contract meta = EntityMetadataHelper.extract(self._class) rmap = meta.relational_map # relational map # Default missing argument to NULL or LIST # todo: respect the default value of the argument for argument_name in spec.args: if argument_name == 'self' or argument_name in attributes: continue default_to_list = argument_name in rmap\ and rmap[argument_name].association in [ AssociationType.ONE_TO_MANY, AssociationType.MANY_TO_MANY ] attributes[argument_name] = [] if default_to_list else None attribute_name_list = list(attributes.keys()) # Remove unwanted arguments/attributes/properties for attribute_name in attribute_name_list: if argument_name == 'self' or attribute_name in spec.args: continue del attributes[attribute_name] return self._class(**attributes)
def register_class(self, entity_class): """ Register the entity class :param entity_class: the class of document/entity :type entity_class: type :rtype: tori.db.repository.Repository .. note:: This is for internal operation only. As it seems to be just a residual from the prototype stage, the follow-up investigation in order to remove the method will be for Tori 3.1. """ key = entity_class if isinstance(entity_class, type): metadata = EntityMetadataHelper.extract(entity_class) key = metadata.collection_name if key not in self._registered_types: self._registered_types[key] = entity_class
def query(self, query): metadata = EntityMetadataHelper.extract(query.origin) # Deprecated in Tori 3.1; Only for backward compatibility if not query.is_new_style: return self.driver.query( metadata, query._condition, self.driver.dialect.get_iterating_constrains(query) ) root_class = query.origin expression_set = query.criteria.get_analyzed_version() # Register the root entity query.join_map[query.alias] = { 'alias': query.alias, 'path': None, 'class': root_class, 'parent_alias': None, 'property_path': None, 'result_list': [] } self._update_join_map(metadata, query.join_map, query.alias) iterating_sequence = self._compute_iterating_sequence(query.join_map) alias_to_query_map = self.driver.dialect.get_alias_to_native_query_map(query) for alias in query.join_map: mapping = query.join_map[alias] for iteration in iterating_sequence: if not self._sub_query(query, alias_to_query_map, iteration): break return query.join_map[query.alias]['result_list']
def prepare_entity_class(cls, collection_name=None, indexes=[]): """ Create a entity class :param cls: the document class :type cls: object :param collection_name: the name of the corresponding collection where the default is the lowercase version of the name of the given class (cls) :type collection_name: str The object decorated with this decorator will be automatically provided with a few additional attributes. =================== ======== =================== ==== ================================= Attribute Access Description Read Write =================== ======== =================== ==== ================================= id Instance Document Identifier Yes Yes, ONLY ``id`` is undefined. __t3_orm_meta__ Static Tori 3's Metadata Yes ONLY the property of the metadata __session__ Instance DB Session Yes Yes, but NOT recommended. =================== ======== =================== ==== ================================= The following attributes might stay around but are deprecated as soon as the stable Tori 3.0 is released. =================== ======== =================== ==== ================================= Attribute Access Description Read Write =================== ======== =================== ==== ================================= __collection_name__ Static Collection Name Yes Yes, but NOT recommended. __relational_map__ Static Relational Map Yes Yes, but NOT recommended. __indexes__ Static Indexing List Yes Yes, but NOT recommended. =================== ======== =================== ==== ================================= ``__session__`` is used to resolve the managing rights in case of using multiple sessions simutaneously. For example, .. code-block:: python @entity class Note(object): def __init__(self, content, title=''): self.content = content self.title = title where the collection name is automatically defined as "note". .. versionchanged:: 3.0 The way Tori stores metadata objects in ``__collection_name__``, ``__relational_map__`` and ``__indexes__`` are now ignored by the ORM in favour of ``__t3_orm_meta__`` which is an entity metadata object. This change is made to allow easier future development. .. tip:: You can define it as "notes" by replacing ``@entity`` with ``@entity('notes')``. """ if not cls: raise ValueError('Expecting a valid type') def get_id(self): return self.__dict__['_id'] if '_id' in self.__dict__ else None def set_id(self, id): """ Define the document ID if the original ID is not defined. :param id: the ID of the document. """ if '_id' in self.__dict__ and self.__dict__['_id']\ and not isinstance(self.__dict__['_id'], PseudoObjectId): raise LockedIdException('The ID is already assigned and cannot be changed.') self._id = id cls.__session__ = None EntityMetadataHelper.imprint( cls, collection_name or cls.__name__.lower(), indexes ) cls.id = property(get_id, set_id) return cls
def _is_entity(self, data): return EntityMetadataHelper.hasMetadata(data)
def __map_property(cls, property_name, guide): metadata = EntityMetadataHelper.extract(cls) metadata.relational_map[property_name] = guide
def apply_relational_map(self, entity): """ Wire connections according to the relational map """ meta = EntityMetadataHelper.extract(entity) rmap = meta.relational_map for property_name in rmap: guide = rmap[property_name] """ :type: tori.db.mapper.RelatingGuide """ # In the reverse mapping, the lazy loading is not possible but so # the proxy object is still used. if guide.inverted_by: target_meta = EntityMetadataHelper.extract(guide.target_class) api = self._driver.collection(target_meta.collection_name) if guide.association in [AssociationType.ONE_TO_ONE, AssociationType.MANY_TO_ONE]: # Replace with Criteria target = api.find_one({guide.inverted_by: entity.id}) entity.__setattr__(property_name, ProxyFactory.make(self, target['_id'], guide)) elif guide.association == AssociationType.ONE_TO_MANY: # Replace with Criteria proxy_list = [ ProxyFactory.make(self, target['_id'], guide) for target in api.find({guide.inverted_by: entity.id}) ] entity.__setattr__(property_name, proxy_list) elif guide.association == AssociationType.MANY_TO_MANY: entity.__setattr__(property_name, ProxyCollection(self, entity, guide)) else: raise IntegrityConstraintError('Unknown type of entity association (reverse mapping)') return # Done the application # In the direct mapping, the lazy loading is applied wherever applicable. if guide.association in [AssociationType.ONE_TO_ONE, AssociationType.MANY_TO_ONE]: if not entity.__getattribute__(property_name): continue entity.__setattr__( property_name, ProxyFactory.make( self, entity.__getattribute__(property_name), guide ) ) elif guide.association == AssociationType.ONE_TO_MANY: proxy_list = [] for object_id in entity.__getattribute__(property_name): if not object_id: continue proxy_list.append(ProxyFactory.make(self, object_id, guide)) entity.__setattr__(property_name, proxy_list) elif guide.association == AssociationType.MANY_TO_MANY: entity.__setattr__(property_name, ProxyCollection(self, entity, guide)) else: raise IntegrityConstraintError('Unknown type of entity association')