[server] improve the speed of setting relations between entities (closes #1625257)
The main idea is to add methods equivalent to session.add_relation and
repository.glob_add_relation which handle several relations in one call. Speed
gain results from:
* using cursor.executemany to run SQL statements
* factorizing some code which otherwise has to be performed for each relation,
such as context manager creation before calling hooks or to enable security,
creation of the EditedEntity instance (when several inlined relations are set
on a single entity, and consequently when refreshing the cached entity)
benchmark runs 1.1x faster for non inlined entities and 125x faster for inlined
entities :-)
# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""
A RelationTag object is an object which allows to link a configuration
information to a relation definition. For instance, the standard
primary view uses a RelationTag object (uicfg.primaryview_section) to
get the section to display relations.
.. sourcecode:: python
# display ``entry_of`` relations in the ``relations`` section in the ``BlogEntry`` primary view
uicfg.primaryview_section.tag_subject_of(('BlogEntry', 'entry_of', '*'),
'relations')
# hide every relation ``entry_of`` in the ``Blog`` primary view
uicfg.primaryview_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
Three primitives are defined:
* ``tag_subject_of`` tag a relation in the subject's context
* ``tag_object_of`` tag a relation in the object's context
* ``tag_attribute`` shortcut for tag_subject_of
"""
__docformat__ = "restructuredtext en"
import logging
from logilab.common.logging_ext import set_log_methods
RTAGS = []
def register_rtag(rtag):
RTAGS.append(rtag)
def _ensure_str_key(key):
return tuple(str(k) for k in key)
class RelationTags(object):
"""a tag store for full relation definitions :
(subject type, relation type, object type, tagged)
allowing to set tags using wildcard (eg '*') as subject type / object type
This class associates a single tag to each key.
"""
_allowed_values = None
_initfunc = None
def __init__(self, name=None, initfunc=None, allowed_values=None):
self._name = name or '<unknown>'
self._tagdefs = {}
if allowed_values is not None:
self._allowed_values = allowed_values
if initfunc is not None:
self._initfunc = initfunc
register_rtag(self)
def __repr__(self):
return '%s: %s' % (self._name, repr(self._tagdefs))
# dict compat
def __getitem__(self, key):
return self.get(*key)
__contains__ = __getitem__
def clear(self):
self._tagdefs.clear()
def _get_keys(self, stype, rtype, otype, tagged):
keys = []
if '*' not in (stype, otype):
keys.append(('*', rtype, '*', tagged))
if '*' != stype:
keys.append(('*', rtype, otype, tagged))
if '*' != otype:
keys.append((stype, rtype, '*', tagged))
keys.append((stype, rtype, otype, tagged))
return keys
def init(self, schema, check=True):
# XXX check existing keys against schema
if check:
for (stype, rtype, otype, tagged), value in self._tagdefs.items():
for ertype in (stype, rtype, otype):
if ertype != '*' and not ertype in schema:
self.warning('removing rtag %s: %s, %s undefined in schema',
(stype, rtype, otype, tagged), value, ertype)
self.del_rtag(stype, rtype, otype, tagged)
break
if self._initfunc is not None:
self.apply(schema, self._initfunc)
def apply(self, schema, func):
for eschema in schema.entities():
if eschema.final:
continue
for rschema, tschemas, role in eschema.relation_definitions(True):
for tschema in tschemas:
if role == 'subject':
sschema, oschema = eschema, tschema
else:
sschema, oschema = tschema, eschema
func(self, sschema, rschema, oschema, role)
# rtag declaration api ####################################################
def tag_attribute(self, key, *args, **kwargs):
key = list(key)
key.append('*')
key.append('subject')
self.tag_relation(key, *args, **kwargs)
def tag_subject_of(self, key, *args, **kwargs):
key = list(key)
key.append('subject')
self.tag_relation(key, *args, **kwargs)
def tag_object_of(self, key, *args, **kwargs):
key = list(key)
key.append('object')
self.tag_relation(key, *args, **kwargs)
def tag_relation(self, key, tag):
assert len(key) == 4, 'bad key: %s' % list(key)
if self._allowed_values is not None:
assert tag in self._allowed_values, \
'%r is not an allowed tag (should be in %s)' % (
tag, self._allowed_values)
self._tagdefs[_ensure_str_key(key)] = tag
return tag
# rtag runtime api ########################################################
def del_rtag(self, *key):
del self._tagdefs[key]
def get(self, *key):
for key in reversed(self._get_keys(*key)):
try:
return self._tagdefs[key]
except KeyError:
continue
return None
def etype_get(self, etype, rtype, role, ttype='*'):
if role == 'subject':
return self.get(etype, rtype, ttype, role)
return self.get(ttype, rtype, etype, role)
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
class RelationTagsSet(RelationTags):
"""This class associates a set of tags to each key.
"""
tag_container_cls = set
def tag_relation(self, key, tag):
rtags = self._tagdefs.setdefault(_ensure_str_key(key),
self.tag_container_cls())
rtags.add(tag)
return rtags
def get(self, stype, rtype, otype, tagged):
rtags = self.tag_container_cls()
for key in self._get_keys(stype, rtype, otype, tagged):
try:
rtags.update(self._tagdefs[key])
except KeyError:
continue
return rtags
class RelationTagsDict(RelationTagsSet):
"""This class associates a set of tags to each key."""
tag_container_cls = dict
def tag_relation(self, key, tag):
key = _ensure_str_key(key)
try:
rtags = self._tagdefs[key]
rtags.update(tag)
return rtags
except KeyError:
self._tagdefs[key] = tag
return tag
def setdefault(self, key, tagkey, tagvalue):
key = _ensure_str_key(key)
try:
rtags = self._tagdefs[key]
rtags.setdefault(tagkey, tagvalue)
return rtags
except KeyError:
self._tagdefs[key] = {tagkey: tagvalue}
return self._tagdefs[key]
class RelationTagsBool(RelationTags):
_allowed_values = frozenset((True, False))
class NoTargetRelationTagsDict(RelationTagsDict):
@property
def name(self):
return self.__class__.name
# tag_subject_of / tag_object_of issue warning if '*' is not given as target
# type, while tag_relation handle it silently since it may be used during
# initialization
def tag_subject_of(self, key, tag):
subj, rtype, obj = key
if obj != '*':
self.warning('using explict target type in %s.tag_subject_of() '
'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
self.name, subj, rtype, subj, rtype, obj)
super(NoTargetRelationTagsDict, self).tag_subject_of((subj, rtype, '*'), tag)
def tag_object_of(self, key, tag):
subj, rtype, obj = key
if subj != '*':
self.warning('using explict subject type in %s.tag_object_of() '
'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
self.name, rtype, obj, subj, rtype, obj)
super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag)
def tag_relation(self, key, tag):
if key[-1] == 'subject' and key[-2] != '*':
if isinstance(key, tuple):
key = list(key)
key[-2] = '*'
elif key[-1] == 'object' and key[0] != '*':
if isinstance(key, tuple):
key = list(key)
key[0] = '*'
super(NoTargetRelationTagsDict, self).tag_relation(key, tag)
set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags'))