diff --git a/docs/api/query-set/join.md b/docs/api/query-set/join.md
index fcf5b88..7a08704 100644
--- a/docs/api/query-set/join.md
+++ b/docs/api/query-set/join.md
@@ -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
-
+
#### \_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.
diff --git a/docs/models/index.md b/docs/models/index.md
index 40ddb63..0bee4ea 100644
--- a/docs/models/index.md
+++ b/docs/models/index.md
@@ -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
diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py
index 20df795..6cf1f12 100644
--- a/ormar/models/helpers/models.py
+++ b/ormar/models/helpers/models.py
@@ -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
diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py
index 20a00d6..030b05b 100644
--- a/ormar/models/metaclass.py
+++ b/ormar/models/metaclass.py
@@ -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]
diff --git a/ormar/models/model.py b/ormar/models/model.py
index 35eaab1..c81a14b 100644
--- a/ormar/models/model.py
+++ b/ormar/models/model.py
@@ -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.
diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py
index 4746db4..514fdf1 100644
--- a/ormar/queryset/clause.py
+++ b/ormar/queryset/clause.py
@@ -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
diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py
index 2b2205f..302289d 100644
--- a/ormar/queryset/join.py
+++ b/ormar/queryset/join.py
@@ -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
diff --git a/ormar/queryset/prefetch_query.py b/ormar/queryset/prefetch_query.py
index dda3baa..96de967 100644
--- a/ormar/queryset/prefetch_query.py
+++ b/ormar/queryset/prefetch_query.py
@@ -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 (
diff --git a/ormar/queryset/query.py b/ormar/queryset/query.py
index 64b9ede..733a1b0 100644
--- a/ormar/queryset/query.py
+++ b/ormar/queryset/query.py
@@ -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
diff --git a/ormar/relations/alias_manager.py b/ormar/relations/alias_manager.py
index 7f7fd7c..0484007 100644
--- a/ormar/relations/alias_manager.py
+++ b/ormar/relations/alias_manager.py
@@ -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:
diff --git a/ormar/relations/relation_proxy.py b/ormar/relations/relation_proxy.py
index 1012155..518fc71 100644
--- a/ormar/relations/relation_proxy.py
+++ b/ormar/relations/relation_proxy.py
@@ -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.
diff --git a/tests/test_forward_refs.py b/tests/test_forward_refs.py
index be355cf..f002ef1 100644
--- a/tests/test_forward_refs.py
+++ b/tests/test_forward_refs.py
@@ -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"
diff --git a/tests/test_order_by.py b/tests/test_order_by.py
index 07d5526..02639ca 100644
--- a/tests/test_order_by.py
+++ b/tests/test_order_by.py
@@ -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()
)
diff --git a/tests/test_selecting_subset_of_columns.py b/tests/test_selecting_subset_of_columns.py
index e3221dd..a2d57db 100644
--- a/tests/test_selecting_subset_of_columns.py
+++ b/tests/test_selecting_subset_of_columns.py
@@ -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": ...,