Skip to content

Commit

Permalink
Relational fields are now lazily constructed via properties instead o…
Browse files Browse the repository at this point in the history
…f in the constructor (#187)

This results in a significant overhead reduction for Model instantiation with many relationships.

Benchmarks place the overhead reduction from an average of 29% to an average of 5% overhead.
or
15~140% speedup for all tests part of Test 2 (Small Relational model)
  • Loading branch information
grigi authored Sep 11, 2019
1 parent 70b0ed7 commit fff091a
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 28 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Changelog
0.13.5
------
- Sample Starlette integration
- Relational fields are now lazily constructed via properties instead of in the constructor,
this results in a significant overhead reduction for Model instantiation with many relationships.

0.13.4
------
Expand All @@ -27,7 +29,7 @@ Changelog

0.13.2
------
* Security fixes for ``«model».save()`` & ``«model».dete()``:
* Security fixes for ``«model».save()`` & ``«model».delete()``:

This is now fully parametrized, and these operations are no longer susceptible to escaping issues.

Expand Down
Binary file modified docs/ORM_Perf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 3 additions & 4 deletions tortoise/backends/base/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@ async def _prefetch_reverse_relation(
self._field_to_db(instance._meta.pk, instance.pk, instance)
for instance in instance_list
} # type: Set[Any]
backward_relation_manager = getattr(self.model, field)
relation_field = backward_relation_manager.relation_field
relation_field = self.model._meta.fields_map[field].relation_field

related_object_list = await related_query.filter(
**{"{}__in".format(relation_field): list(instance_id_set)}
Expand Down Expand Up @@ -301,9 +300,9 @@ def _make_prefetch_queries(self) -> None:
self._prefetch_queries[field] = related_query

async def _do_prefetch(self, instance_id_list: list, field: str, related_query) -> list:
if isinstance(getattr(self.model, field), fields.BackwardFKRelation):
if field in self.model._meta.backward_fk_fields:
return await self._prefetch_reverse_relation(instance_id_list, field, related_query)
if isinstance(getattr(self.model, field), fields.ManyToManyField):
if field in self.model._meta.m2m_fields:
return await self._prefetch_m2m_relation(instance_id_list, field, related_query)
return await self._prefetch_direct_relation(instance_id_list, field, related_query)

Expand Down
65 changes: 42 additions & 23 deletions tortoise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ def _fk_getter(self, _key):
return getattr(self, _key, None)


def _rfk_getter(self, _key, ftype, frelfield):
val = getattr(self, _key, None)
if val is None:
val = RelationQueryContainer(ftype, frelfield, self)
setattr(self, _key, val)
return val


def _m2m_getter(self, _key, field_object):
val = getattr(self, _key, None)
if val is None:
val = ManyToManyRelationManager(field_object.type, self, field_object)
setattr(self, _key, val)
return val


class MetaInfo:
__slots__ = (
"abstract",
Expand Down Expand Up @@ -168,6 +184,32 @@ def finalise_fields(self) -> None:
),
)

# Create lazy reverse FK fields on model.
for key in self.backward_fk_fields:
_key = "_{}".format(key)
field_object = self.fields_map[key] # type: fields.BackwardFKRelation # type: ignore
setattr(
self._model,
key,
property(
partial(
_rfk_getter,
_key=_key,
ftype=field_object.type,
frelfield=field_object.relation_field,
)
),
)

# Create lazy M2M fields on model.
for key in self.m2m_fields:
_key = "_{}".format(key)
setattr(
self._model,
key,
property(partial(_m2m_getter, _key=_key, field_object=self.fields_map[key])),
)

def _generate_filters(self) -> None:
get_overridden_filter_func = self.db.executor_class.get_overridden_filter_func
for key, filter_info in self._filters.items():
Expand Down Expand Up @@ -311,7 +353,6 @@ def __init__(self, *args, **kwargs) -> None:
# self._meta is a very common attribute lookup, lets cache it.
meta = self._meta
self._saved_in_db = meta.pk_attr in kwargs and meta.pk.generated
self._init_lazy_fkm2m()

# Assign values and do type conversions
passed_fields = {*kwargs.keys()}
Expand All @@ -330,7 +371,6 @@ def __init__(self, *args, **kwargs) -> None:
def _init_from_db(cls, **kwargs) -> MODEL_TYPE:
self = cls.__new__(cls)
self._saved_in_db = True
self._init_lazy_fkm2m()

meta = self._meta

Expand All @@ -341,27 +381,6 @@ def _init_from_db(cls, **kwargs) -> MODEL_TYPE:

return self

def _init_lazy_fkm2m(self) -> None:
meta = self._meta
# Create lazy fk/m2m objects
for key in meta.backward_fk_fields:
field_object = meta.fields_map[key]
setattr(
self,
key,
RelationQueryContainer(
field_object.type, field_object.relation_field, self # type: ignore
),
)

for key in meta.m2m_fields:
field_object = meta.fields_map[key]
setattr(
self,
key,
ManyToManyRelationManager(field_object.type, self, field_object), # type: ignore
)

def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]:
"""
Sets values for fields honoring type transformations and
Expand Down

0 comments on commit fff091a

Please sign in to comment.