Refactor in join in order to make possibility for nested duplicated relations (and it was a mess :D)

This commit is contained in:
collerek
2021-01-15 17:05:23 +01:00
parent d10141ba6f
commit 0fe95b0c7b
14 changed files with 271 additions and 303 deletions

View File

@ -150,11 +150,11 @@ Process order_by causes for non m2m relations.
- `fields (Optional[Union[Set, Dict]])`: fields to include
- `exclude_fields (Optional[Union[Set, Dict]])`: fields to exclude
<a name="queryset.join.SqlJoin._switch_many_to_many_order_columns"></a>
<a name="queryset.join.SqlJoin._replace_many_to_many_order_by_columns"></a>
#### \_switch\_many\_to\_many\_order\_columns
```python
| _switch_many_to_many_order_columns(part: str, new_part: str) -> None
| _replace_many_to_many_order_by_columns(part: str, new_part: str) -> None
```
Substitutes the name of the relation with actual model name in m2m order bys.

View File

@ -76,7 +76,7 @@ Since it can be a function you can set `default=datetime.datetime.now` and get c
response with `include`/`exclude` and `response_model_include`/`response_model_exclude` accordingly.
```python
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
class User(ormar.Model):
class Meta:
tablename: str = "users2"
@ -93,14 +93,14 @@ class User(ormar.Model):
pydantic_only=True, default=datetime.datetime.now
)
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
app =FastAPI()
@app.post("/users/")
async def create_user(user: User):
return await user.save()
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
def test_excluding_fields_in_endpoints():
client = TestClient(app)
@ -127,7 +127,7 @@ def test_excluding_fields_in_endpoints():
assert response.json().get("timestamp") == str(timestamp).replace(" ", "T")
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
```
#### Property fields
@ -190,7 +190,7 @@ in the response from `fastapi` and `dict()` and `json()` methods. You cannot pas
```
```python
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
def gen_pass(): # note: NOT production ready
choices = string.ascii_letters + string.digits + "!@#$%^&*()"
return "".join(random.choice(choices) for _ in range(20))
@ -215,7 +215,7 @@ class RandomModel(ormar.Model):
def full_name(self) -> str:
return " ".join([self.first_name, self.last_name])
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
app =FastAPI()
# explicitly exclude property_field in this endpoint
@ -223,7 +223,7 @@ app =FastAPI()
async def create_user(user: RandomModel):
return await user.save()
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
def test_excluding_property_field_in_endpoints2():
client = TestClient(app)
@ -241,7 +241,7 @@ def test_excluding_property_field_in_endpoints2():
# despite being decorated with property_field if you explictly exclude it it will be gone
assert response.json().get("full_name") is None
# <==part of code removed for clarity==>
# <==related of code removed for clarity==>
```
#### Fields names vs Column names

View File

@ -1,4 +1,5 @@
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type
import itertools
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type
import ormar # noqa: I100
from ormar.fields.foreign_key import ForeignKeyField
@ -109,3 +110,32 @@ def validate_related_names_in_relations( # noqa CCR001
f"\nTip: provide different related_name for FK and/or M2M fields"
)
previous_related_names.append(field.related_name)
def group_related_list(list_: List) -> Dict:
"""
Translates the list of related strings into a dictionary.
That way nested models are grouped to traverse them in a right order
and to avoid repetition.
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
will become:
{'people': {'houses': [], 'cars': ['models', 'colors']}}
:param list_: list of related models used in select related
:type list_: List[str]
:return: list converted to dictionary to avoid repetition and group nested models
:rtype: Dict[str, List]
"""
test_dict: Dict[str, Any] = dict()
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
for key, group in grouped:
group_list = list(group)
new = [
"__".join(x.split("__")[1:]) for x in group_list if len(x.split("__")) > 1
]
if any("__" in x for x in new):
test_dict[key] = group_related_list(new)
else:
test_dict[key] = new
return test_dict

View File

@ -221,7 +221,7 @@ def update_attrs_and_fields(
:param attrs: new namespace for class being constructed
:type attrs: Dict
:param new_attrs: part of the namespace extracted from parent class
:param new_attrs: related of the namespace extracted from parent class
:type new_attrs: Dict
:param model_fields: ormar fields in defined in current class
:type model_fields: Dict[str, BaseField]

View File

@ -1,4 +1,3 @@
import itertools
from typing import (
Any,
Dict,
@ -18,38 +17,9 @@ import ormar.queryset # noqa I100
from ormar.exceptions import ModelPersistenceError, NoMatch
from ormar.fields.many_to_many import ManyToManyField
from ormar.models import NewBaseModel # noqa I100
from ormar.models.helpers.models import group_related_list
from ormar.models.metaclass import ModelMeta
def group_related_list(list_: List) -> Dict:
"""
Translates the list of related strings into a dictionary.
That way nested models are grouped to traverse them in a right order
and to avoid repetition.
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
will become:
{'people': {'houses': [], 'cars': ['models', 'colors']}}
:param list_: list of related models used in select related
:type list_: List[str]
:return: list converted to dictionary to avoid repetition and group nested models
:rtype: Dict[str, List]
"""
test_dict: Dict[str, Any] = dict()
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
for key, group in grouped:
group_list = list(group)
new = [
"__".join(x.split("__")[1:]) for x in group_list if len(x.split("__")) > 1
]
if any("__" in x for x in new):
test_dict[key] = group_related_list(new)
else:
test_dict[key] = new
return test_dict
if TYPE_CHECKING: # pragma nocover
from ormar import QuerySet
@ -73,9 +43,11 @@ class Model(NewBaseModel):
select_related: List = None,
related_models: Any = None,
previous_model: Type[T] = None,
source_model: Type[T] = None,
related_name: str = None,
fields: Optional[Union[Dict, Set]] = None,
exclude_fields: Optional[Union[Dict, Set]] = None,
current_relation_str: str = None,
) -> Optional[T]:
"""
Model method to convert raw sql row from database into ormar.Model instance.
@ -112,7 +84,10 @@ class Model(NewBaseModel):
item: Dict[str, Any] = {}
select_related = select_related or []
related_models = related_models or []
table_prefix = ""
if select_related:
source_model = cls
related_models = group_related_list(select_related)
rel_name2 = related_name
@ -135,11 +110,15 @@ class Model(NewBaseModel):
previous_model = through_field.through # type: ignore
if previous_model and rel_name2:
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
previous_model, rel_name2
)
else:
table_prefix = ""
# TODO finish duplicated nested relation or remove this
if current_relation_str and "__" in current_relation_str and source_model:
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
from_model=source_model, relation_name=current_relation_str
)
if not table_prefix:
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
from_model=previous_model, relation_name=rel_name2
)
item = cls.populate_nested_models_from_row(
item=item,
@ -147,6 +126,8 @@ class Model(NewBaseModel):
related_models=related_models,
fields=fields,
exclude_fields=exclude_fields,
current_relation_str=current_relation_str,
source_model=source_model,
)
item = cls.extract_prefixed_table_columns(
item=item,
@ -163,8 +144,6 @@ class Model(NewBaseModel):
)
instance = cls(**item)
instance.set_save_status(True)
else:
instance = None
return instance
@classmethod
@ -175,6 +154,8 @@ class Model(NewBaseModel):
related_models: Any,
fields: Optional[Union[Dict, Set]] = None,
exclude_fields: Optional[Union[Dict, Set]] = None,
current_relation_str: str = None,
source_model: Type[T] = None,
) -> dict:
"""
Traverses structure of related models and populates the nested models
@ -202,35 +183,31 @@ class Model(NewBaseModel):
and values are database values
:rtype: Dict
"""
for related in related_models:
relation_str = (
"__".join([current_relation_str, related])
if current_relation_str
else related
)
fields = cls.get_included(fields, related)
exclude_fields = cls.get_excluded(exclude_fields, related)
model_cls = cls.Meta.model_fields[related].to
remainder = None
if isinstance(related_models, dict) and related_models[related]:
first_part, remainder = related, related_models[related]
model_cls = cls.Meta.model_fields[first_part].to
fields = cls.get_included(fields, first_part)
exclude_fields = cls.get_excluded(exclude_fields, first_part)
child = model_cls.from_row(
row,
related_models=remainder,
previous_model=cls,
related_name=related,
fields=fields,
exclude_fields=exclude_fields,
)
item[model_cls.get_column_name_from_alias(first_part)] = child
else:
model_cls = cls.Meta.model_fields[related].to
fields = cls.get_included(fields, related)
exclude_fields = cls.get_excluded(exclude_fields, related)
child = model_cls.from_row(
row,
previous_model=cls,
related_name=related,
fields=fields,
exclude_fields=exclude_fields,
)
item[model_cls.get_column_name_from_alias(related)] = child
remainder = related_models[related]
child = model_cls.from_row(
row,
related_models=remainder,
previous_model=cls,
related_name=related,
fields=fields,
exclude_fields=exclude_fields,
current_relation_str=relation_str,
source_model=source_model,
)
item[model_cls.get_column_name_from_alias(related)] = child
return item
@ -251,7 +228,7 @@ class Model(NewBaseModel):
All joined tables have prefixes to allow duplicate column names,
as well as duplicated joins to the same table from multiple different tables.
Extracted fields populates the item dict later used to construct a Model.
Extracted fields populates the related dict later used to construct a Model.
Used in Model.from_row and PrefetchQuery._populate_rows methods.

View File

@ -194,7 +194,9 @@ class QueryClause:
previous_model = through_field.through
part2 = through_field.default_target_field_name() # type: ignore
manager = model_cls.Meta.alias_manager
table_prefix = manager.resolve_relation_alias(previous_model, part2)
table_prefix = manager.resolve_relation_alias(
from_model=previous_model, relation_name=part2
)
model_cls = model_cls.Meta.model_fields[part].to
previous_model = model_cls
return select_related, table_prefix, model_cls

View File

@ -1,8 +1,8 @@
from collections import OrderedDict
from typing import (
Any,
Dict,
List,
NamedTuple,
Optional,
Set,
TYPE_CHECKING,
@ -14,24 +14,13 @@ from typing import (
import sqlalchemy
from sqlalchemy import text
from ormar.fields import ManyToManyField # noqa I100
from ormar.fields import BaseField, ManyToManyField # noqa I100
from ormar.relations import AliasManager
if TYPE_CHECKING: # pragma no cover
from ormar import Model
class JoinParameters(NamedTuple):
"""
Named tuple that holds set of parameters passed during join construction.
"""
prev_model: Type["Model"]
previous_alias: str
from_table: str
model_cls: Type["Model"]
class SqlJoin:
def __init__( # noqa: CFQ002
self,
@ -42,7 +31,12 @@ class SqlJoin:
exclude_fields: Optional[Union[Set, Dict]],
order_columns: Optional[List],
sorted_orders: OrderedDict,
main_model: Type["Model"],
related_models: Any = None,
own_alias: str = "",
) -> None:
self.own_alias = own_alias
self.related_models = related_models or []
self.used_aliases = used_aliases
self.select_from = select_from
self.columns = columns
@ -50,18 +44,17 @@ class SqlJoin:
self.exclude_fields = exclude_fields
self.order_columns = order_columns
self.sorted_orders = sorted_orders
self.main_model = main_model
@staticmethod
def alias_manager(model_cls: Type["Model"]) -> AliasManager:
@property
def alias_manager(self) -> AliasManager:
"""
Shortcut for ormars model AliasManager stored on Meta.
Shortcut for ormar's model AliasManager stored on Meta.
:param model_cls: ormar Model class
:type model_cls: Type[Model]
:return: alias manager from model's Meta
:rtype: AliasManager
"""
return model_cls.Meta.alias_manager
return self.main_model.Meta.alias_manager
@staticmethod
def on_clause(
@ -86,33 +79,32 @@ class SqlJoin:
right_part = f"{previous_alias + '_' if previous_alias else ''}{from_clause}"
return text(f"{left_part}={right_part}")
@staticmethod
def update_inclusions(
model_cls: Type["Model"],
fields: Optional[Union[Set, Dict]],
exclude_fields: Optional[Union[Set, Dict]],
nested_name: str,
) -> Tuple[Optional[Union[Dict, Set]], Optional[Union[Dict, Set]]]:
"""
Extract nested fields and exclude_fields if applicable.
:param model_cls: ormar model class
:type model_cls: Type["Model"]
:param fields: fields to include
:type fields: Optional[Union[Set, Dict]]
:param exclude_fields: fields to exclude
:type exclude_fields: Optional[Union[Set, Dict]]
:param nested_name: name of the nested field
:type nested_name: str
:return: updated exclude and include fields from nested objects
:rtype: Tuple[Optional[Union[Dict, Set]], Optional[Union[Dict, Set]]]
"""
fields = model_cls.get_included(fields, nested_name)
exclude_fields = model_cls.get_excluded(exclude_fields, nested_name)
return fields, exclude_fields
def process_deeper_join(
self, related_name: str, model_cls: Type["Model"], remainder: Any, alias: str,
) -> None:
sql_join = SqlJoin(
used_aliases=self.used_aliases,
select_from=self.select_from,
columns=self.columns,
fields=self.main_model.get_excluded(self.fields, related_name),
exclude_fields=self.main_model.get_excluded(
self.exclude_fields, related_name
),
order_columns=self.order_columns,
sorted_orders=self.sorted_orders,
main_model=model_cls,
related_models=remainder,
own_alias=alias,
)
(
self.used_aliases,
self.select_from,
self.columns,
self.sorted_orders,
) = sql_join.build_join(related_name)
def build_join( # noqa: CCR001
self, item: str, join_parameters: JoinParameters
self, related: str
) -> Tuple[List, sqlalchemy.sql.select, List, OrderedDict]:
"""
Main external access point for building a join.
@ -120,59 +112,61 @@ class SqlJoin:
handles switching to through models for m2m relations, returns updated lists of
used_aliases and sort_orders.
:param item: string with join definition
:type item: str
:param join_parameters: parameters from previous/ current join
:type join_parameters: JoinParameters
:param related: string with join definition
:type related: str
:return: list of used aliases, select from, list of aliased columns, sort orders
:rtype: Tuple[List[str], Join, List[TextClause], collections.OrderedDict]
"""
fields = self.fields
exclude_fields = self.exclude_fields
target_field = self.main_model.Meta.model_fields[related]
prev_model = self.main_model
# TODO: Finish refactoring here
if issubclass(target_field, ManyToManyField):
new_part = self.process_m2m_related_name_change(
target_field=target_field, related=related
)
self._replace_many_to_many_order_by_columns(related, new_part)
for index, part in enumerate(item.split("__")):
if issubclass(
join_parameters.model_cls.Meta.model_fields[part], ManyToManyField
model_cls = target_field.through
alias = self.alias_manager.resolve_relation_alias(
from_model=prev_model, relation_name=related
)
if alias not in self.used_aliases:
self._process_join(
model_cls=model_cls,
related=related,
alias=alias,
target_field=target_field,
)
related = new_part
self.own_alias = alias
prev_model = model_cls
target_field = target_field.through.Meta.model_fields[related]
model_cls = target_field.to
alias = model_cls.Meta.alias_manager.resolve_relation_alias(
from_model=prev_model, relation_name=related
)
if alias not in self.used_aliases:
self._process_join(
model_cls=model_cls,
prev_model=prev_model,
related=related,
alias=alias,
target_field=target_field,
)
for related_name in self.related_models:
remainder = None
if (
isinstance(self.related_models, dict)
and self.related_models[related_name]
):
_fields = join_parameters.model_cls.Meta.model_fields
target_field = _fields[part]
if (
target_field.self_reference
and part == target_field.self_reference_primary
):
new_part = target_field.default_source_field_name() # type: ignore
else:
new_part = target_field.default_target_field_name() # type: ignore
self._switch_many_to_many_order_columns(part, new_part)
if index > 0: # nested joins
fields, exclude_fields = SqlJoin.update_inclusions(
model_cls=join_parameters.model_cls,
fields=fields,
exclude_fields=exclude_fields,
nested_name=part,
)
join_parameters = self._build_join_parameters(
part=part,
join_params=join_parameters,
is_multi=True,
fields=fields,
exclude_fields=exclude_fields,
)
part = new_part
if index > 0: # nested joins
fields, exclude_fields = SqlJoin.update_inclusions(
model_cls=join_parameters.model_cls,
fields=fields,
exclude_fields=exclude_fields,
nested_name=part,
)
join_parameters = self._build_join_parameters(
part=part,
join_params=join_parameters,
fields=fields,
exclude_fields=exclude_fields,
remainder = self.related_models[related_name]
self.process_deeper_join(
related_name=related_name,
model_cls=model_cls,
remainder=remainder,
alias=alias,
)
return (
@ -182,65 +176,44 @@ class SqlJoin:
self.sorted_orders,
)
def _build_join_parameters(
self,
part: str,
join_params: JoinParameters,
fields: Optional[Union[Set, Dict]],
exclude_fields: Optional[Union[Set, Dict]],
is_multi: bool = False,
) -> JoinParameters:
@staticmethod
def process_m2m_related_name_change(
target_field: Type[ManyToManyField], related: str, reverse: bool = False
) -> str:
"""
Updates used_aliases to not join multiple times to the same table.
Updates join parameters with new values.
Extracts relation name to link join through the Through model declared on
relation field.
:param part: part of the join str definition
:type part: str
:param join_params: parameters from previous/ current join
:type join_params: JoinParameters
:param fields: fields to include
:type fields: Optional[Union[Set, Dict]]
:param exclude_fields: fields to exclude
:type exclude_fields: Optional[Union[Set, Dict]]
:param is_multi: flag if the relation is m2m
:type is_multi: bool
:return: updated join parameters
:rtype: ormar.queryset.join.JoinParameters
Changes the same names in order_by queries if they are present.
:param reverse: flag if it's on_clause lookup - use reverse fields
:type reverse: bool
:param target_field: relation field
:type target_field: Type[ManyToManyField]
:param related: name of the relation
:type related: str
:return: new relation name switched to through model field
:rtype: str
"""
if is_multi:
model_cls = join_params.model_cls.Meta.model_fields[part].through
else:
model_cls = join_params.model_cls.Meta.model_fields[part].to
to_table = model_cls.Meta.table.name
alias = model_cls.Meta.alias_manager.resolve_relation_alias(
join_params.prev_model, part
is_primary_self_ref = (
target_field.self_reference
and related == target_field.self_reference_primary
)
if alias not in self.used_aliases:
self._process_join(
join_params=join_params,
is_multi=is_multi,
model_cls=model_cls,
part=part,
alias=alias,
fields=fields,
exclude_fields=exclude_fields,
)
previous_alias = alias
from_table = to_table
prev_model = model_cls
return JoinParameters(prev_model, previous_alias, from_table, model_cls)
if (is_primary_self_ref and not reverse) or (
not is_primary_self_ref and reverse
):
new_part = target_field.default_source_field_name() # type: ignore
else:
new_part = target_field.default_target_field_name() # type: ignore
return new_part
def _process_join( # noqa: CFQ002
self,
join_params: JoinParameters,
is_multi: bool,
model_cls: Type["Model"],
part: str,
related: str,
alias: str,
fields: Optional[Union[Set, Dict]],
exclude_fields: Optional[Union[Set, Dict]],
target_field: Type[BaseField],
prev_model: Type["Model"] = None,
) -> None:
"""
Resolves to and from column names and table names.
@ -255,63 +228,53 @@ class SqlJoin:
Process order_by causes for non m2m relations.
:param join_params: parameters from previous/ current join
:type join_params: JoinParameters
:param is_multi: flag if it's m2m relation
:type is_multi: bool
:param model_cls:
:type model_cls: ormar.models.metaclass.ModelMetaclass
:param part: name of the field used in join
:type part: str
:param related: name of the field used in join
:type related: str
:param alias: alias of the current join
:type alias: str
:param fields: fields to include
:type fields: Optional[Union[Set, Dict]]
:param exclude_fields: fields to exclude
:type exclude_fields: Optional[Union[Set, Dict]]
"""
to_table = model_cls.Meta.table.name
to_key, from_key = self.get_to_and_from_keys(
join_params, is_multi, model_cls, part
)
to_key, from_key = self.get_to_and_from_keys(related, target_field)
prev_model = prev_model or self.main_model
on_clause = self.on_clause(
previous_alias=join_params.previous_alias,
previous_alias=self.own_alias,
alias=alias,
from_clause=f"{join_params.from_table}.{from_key}",
from_clause=f"{prev_model.Meta.tablename}.{from_key}",
to_clause=f"{to_table}.{to_key}",
)
target_table = self.alias_manager(model_cls).prefixed_table_name(
alias, to_table
)
target_table = self.alias_manager.prefixed_table_name(alias, to_table)
self.select_from = sqlalchemy.sql.outerjoin(
self.select_from, target_table, on_clause
)
pkname_alias = model_cls.get_column_alias(model_cls.Meta.pkname)
if not is_multi:
if not issubclass(target_field, ManyToManyField):
self.get_order_bys(
alias=alias,
to_table=to_table,
pkname_alias=pkname_alias,
part=part,
part=related,
model_cls=model_cls,
)
self_related_fields = model_cls.own_table_columns(
model=model_cls,
fields=fields,
exclude_fields=exclude_fields,
fields=self.fields,
exclude_fields=self.exclude_fields,
use_alias=True,
)
self.columns.extend(
self.alias_manager(model_cls).prefixed_columns(
self.alias_manager.prefixed_columns(
alias, model_cls.Meta.table, self_related_fields
)
)
self.used_aliases.append(alias)
def _switch_many_to_many_order_columns(self, part: str, new_part: str) -> None:
def _replace_many_to_many_order_by_columns(self, part: str, new_part: str) -> None:
"""
Substitutes the name of the relation with actual model name in m2m order bys.
@ -325,7 +288,7 @@ class SqlJoin:
x.split("__") for x in self.order_columns if "__" in x
]
for condition in split_order_columns:
if condition[-2] == part or condition[-2][1:] == part:
if self._check_if_condition_apply(condition, part):
condition[-2] = condition[-2].replace(part, new_part)
self.order_columns = [x for x in self.order_columns if "__" not in x] + [
"__".join(x) for x in split_order_columns
@ -413,51 +376,34 @@ class SqlJoin:
order = text(f"{alias}_{to_table}.{pkname_alias}")
self.sorted_orders[f"{alias}.{pkname_alias}"] = order
@staticmethod
def get_to_and_from_keys(
join_params: JoinParameters,
is_multi: bool,
model_cls: Type["Model"],
part: str,
self, related: str, target_field: Type[BaseField]
) -> Tuple[str, str]:
"""
Based on the relation type, name of the relation and previous models and parts
stored in JoinParameters it resolves the current to and from keys, which are
different for ManyToMany relation, ForeignKey and reverse part of relations.
different for ManyToMany relation, ForeignKey and reverse related of relations.
:param join_params: parameters from previous/ current join
:type join_params: JoinParameters
:param is_multi: flag if the relation is of m2m type
:type is_multi: bool
:param model_cls: ormar model class
:type model_cls: Type[Model]
:param part: name of the current relation join
:type part: str
:param target_field: relation field
:type target_field: Type[ForeignKeyField]
:param related: name of the current relation join
:type related: str
:return: to key and from key
:rtype: Tuple[str, str]
"""
if is_multi:
target_field = join_params.model_cls.Meta.model_fields[part]
if (
target_field.self_reference
and part == target_field.self_reference_primary
):
to_key = target_field.default_target_field_name() # type: ignore
else:
to_key = target_field.default_source_field_name() # type: ignore
from_key = join_params.prev_model.get_column_alias(
join_params.prev_model.Meta.pkname
if issubclass(target_field, ManyToManyField):
to_key = self.process_m2m_related_name_change(
target_field=target_field, related=related, reverse=True
)
from_key = self.main_model.get_column_alias(self.main_model.Meta.pkname)
elif join_params.prev_model.Meta.model_fields[part].virtual:
to_field = join_params.prev_model.Meta.model_fields[part].get_related_name()
to_key = model_cls.get_column_alias(to_field)
from_key = join_params.prev_model.get_column_alias(
join_params.prev_model.Meta.pkname
)
elif target_field.virtual:
to_field = target_field.get_related_name()
to_key = target_field.to.get_column_alias(to_field)
from_key = self.main_model.get_column_alias(self.main_model.Meta.pkname)
else:
to_key = model_cls.get_column_alias(model_cls.Meta.pkname)
from_key = join_params.prev_model.get_column_alias(part)
to_key = target_field.to.get_column_alias(target_field.to.Meta.pkname)
from_key = self.main_model.get_column_alias(related)
return to_key, from_key

View File

@ -526,7 +526,7 @@ class PrefetchQuery:
query_target = target_field.through
select_related = [target_name]
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias(
query_target, target_name
from_model=query_target, relation_name=target_name
)
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
@ -551,14 +551,14 @@ class PrefetchQuery:
@staticmethod
def _get_select_related_if_apply(related: str, select_dict: Dict) -> Dict:
"""
Extract nested part of select_related dictionary to extract models nested
Extract nested related of select_related dictionary to extract models nested
deeper on related model and already loaded in select related query.
:param related: name of the relation
:type related: str
:param select_dict: dictionary of select related models in main query
:type select_dict: Dict
:return: dictionary with nested part of select related
:return: dictionary with nested related of select related
:rtype: Dict
"""
return (

View File

@ -6,8 +6,9 @@ import sqlalchemy
from sqlalchemy import text
import ormar # noqa I100
from ormar.models.helpers.models import group_related_list
from ormar.queryset import FilterQuery, LimitQuery, OffsetQuery, OrderQuery
from ormar.queryset.join import JoinParameters, SqlJoin
from ormar.queryset.join import SqlJoin
if TYPE_CHECKING: # pragma no cover
from ormar import Model
@ -140,14 +141,16 @@ class Query:
else:
self.select_from = self.table
# TODO: Refactor to convert to nested dict like in from_row in model
self._select_related.sort(key=lambda item: (item, -len(item)))
related_models = group_related_list(self._select_related)
for item in self._select_related:
join_parameters = JoinParameters(
self.model_cls, "", self.table.name, self.model_cls
)
fields = self.model_cls.get_included(self.fields, item)
exclude_fields = self.model_cls.get_excluded(self.exclude_fields, item)
for related in related_models:
fields = self.model_cls.get_included(self.fields, related)
exclude_fields = self.model_cls.get_excluded(self.exclude_fields, related)
remainder = None
if isinstance(related_models, dict) and related_models[related]:
remainder = related_models[related]
sql_join = SqlJoin(
used_aliases=self.used_aliases,
select_from=self.select_from,
@ -156,6 +159,8 @@ class Query:
exclude_fields=exclude_fields,
order_columns=self.order_columns,
sorted_orders=self.sorted_orders,
main_model=self.model_cls,
related_models=remainder,
)
(
@ -163,14 +168,14 @@ class Query:
self.select_from,
self.columns,
self.sorted_orders,
) = sql_join.build_join(item, join_parameters)
) = sql_join.build_join(related)
expr = sqlalchemy.sql.select(self.columns)
expr = expr.select_from(self.select_from)
expr = self._apply_expression_modifiers(expr)
# print(expr.compile(compile_kwargs={"literal_binds": True}))
# print("\n", expr.compile(compile_kwargs={"literal_binds": True}))
self._reset_query_parameters()
return expr

View File

@ -113,6 +113,19 @@ class AliasManager:
if child_key not in self._aliases_new:
self._aliases_new[child_key] = get_table_alias()
def add_alias(self, alias_key: str) -> str:
"""
Adds alias to the dictionary of aliases under given key.
:param alias_key: key of relation to generate alias for
:type alias_key: str
:return: generated alias
:rtype: str
"""
alias = get_table_alias()
self._aliases_new[alias_key] = alias
return alias
def resolve_relation_alias(
self, from_model: Type["Model"], relation_name: str
) -> str:

View File

@ -127,7 +127,7 @@ class RelationProxy(list):
self, item: "Model", keep_reversed: bool = True
) -> None:
"""
Removes the item from relation with parent.
Removes the related from relation with parent.
Through models are automatically deleted for m2m relations.

View File

@ -190,7 +190,7 @@ async def test_m2m_self_forwardref_relation(cleanup):
# await steve.friends.add(billy)
billy_check = await Child.objects.select_related(
["friends", "favourite_game", "least_favourite_game",]
["friends", "favourite_game", "least_favourite_game"]
).get(name="Billy")
assert len(billy_check.friends) == 2
assert billy_check.friends[0].name == "Kate"
@ -200,5 +200,6 @@ async def test_m2m_self_forwardref_relation(cleanup):
kate_check = await Child.objects.select_related(["also_friends",]).get(
name="Kate"
)
assert len(kate_check.also_friends) == 1
assert kate_check.also_friends[0].name == "Billy"

View File

@ -280,7 +280,7 @@ async def test_sort_order_on_many_to_many():
assert users[1].cars[3].name == "Buggy"
users = (
await User.objects.select_related(["cars", "cars__factory"])
await User.objects.select_related(["cars__factory"])
.order_by(["-cars__factory__name", "cars__name"])
.all()
)

View File

@ -116,9 +116,7 @@ async def test_selecting_subset():
)
all_cars = (
await Car.objects.select_related(
["manufacturer", "manufacturer__hq", "manufacturer__hq__nicks"]
)
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
[
"id",
@ -132,9 +130,7 @@ async def test_selecting_subset():
)
all_cars2 = (
await Car.objects.select_related(
["manufacturer", "manufacturer__hq", "manufacturer__hq__nicks"]
)
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,
@ -149,9 +145,7 @@ async def test_selecting_subset():
)
all_cars3 = (
await Car.objects.select_related(
["manufacturer", "manufacturer__hq", "manufacturer__hq__nicks"]
)
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,