Skip to content

Commit

Permalink
Efficiency improvements in model & query construction for 5-30% speed…
Browse files Browse the repository at this point in the history
…up for fetch operations (#158)

* Split model consructor into from-Python and from-DB paths, leading to 15-25% speedup for large fetch operations.
* More efficient queryset manipulation, 5-30% speedup for small fetches.
  • Loading branch information
grigi authored Jul 21, 2019
1 parent 0397a4c commit 5f6bd8c
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 132 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ Changelog

0.12.6
------
* Handle a __models__ variable within modules to override the model discovery mechanism.
* Handle a ``__models__`` variable within modules to override the model discovery mechanism.

If you define the ``__models__`` variable in ``yourapp.models`` (or wherever you specify to load your models from),
``generate_schema()`` will use that list, rather than automatically finding all models for you.

* Split model consructor into from-Python and from-DB paths, leading to 15-25% speedup for large fetch operations.
* More efficient queryset manipulation, 5-30% speedup for small fetches.

0.12.5
------
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.
5 changes: 3 additions & 2 deletions tortoise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ def _discover_models(cls, models_path: str, app_label: str) -> List[Type[Model]]
if attr._meta.app and attr._meta.app != app_label:
continue
attr._meta.app = app_label
attr._meta.finalise_pk()
discovered_models.append(attr)
return discovered_models

Expand Down Expand Up @@ -275,7 +276,7 @@ def _get_config_from_config_file(cls, config_file: str) -> dict:
def _build_initial_querysets(cls) -> None:
for app in cls.apps.values():
for model in app.values():
model._meta.generate_filters()
model._meta.finalise_model()
model._meta.basequery = model._meta.db.query_class.from_(model._meta.table)
model._meta.basequery_all_fields = model._meta.basequery.select(
*model._meta.db_fields
Expand Down Expand Up @@ -465,4 +466,4 @@ async def do_stuff():
loop.run_until_complete(Tortoise.close_connections())


__version__ = "0.12.5"
__version__ = "0.12.6"
4 changes: 3 additions & 1 deletion tortoise/backends/base/executor.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from copy import copy
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type # noqa

Expand Down Expand Up @@ -50,7 +51,7 @@ async def execute_select(self, query, custom_fields: Optional[list] = None) -> l
raw_results = await self.db.execute_query(query.get_sql())
instance_list = []
for row in raw_results:
instance = self.model(_from_db=True, **row)
instance = self.model._init_from_db(**row)
if custom_fields:
for field in custom_fields:
setattr(instance, field, row[field])
Expand Down Expand Up @@ -248,6 +249,7 @@ def _make_prefetch_queries(self) -> None:
related_model_field = self.model._meta.fields_map.get(field)
related_model = related_model_field.type
related_query = related_model.all().using_db(self.db)
related_query.query = copy(related_query.model._meta.basequery)
if forwarded_prefetches:
related_query = related_query.prefetch_related(*forwarded_prefetches)
self._prefetch_queries[field] = related_query
Expand Down
162 changes: 79 additions & 83 deletions tortoise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ class MetaInfo:
"abstract",
"table",
"app",
"_fields",
"_db_fields",
"fields",
"db_fields",
"m2m_fields",
"fk_fields",
"backward_fk_fields",
"_fetch_fields",
"fetch_fields",
"fields_db_projection",
"_inited",
"_fields_db_projection_reverse",
"fields_db_projection_reverse",
"filters",
"fields_map",
"default_connection",
Expand All @@ -53,24 +53,26 @@ class MetaInfo:
"_filters",
"unique_together",
"pk_attr",
"_generated_db_fields",
"generated_db_fields",
"_model",
"table_description",
"pk",
"db_pk_field",
)

def __init__(self, meta) -> None:
self.abstract = getattr(meta, "abstract", False) # type: bool
self.table = getattr(meta, "table", "") # type: str
self.app = getattr(meta, "app", None) # type: Optional[str]
self.unique_together = get_unique_together(meta) # type: Optional[Union[Tuple, List]]
self._fields = None # type: Optional[Set[str]]
self._db_fields = None # type: Optional[Set[str]]
self.unique_together = get_unique_together(meta) # type: Union[Tuple, List]
self.fields = set() # type: Set[str]
self.db_fields = set() # type: Set[str]
self.m2m_fields = set() # type: Set[str]
self.fk_fields = set() # type: Set[str]
self.backward_fk_fields = set() # type: Set[str]
self._fetch_fields = None # type: Optional[Set[str]]
self.fetch_fields = set() # type: Set[str]
self.fields_db_projection = {} # type: Dict[str,str]
self._fields_db_projection_reverse = None # type: Optional[Dict[str,str]]
self.fields_db_projection_reverse = {} # type: Dict[str,str]
self._filters = {} # type: Dict[str, Dict[str, dict]]
self.filters = {} # type: Dict[str, dict]
self.fields_map = {} # type: Dict[str, fields.Field]
Expand All @@ -79,86 +81,32 @@ def __init__(self, meta) -> None:
self.basequery = Query() # type: Query
self.basequery_all_fields = Query() # type: Query
self.pk_attr = getattr(meta, "pk_attr", "") # type: str
self._generated_db_fields = None # type: Optional[Tuple[str]]
self.generated_db_fields = None # type: Tuple[str] # type: ignore
self._model = None # type: "Model" # type: ignore
self.table_description = getattr(meta, "table_description", "") # type: str
self.pk = None # type: fields.Field # type: ignore
self.db_pk_field = "" # type: str

def add_field(self, name: str, value: Field):
if name in self.fields_map:
raise ConfigurationError("Field {} already present in meta".format(name))
setattr(self._model, name, value)
value.model = self._model
self.fields_map[name] = value
self._fields = None

if value.has_db_field:
self.fields_db_projection[name] = value.source_field or name
self._fields_db_projection_reverse = None

if isinstance(value, fields.ManyToManyField):
self.m2m_fields.add(name)
self._fetch_fields = None
elif isinstance(value, fields.BackwardFKRelation):
self.backward_fk_fields.add(name)
self._fetch_fields = None

field_filters = get_filters_for_field(
field_name=name, field=value, source_field=value.source_field or name
)
self._filters.update(field_filters)
self.generate_filters()

@property
def fields_db_projection_reverse(self) -> Dict[str, str]:
if self._fields_db_projection_reverse is None:
self._fields_db_projection_reverse = {
value: key for key, value in self.fields_db_projection.items()
}
return self._fields_db_projection_reverse

@property
def fields(self) -> Set[str]:
if self._fields is None:
self._fields = set(self.fields_map.keys())
return self._fields

@property
def db_fields(self) -> Set[str]:
if self._db_fields is None:
self._db_fields = set(self.fields_db_projection.values())
return self._db_fields

@property
def fetch_fields(self):
if self._fetch_fields is None:
self._fetch_fields = self.m2m_fields | self.backward_fk_fields | self.fk_fields
return self._fetch_fields

@property
def pk(self):
return self.fields_map[self.pk_attr]

@property
def db_pk_field(self) -> str:
field_object = self.fields_map[self.pk_attr]
return field_object.source_field or self.pk_attr

@property
def is_pk_generated(self) -> bool:
field_object = self.fields_map[self.pk_attr]
return field_object.generated

@property
def generated_db_fields(self) -> Tuple[str]:
"""Return list of names of db fields that are generated on db side"""
if self._generated_db_fields is None:
generated_fields = []
for field in self.fields_map.values():
if not field.generated:
continue
generated_fields.append(field.source_field or field.model_field_name)
self._generated_db_fields = tuple(generated_fields) # type: ignore
return self._generated_db_fields # type: ignore
self.finalise_fields()

@property
def db(self) -> BaseDBAsyncClient:
Expand All @@ -170,7 +118,33 @@ def db(self) -> BaseDBAsyncClient:
def get_filter(self, key: str) -> dict:
return self.filters[key]

def generate_filters(self) -> None:
def finalise_pk(self) -> None:
self.pk = self.fields_map[self.pk_attr]
self.db_pk_field = self.pk.source_field or self.pk_attr

def finalise_model(self) -> None:
"""
Finalise the model after it had been fully loaded.
"""
self.finalise_fields()
self._generate_filters()

def finalise_fields(self) -> None:
self.db_fields = set(self.fields_db_projection.values())
self.fields = set(self.fields_map.keys())
self.fields_db_projection_reverse = {
value: key for key, value in self.fields_db_projection.items()
}
self.fetch_fields = self.m2m_fields | self.backward_fk_fields | self.fk_fields

generated_fields = []
for field in self.fields_map.values():
if not field.generated:
continue
generated_fields.append(field.source_field or field.model_field_name)
self.generated_db_fields = tuple(generated_fields) # type: ignore

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():
overridden_operator = get_overridden_filter_func( # type: ignore
Expand Down Expand Up @@ -301,6 +275,7 @@ def __search_for_field_attributes(base, attrs: dict):
field.model = new_class

meta._model = new_class
meta.finalise_fields()
return new_class


Expand All @@ -311,8 +286,42 @@ class Model(metaclass=ModelMeta):
def __init__(self, *args, _from_db: bool = False, **kwargs) -> None:
# self._meta is a very common attribute lookup, lets cache it.
meta = self._meta
self._saved_in_db = _from_db or (meta.pk_attr in kwargs and meta.is_pk_generated)
self._saved_in_db = _from_db or (meta.pk_attr in kwargs and meta.pk.generated)
self._init_lazy_fkm2m()

# Assign values and do type conversions
passed_fields = {*kwargs.keys()}
passed_fields.update(meta.fetch_fields)
passed_fields |= self._set_field_values(kwargs)

# Assign defaults for missing fields
for key in meta.fields.difference(passed_fields):
field_object = meta.fields_map[key]
if callable(field_object.default):
setattr(self, key, field_object.default())
else:
setattr(self, key, field_object.default)

@classmethod
def _init_from_db(cls, **kwargs) -> MODEL_TYPE:
self = cls.__new__(cls)
self._saved_in_db = True
self._init_lazy_fkm2m()

meta = self._meta

for key, value in kwargs.items():
if key in meta.fields:
field_object = meta.fields_map[key]
setattr(self, key, field_object.to_python_value(value))
elif key in meta.db_fields:
field_object = meta.fields_map[meta.fields_db_projection_reverse[key]]
setattr(self, key, field_object.to_python_value(value))

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]
Expand All @@ -332,19 +341,6 @@ def __init__(self, *args, _from_db: bool = False, **kwargs) -> None:
ManyToManyRelationManager(field_object.type, self, field_object), # type: ignore
)

# Assign values and do type conversions
passed_fields = set(kwargs.keys())
passed_fields.update(meta.fetch_fields)
passed_fields |= self._set_field_values(kwargs)

# Assign defaults for missing fields
for key in meta.fields.difference(passed_fields):
field_object = meta.fields_map[key]
if callable(field_object.default):
setattr(self, key, field_object.default())
else:
setattr(self, key, field_object.default)

def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]:
"""
Sets values for fields honoring type transformations and
Expand Down
2 changes: 2 additions & 0 deletions tortoise/query_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from copy import copy
from typing import Any, List, Mapping, Optional, Tuple # noqa

from pypika import Table
Expand Down Expand Up @@ -309,6 +310,7 @@ class Prefetch:
def __init__(self, relation, queryset) -> None:
self.relation = relation
self.queryset = queryset
self.queryset.query = copy(self.queryset.model._meta.basequery)

def resolve_for_queryset(self, queryset) -> None:
relation_split = self.relation.split("__")
Expand Down
Loading

0 comments on commit 5f6bd8c

Please sign in to comment.