Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split model consructor into from-Python and from-DB paths, leading to 15-25% speedup for large fetch operations. #158

Merged
merged 5 commits into from
Jul 21, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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