update docs, add load_all(), tests for load_all, make through field optional
This commit is contained in:
@ -306,7 +306,7 @@ async def joins():
|
||||
# visit: https://collerek.github.io/ormar/relations/
|
||||
|
||||
# to read more about joins and subqueries
|
||||
# visit: https://collerek.github.io/ormar/queries/delete/
|
||||
# visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/
|
||||
|
||||
|
||||
async def filter_and_sort():
|
||||
|
||||
@ -72,6 +72,27 @@ Excludes defaults and alias as they are populated separately
|
||||
|
||||
`(bool)`: True if field is present on pydantic.FieldInfo
|
||||
|
||||
<a name="fields.base.BaseField.get_base_pydantic_field_info"></a>
|
||||
#### get\_base\_pydantic\_field\_info
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| get_base_pydantic_field_info(cls, allow_null: bool) -> FieldInfo
|
||||
```
|
||||
|
||||
Generates base pydantic.FieldInfo with only default and optionally
|
||||
required to fix pydantic Json field being set to required=False.
|
||||
Used in an ormar Model Metaclass.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `allow_null (bool)`: flag if the default value can be None
|
||||
or if it should be populated by pydantic Undefined
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(pydantic.FieldInfo)`: instance of base pydantic.FieldInfo
|
||||
|
||||
<a name="fields.base.BaseField.convert_to_pydantic_field_info"></a>
|
||||
#### convert\_to\_pydantic\_field\_info
|
||||
|
||||
|
||||
@ -332,3 +332,32 @@ Selects the appropriate constructor based on a passed value.
|
||||
|
||||
`(Optional[Union["Model", List["Model"]]])`: returns a Model or a list of Models
|
||||
|
||||
<a name="fields.foreign_key.ForeignKeyField.get_relation_name"></a>
|
||||
#### get\_relation\_name
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| get_relation_name(cls) -> str
|
||||
```
|
||||
|
||||
Returns name of the relation, which can be a own name or through model
|
||||
names for m2m models
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="fields.foreign_key.ForeignKeyField.get_source_model"></a>
|
||||
#### get\_source\_model
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| get_source_model(cls) -> Type["Model"]
|
||||
```
|
||||
|
||||
Returns model from which the relation comes -> either owner or through model
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Type["Model"])`: source model
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ pydantic field to use and type of the target column field.
|
||||
#### ManyToMany
|
||||
|
||||
```python
|
||||
ManyToMany(to: "ToType", through: "ToType", *, name: str = None, unique: bool = False, virtual: bool = False, **kwargs: Any, ,) -> Any
|
||||
ManyToMany(to: "ToType", through: Optional["ToType"] = None, *, name: str = None, unique: bool = False, virtual: bool = False, **kwargs: Any, ,) -> Any
|
||||
```
|
||||
|
||||
Despite a name it's a function that returns constructed ManyToManyField.
|
||||
@ -134,3 +134,42 @@ Evaluates the ForwardRef to actual Field based on global and local namespaces
|
||||
|
||||
`(None)`: None
|
||||
|
||||
<a name="fields.many_to_many.ManyToManyField.get_relation_name"></a>
|
||||
#### get\_relation\_name
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| get_relation_name(cls) -> str
|
||||
```
|
||||
|
||||
Returns name of the relation, which can be a own name or through model
|
||||
names for m2m models
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="fields.many_to_many.ManyToManyField.get_source_model"></a>
|
||||
#### get\_source\_model
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| get_source_model(cls) -> Type["Model"]
|
||||
```
|
||||
|
||||
Returns model from which the relation comes -> either owner or through model
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Type["Model"])`: source model
|
||||
|
||||
<a name="fields.many_to_many.ManyToManyField.create_default_through_model"></a>
|
||||
#### create\_default\_through\_model
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| create_default_through_model(cls) -> None
|
||||
```
|
||||
|
||||
Creates default empty through model if no additional fields are required.
|
||||
|
||||
|
||||
188
docs/api/models/excludable-items.md
Normal file
188
docs/api/models/excludable-items.md
Normal file
@ -0,0 +1,188 @@
|
||||
<a name="models.excludable"></a>
|
||||
# models.excludable
|
||||
|
||||
<a name="models.excludable.Excludable"></a>
|
||||
## Excludable Objects
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Excludable()
|
||||
```
|
||||
|
||||
Class that keeps sets of fields to exclude and include
|
||||
|
||||
<a name="models.excludable.Excludable.get_copy"></a>
|
||||
#### get\_copy
|
||||
|
||||
```python
|
||||
| get_copy() -> "Excludable"
|
||||
```
|
||||
|
||||
Return copy of self to avoid in place modifications
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(ormar.models.excludable.Excludable)`: copy of self with copied sets
|
||||
|
||||
<a name="models.excludable.Excludable.set_values"></a>
|
||||
#### set\_values
|
||||
|
||||
```python
|
||||
| set_values(value: Set, is_exclude: bool) -> None
|
||||
```
|
||||
|
||||
Appends the data to include/exclude sets.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `value (set)`: set of values to add
|
||||
- `is_exclude (bool)`: flag if values are to be excluded or included
|
||||
|
||||
<a name="models.excludable.Excludable.is_included"></a>
|
||||
#### is\_included
|
||||
|
||||
```python
|
||||
| is_included(key: str) -> bool
|
||||
```
|
||||
|
||||
Check if field in included (in set or set is {...})
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `key (str)`: key to check
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="models.excludable.Excludable.is_excluded"></a>
|
||||
#### is\_excluded
|
||||
|
||||
```python
|
||||
| is_excluded(key: str) -> bool
|
||||
```
|
||||
|
||||
Check if field in excluded (in set or set is {...})
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `key (str)`: key to check
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="models.excludable.ExcludableItems"></a>
|
||||
## ExcludableItems Objects
|
||||
|
||||
```python
|
||||
class ExcludableItems()
|
||||
```
|
||||
|
||||
Keeps a dictionary of Excludables by alias + model_name keys
|
||||
to allow quick lookup by nested models without need to travers
|
||||
deeply nested dictionaries and passing include/exclude around
|
||||
|
||||
<a name="models.excludable.ExcludableItems.from_excludable"></a>
|
||||
#### from\_excludable
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| from_excludable(cls, other: "ExcludableItems") -> "ExcludableItems"
|
||||
```
|
||||
|
||||
Copy passed ExcludableItems to avoid inplace modifications.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `other (ormar.models.excludable.ExcludableItems)`: other excludable items to be copied
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(ormar.models.excludable.ExcludableItems)`: copy of other
|
||||
|
||||
<a name="models.excludable.ExcludableItems.get"></a>
|
||||
#### get
|
||||
|
||||
```python
|
||||
| get(model_cls: Type["Model"], alias: str = "") -> Excludable
|
||||
```
|
||||
|
||||
Return Excludable for given model and alias.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model_cls (ormar.models.metaclass.ModelMetaclass)`: target model to check
|
||||
- `alias (str)`: table alias from relation manager
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(ormar.models.excludable.Excludable)`: Excludable for given model and alias
|
||||
|
||||
<a name="models.excludable.ExcludableItems.build"></a>
|
||||
#### build
|
||||
|
||||
```python
|
||||
| build(items: Union[List[str], str, Tuple[str], Set[str], Dict], model_cls: Type["Model"], is_exclude: bool = False) -> None
|
||||
```
|
||||
|
||||
Receives the one of the types of items and parses them as to achieve
|
||||
a end situation with one excludable per alias/model in relation.
|
||||
|
||||
Each excludable has two sets of values - one to include, one to exclude.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `items (Union[List[str], str, Tuple[str], Set[str], Dict])`: values to be included or excluded
|
||||
- `model_cls (ormar.models.metaclass.ModelMetaclass)`: source model from which relations are constructed
|
||||
- `is_exclude (bool)`: flag if items should be included or excluded
|
||||
|
||||
<a name="models.excludable.ExcludableItems._set_excludes"></a>
|
||||
#### \_set\_excludes
|
||||
|
||||
```python
|
||||
| _set_excludes(items: Set, model_name: str, is_exclude: bool, alias: str = "") -> None
|
||||
```
|
||||
|
||||
Sets set of values to be included or excluded for given key and model.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `items (set)`: items to include/exclude
|
||||
- `model_name (str)`: name of model to construct key
|
||||
- `is_exclude (bool)`: flag if values should be included or excluded
|
||||
- `alias (str)`:
|
||||
|
||||
<a name="models.excludable.ExcludableItems._traverse_dict"></a>
|
||||
#### \_traverse\_dict
|
||||
|
||||
```python
|
||||
| _traverse_dict(values: Dict, source_model: Type["Model"], model_cls: Type["Model"], is_exclude: bool, related_items: List = None, alias: str = "") -> None
|
||||
```
|
||||
|
||||
Goes through dict of nested values and construct/update Excludables.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `values (Dict)`: items to include/exclude
|
||||
- `source_model (ormar.models.metaclass.ModelMetaclass)`: source model from which relations are constructed
|
||||
- `model_cls (ormar.models.metaclass.ModelMetaclass)`: model from which current relation is constructed
|
||||
- `is_exclude (bool)`: flag if values should be included or excluded
|
||||
- `related_items (List)`: list of names of related fields chain
|
||||
- `alias (str)`: alias of relation
|
||||
|
||||
<a name="models.excludable.ExcludableItems._traverse_list"></a>
|
||||
#### \_traverse\_list
|
||||
|
||||
```python
|
||||
| _traverse_list(values: Set[str], model_cls: Type["Model"], is_exclude: bool) -> None
|
||||
```
|
||||
|
||||
Goes through list of values and construct/update Excludables.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `values (set)`: items to include/exclude
|
||||
- `model_cls (ormar.models.metaclass.ModelMetaclass)`: model from which current relation is constructed
|
||||
- `is_exclude (bool)`: flag if values should be included or excluded
|
||||
|
||||
@ -87,28 +87,6 @@ extraction of ormar model_fields.
|
||||
|
||||
`(Tuple[Dict, Dict])`: namespace of the class updated, dict of extracted model_fields
|
||||
|
||||
<a name="models.helpers.models.validate_related_names_in_relations"></a>
|
||||
#### validate\_related\_names\_in\_relations
|
||||
|
||||
```python
|
||||
validate_related_names_in_relations(model_fields: Dict, new_model: Type["Model"]) -> None
|
||||
```
|
||||
|
||||
Performs a validation of relation_names in relation fields.
|
||||
If multiple fields are leading to the same related model
|
||||
only one can have empty related_name param
|
||||
(populated by default as model.name.lower()+'s').
|
||||
Also related_names have to be unique for given related model.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `ModelDefinitionError`: if validation of related_names fail
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model_fields (Dict[str, ormar.Field])`: dictionary of declared ormar model fields
|
||||
- `new_model (Model class)`:
|
||||
|
||||
<a name="models.helpers.models.group_related_list"></a>
|
||||
#### group\_related\_list
|
||||
|
||||
@ -134,3 +112,23 @@ Result dictionary is sorted by length of the values and by key
|
||||
|
||||
`(Dict[str, List])`: list converted to dictionary to avoid repetition and group nested models
|
||||
|
||||
<a name="models.helpers.models.meta_field_not_set"></a>
|
||||
#### meta\_field\_not\_set
|
||||
|
||||
```python
|
||||
meta_field_not_set(model: Type["Model"], field_name: str) -> bool
|
||||
```
|
||||
|
||||
Checks if field with given name is already present in model.Meta.
|
||||
Then check if it's set to something truthful
|
||||
(in practice meaning not None, as it's non or ormar Field only).
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model (Model class)`: newly constructed model
|
||||
- `field_name (str)`: name of the ormar field
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
#### create\_pydantic\_field
|
||||
|
||||
```python
|
||||
create_pydantic_field(field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]) -> None
|
||||
create_pydantic_field(field_name: str, model: Type["Model"], model_field: Type["ManyToManyField"]) -> None
|
||||
```
|
||||
|
||||
Registers pydantic field on through model that leads to passed model
|
||||
@ -42,7 +42,7 @@ field_name. Returns a pydantic field with type of field_name field type.
|
||||
#### populate\_default\_pydantic\_field\_value
|
||||
|
||||
```python
|
||||
populate_default_pydantic_field_value(ormar_field: Type[BaseField], field_name: str, attrs: dict) -> dict
|
||||
populate_default_pydantic_field_value(ormar_field: Type["BaseField"], field_name: str, attrs: dict) -> dict
|
||||
```
|
||||
|
||||
Grabs current value of the ormar Field in class namespace
|
||||
@ -94,7 +94,7 @@ Those annotations are later used by pydantic to construct it's own fields.
|
||||
#### get\_pydantic\_base\_orm\_config
|
||||
|
||||
```python
|
||||
get_pydantic_base_orm_config() -> Type[BaseConfig]
|
||||
get_pydantic_base_orm_config() -> Type[pydantic.BaseConfig]
|
||||
```
|
||||
|
||||
Returns empty pydantic Config with orm_mode set to True.
|
||||
|
||||
25
docs/api/models/helpers/related-names-validation.md
Normal file
25
docs/api/models/helpers/related-names-validation.md
Normal file
@ -0,0 +1,25 @@
|
||||
<a name="models.helpers.related_names_validation"></a>
|
||||
# models.helpers.related\_names\_validation
|
||||
|
||||
<a name="models.helpers.related_names_validation.validate_related_names_in_relations"></a>
|
||||
#### validate\_related\_names\_in\_relations
|
||||
|
||||
```python
|
||||
validate_related_names_in_relations(model_fields: Dict, new_model: Type["Model"]) -> None
|
||||
```
|
||||
|
||||
Performs a validation of relation_names in relation fields.
|
||||
If multiple fields are leading to the same related model
|
||||
only one can have empty related_name param
|
||||
(populated by default as model.name.lower()+'s').
|
||||
Also related_names have to be unique for given related model.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `ModelDefinitionError`: if validation of related_names fail
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model_fields (Dict[str, ormar.Field])`: dictionary of declared ormar model fields
|
||||
- `new_model (Model class)`:
|
||||
|
||||
@ -23,7 +23,7 @@ aliases for proper sql joins.
|
||||
#### register\_many\_to\_many\_relation\_on\_build
|
||||
|
||||
```python
|
||||
register_many_to_many_relation_on_build(field: Type[ManyToManyField]) -> None
|
||||
register_many_to_many_relation_on_build(field: Type["ManyToManyField"]) -> None
|
||||
```
|
||||
|
||||
Registers connection between through model and both sides of the m2m relation.
|
||||
@ -89,11 +89,24 @@ Autogenerated reverse fields also set related_name to the original field name.
|
||||
|
||||
- `model_field (relation Field)`: original relation ForeignKey field
|
||||
|
||||
<a name="models.helpers.relations.register_through_shortcut_fields"></a>
|
||||
#### register\_through\_shortcut\_fields
|
||||
|
||||
```python
|
||||
register_through_shortcut_fields(model_field: Type["ManyToManyField"]) -> None
|
||||
```
|
||||
|
||||
Registers m2m relation through shortcut on both ends of the relation.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model_field (ManyToManyField)`: relation field defined in parent model
|
||||
|
||||
<a name="models.helpers.relations.register_relation_in_alias_manager"></a>
|
||||
#### register\_relation\_in\_alias\_manager
|
||||
|
||||
```python
|
||||
register_relation_in_alias_manager(field: Type[ForeignKeyField]) -> None
|
||||
register_relation_in_alias_manager(field: Type["ForeignKeyField"]) -> None
|
||||
```
|
||||
|
||||
Registers the relation (and reverse relation) in alias manager.
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
#### adjust\_through\_many\_to\_many\_model
|
||||
|
||||
```python
|
||||
adjust_through_many_to_many_model(model_field: Type[ManyToManyField]) -> None
|
||||
adjust_through_many_to_many_model(model_field: Type["ManyToManyField"]) -> None
|
||||
```
|
||||
|
||||
Registers m2m relation on through model.
|
||||
@ -21,7 +21,7 @@ Sets pydantic fields with child and parent model types.
|
||||
#### create\_and\_append\_m2m\_fk
|
||||
|
||||
```python
|
||||
create_and_append_m2m_fk(model: Type["Model"], model_field: Type[ManyToManyField], field_name: str) -> None
|
||||
create_and_append_m2m_fk(model: Type["Model"], model_field: Type["ManyToManyField"], field_name: str) -> None
|
||||
```
|
||||
|
||||
Registers sqlalchemy Column with sqlalchemy.ForeignKey leading to the model.
|
||||
@ -38,7 +38,7 @@ Newly created field is added to m2m relation through model Meta columns and tabl
|
||||
#### check\_pk\_column\_validity
|
||||
|
||||
```python
|
||||
check_pk_column_validity(field_name: str, field: BaseField, pkname: Optional[str]) -> Optional[str]
|
||||
check_pk_column_validity(field_name: str, field: "BaseField", pkname: Optional[str]) -> Optional[str]
|
||||
```
|
||||
|
||||
Receives the field marked as primary key and verifies if the pkname
|
||||
@ -165,7 +165,7 @@ It populates name, metadata, columns and constraints.
|
||||
#### update\_column\_definition
|
||||
|
||||
```python
|
||||
update_column_definition(model: Union[Type["Model"], Type["NewBaseModel"]], field: Type[ForeignKeyField]) -> None
|
||||
update_column_definition(model: Union[Type["Model"], Type["NewBaseModel"]], field: Type["ForeignKeyField"]) -> None
|
||||
```
|
||||
|
||||
Updates a column with a new type column based on updated parameters in FK fields.
|
||||
|
||||
120
docs/api/models/helpers/validation.md
Normal file
120
docs/api/models/helpers/validation.md
Normal file
@ -0,0 +1,120 @@
|
||||
<a name="models.helpers.validation"></a>
|
||||
# models.helpers.validation
|
||||
|
||||
<a name="models.helpers.validation.check_if_field_has_choices"></a>
|
||||
#### check\_if\_field\_has\_choices
|
||||
|
||||
```python
|
||||
check_if_field_has_choices(field: Type[BaseField]) -> bool
|
||||
```
|
||||
|
||||
Checks if given field has choices populated.
|
||||
A if it has one, a validator for this field needs to be attached.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (BaseField)`: ormar field to check
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="models.helpers.validation.convert_choices_if_needed"></a>
|
||||
#### convert\_choices\_if\_needed
|
||||
|
||||
```python
|
||||
convert_choices_if_needed(field: Type["BaseField"], value: Any) -> Tuple[Any, List]
|
||||
```
|
||||
|
||||
Converts dates to isoformat as fastapi can check this condition in routes
|
||||
and the fields are not yet parsed.
|
||||
|
||||
Converts enums to list of it's values.
|
||||
|
||||
Converts uuids to strings.
|
||||
|
||||
Converts decimal to float with given scale.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (Type[BaseField])`: ormar field to check with choices
|
||||
- `values (Dict)`: current values of the model to verify
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Tuple[Any, List])`: value, choices list
|
||||
|
||||
<a name="models.helpers.validation.validate_choices"></a>
|
||||
#### validate\_choices
|
||||
|
||||
```python
|
||||
validate_choices(field: Type["BaseField"], value: Any) -> None
|
||||
```
|
||||
|
||||
Validates if given value is in provided choices.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `ValueError`: If value is not in choices.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (Type[BaseField])`: field to validate
|
||||
- `value (Any)`: value of the field
|
||||
|
||||
<a name="models.helpers.validation.choices_validator"></a>
|
||||
#### choices\_validator
|
||||
|
||||
```python
|
||||
choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, Any]
|
||||
```
|
||||
|
||||
Validator that is attached to pydantic model pre root validators.
|
||||
Validator checks if field value is in field.choices list.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `ValueError`: if field value is outside of allowed choices.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `cls (Model class)`: constructed class
|
||||
- `values (Dict[str, Any])`: dictionary of field values (pydantic side)
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict[str, Any])`: values if pass validation, otherwise exception is raised
|
||||
|
||||
<a name="models.helpers.validation.construct_modify_schema_function"></a>
|
||||
#### construct\_modify\_schema\_function
|
||||
|
||||
```python
|
||||
construct_modify_schema_function(fields_with_choices: List) -> SchemaExtraCallable
|
||||
```
|
||||
|
||||
Modifies the schema to include fields with choices validator.
|
||||
Those fields will be displayed in schema as Enum types with available choices
|
||||
values listed next to them.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `fields_with_choices (List)`: list of fields with choices validation
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Callable)`: callable that will be run by pydantic to modify the schema
|
||||
|
||||
<a name="models.helpers.validation.populate_choices_validators"></a>
|
||||
#### populate\_choices\_validators
|
||||
|
||||
```python
|
||||
populate_choices_validators(model: Type["Model"]) -> None
|
||||
```
|
||||
|
||||
Checks if Model has any fields with choices set.
|
||||
If yes it adds choices validation into pre root validators.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model (Model class)`: newly constructed Model
|
||||
|
||||
@ -30,88 +30,12 @@ passed items.
|
||||
|
||||
`(Union[Set, Dict, None])`: child extracted from items if exists
|
||||
|
||||
<a name="models.mixins.excludable_mixin.ExcludableMixin.get_excluded"></a>
|
||||
#### get\_excluded
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| get_excluded(exclude: Union[Set, Dict, None], key: str = None) -> Union[Set, Dict, None]
|
||||
```
|
||||
|
||||
Proxy to ExcludableMixin.get_child for exclusions.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `exclude (Union[Set, Dict, None])`: bag of items to exclude
|
||||
- `key (str)`: name of the child to extract
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Union[Set, Dict, None])`: child extracted from items if exists
|
||||
|
||||
<a name="models.mixins.excludable_mixin.ExcludableMixin.get_included"></a>
|
||||
#### get\_included
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| get_included(include: Union[Set, Dict, None], key: str = None) -> Union[Set, Dict, None]
|
||||
```
|
||||
|
||||
Proxy to ExcludableMixin.get_child for inclusions.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `include (Union[Set, Dict, None])`: bag of items to include
|
||||
- `key (str)`: name of the child to extract
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Union[Set, Dict, None])`: child extracted from items if exists
|
||||
|
||||
<a name="models.mixins.excludable_mixin.ExcludableMixin.is_excluded"></a>
|
||||
#### is\_excluded
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| is_excluded(exclude: Union[Set, Dict, None], key: str = None) -> bool
|
||||
```
|
||||
|
||||
Checks if given key should be excluded on model/ dict.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `exclude (Union[Set, Dict, None])`: bag of items to exclude
|
||||
- `key (str)`: name of the child to extract
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Union[Set, Dict, None])`: child extracted from items if exists
|
||||
|
||||
<a name="models.mixins.excludable_mixin.ExcludableMixin.is_included"></a>
|
||||
#### is\_included
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| is_included(include: Union[Set, Dict, None], key: str = None) -> bool
|
||||
```
|
||||
|
||||
Checks if given key should be included on model/ dict.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `include (Union[Set, Dict, None])`: bag of items to include
|
||||
- `key (str)`: name of the child to extract
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Union[Set, Dict, None])`: child extracted from items if exists
|
||||
|
||||
<a name="models.mixins.excludable_mixin.ExcludableMixin._populate_pk_column"></a>
|
||||
#### \_populate\_pk\_column
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| _populate_pk_column(model: Type["Model"], columns: List[str], use_alias: bool = False) -> List[str]
|
||||
| _populate_pk_column(model: Union[Type["Model"], Type["ModelRow"]], columns: List[str], use_alias: bool = False) -> List[str]
|
||||
```
|
||||
|
||||
Adds primary key column/alias (depends on use_alias flag) to list of
|
||||
@ -132,7 +56,7 @@ column names that are selected.
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| own_table_columns(cls, model: Type["Model"], fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]], use_alias: bool = False) -> List[str]
|
||||
| own_table_columns(cls, model: Union[Type["Model"], Type["ModelRow"]], excludable: ExcludableItems, alias: str = "", use_alias: bool = False) -> List[str]
|
||||
```
|
||||
|
||||
Returns list of aliases or field names for given model.
|
||||
@ -145,9 +69,9 @@ Primary key field is always added and cannot be excluded (will be added anyway).
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `alias (str)`: relation prefix
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
- `model (Type["Model"])`: model on columns are selected
|
||||
- `fields (Optional[Union[Set, Dict]])`: set/dict of fields to include
|
||||
- `exclude_fields (Optional[Union[Set, Dict]])`: set/dict of fields to exclude
|
||||
- `use_alias (bool)`: flag if aliases or field names should be used
|
||||
|
||||
**Returns**:
|
||||
@ -183,7 +107,7 @@ exclusion, for nested models all related models are excluded.
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| get_names_to_exclude(cls, fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None) -> Set
|
||||
| get_names_to_exclude(cls, excludable: ExcludableItems, alias: str) -> Set
|
||||
```
|
||||
|
||||
Returns a set of models field names that should be explicitly excluded
|
||||
@ -197,8 +121,8 @@ them with dicts constructed from those db rows.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `fields (Optional[Union[Set, Dict]])`: set/dict of fields to include
|
||||
- `exclude_fields (Optional[Union[Set, Dict]])`: set/dict of fields to exclude
|
||||
- `alias (str)`: alias of current relation
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
|
||||
**Returns**:
|
||||
|
||||
|
||||
@ -40,12 +40,26 @@ List is cached in cls._related_fields for quicker access.
|
||||
|
||||
`(List)`: list of related fields
|
||||
|
||||
<a name="models.mixins.relation_mixin.RelationMixin.extract_through_names"></a>
|
||||
#### extract\_through\_names
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| extract_through_names(cls) -> Set
|
||||
```
|
||||
|
||||
Extracts related fields through names which are shortcuts to through models.
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Set)`: set of related through fields names
|
||||
|
||||
<a name="models.mixins.relation_mixin.RelationMixin.extract_related_names"></a>
|
||||
#### extract\_related\_names
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| extract_related_names(cls) -> Set
|
||||
| extract_related_names(cls) -> Set[str]
|
||||
```
|
||||
|
||||
Returns List of fields names for all relations declared on a model.
|
||||
@ -53,7 +67,7 @@ List is cached in cls._related_names for quicker access.
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(List)`: list of related fields names
|
||||
`(Set)`: set of related fields names
|
||||
|
||||
<a name="models.mixins.relation_mixin.RelationMixin._extract_db_related_names"></a>
|
||||
#### \_extract\_db\_related\_names
|
||||
@ -91,3 +105,24 @@ for nested models all related models are returned.
|
||||
|
||||
`(Set)`: set of non mandatory related fields
|
||||
|
||||
<a name="models.mixins.relation_mixin.RelationMixin._iterate_related_models"></a>
|
||||
#### \_iterate\_related\_models
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| _iterate_related_models(cls, visited: Set[Union[Type["Model"], Type["RelationMixin"]]] = None, source_relation: str = None, source_model: Union[Type["Model"], Type["RelationMixin"]] = None) -> List[str]
|
||||
```
|
||||
|
||||
Iterates related models recursively to extract relation strings of
|
||||
nested not visited models.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `visited (Set[str])`: set of already visited models
|
||||
- `source_relation (str)`: name of the current relation
|
||||
- `source_model (Type["Model"])`: model from which relation comes in nested relations
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(List[str])`: list of relation strings to be passed to select_related
|
||||
|
||||
|
||||
@ -91,3 +91,22 @@ passed by the user.
|
||||
|
||||
`(Dict)`: dictionary of model that is about to be saved
|
||||
|
||||
<a name="models.mixins.save_mixin.SavePrepareMixin.validate_choices"></a>
|
||||
#### validate\_choices
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| validate_choices(cls, new_kwargs: Dict) -> Dict
|
||||
```
|
||||
|
||||
Receives dictionary of model that is about to be saved and validates the
|
||||
fields with choices set to see if the value is allowed.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `new_kwargs (Dict)`: dictionary of model that is about to be saved
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict)`: dictionary of model that is about to be saved
|
||||
|
||||
|
||||
@ -12,61 +12,6 @@ Class used for type hinting.
|
||||
Users can subclass this one for convenience but it's not required.
|
||||
The only requirement is that ormar.Model has to have inner class with name Meta.
|
||||
|
||||
<a name="models.metaclass.check_if_field_has_choices"></a>
|
||||
#### check\_if\_field\_has\_choices
|
||||
|
||||
```python
|
||||
check_if_field_has_choices(field: Type[BaseField]) -> bool
|
||||
```
|
||||
|
||||
Checks if given field has choices populated.
|
||||
A if it has one, a validator for this field needs to be attached.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (BaseField)`: ormar field to check
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="models.metaclass.choices_validator"></a>
|
||||
#### choices\_validator
|
||||
|
||||
```python
|
||||
choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, Any]
|
||||
```
|
||||
|
||||
Validator that is attached to pydantic model pre root validators.
|
||||
Validator checks if field value is in field.choices list.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `ValueError`: if field value is outside of allowed choices.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `cls (Model class)`: constructed class
|
||||
- `values (Dict[str, Any])`: dictionary of field values (pydantic side)
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict[str, Any])`: values if pass validation, otherwise exception is raised
|
||||
|
||||
<a name="models.metaclass.populate_choices_validators"></a>
|
||||
#### populate\_choices\_validators
|
||||
|
||||
```python
|
||||
populate_choices_validators(model: Type["Model"]) -> None
|
||||
```
|
||||
|
||||
Checks if Model has any fields with choices set.
|
||||
If yes it adds choices validation into pre root validators.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model (Model class)`: newly constructed Model
|
||||
|
||||
<a name="models.metaclass.add_cached_properties"></a>
|
||||
#### add\_cached\_properties
|
||||
|
||||
@ -87,26 +32,6 @@ All properties here are used as "cache" to not recalculate them constantly.
|
||||
|
||||
- `new_model (Model class)`: newly constructed Model
|
||||
|
||||
<a name="models.metaclass.meta_field_not_set"></a>
|
||||
#### meta\_field\_not\_set
|
||||
|
||||
```python
|
||||
meta_field_not_set(model: Type["Model"], field_name: str) -> bool
|
||||
```
|
||||
|
||||
Checks if field with given name is already present in model.Meta.
|
||||
Then check if it's set to something truthful
|
||||
(in practice meaning not None, as it's non or ormar Field only).
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `model (Model class)`: newly constructed model
|
||||
- `field_name (str)`: name of the ormar field
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="models.metaclass.add_property_fields"></a>
|
||||
#### add\_property\_fields
|
||||
|
||||
@ -141,24 +66,6 @@ Signals are emitted in both model own methods and in selected queryset ones.
|
||||
|
||||
- `new_model (Model class)`: newly constructed model
|
||||
|
||||
<a name="models.metaclass.update_attrs_and_fields"></a>
|
||||
#### update\_attrs\_and\_fields
|
||||
|
||||
```python
|
||||
update_attrs_and_fields(attrs: Dict, new_attrs: Dict, model_fields: Dict, new_model_fields: Dict, new_fields: Set) -> Dict
|
||||
```
|
||||
|
||||
Updates __annotations__, values of model fields (so pydantic FieldInfos)
|
||||
as well as model.Meta.model_fields definitions from parents.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `attrs (Dict)`: new namespace for class being constructed
|
||||
- `new_attrs (Dict)`: related of the namespace extracted from parent class
|
||||
- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class
|
||||
- `new_model_fields (Dict[str, BaseField])`: ormar fields defined in parent classes
|
||||
- `new_fields (Set[str])`: set of new fields names
|
||||
|
||||
<a name="models.metaclass.verify_constraint_names"></a>
|
||||
#### verify\_constraint\_names
|
||||
|
||||
@ -195,7 +102,7 @@ Updates Meta parameters in child from parent if needed.
|
||||
#### copy\_and\_replace\_m2m\_through\_model
|
||||
|
||||
```python
|
||||
copy_and_replace_m2m_through_model(field: Type[ManyToManyField], field_name: str, table_name: str, parent_fields: Dict, attrs: Dict, meta: ModelMeta) -> None
|
||||
copy_and_replace_m2m_through_model(field: Type[ManyToManyField], field_name: str, table_name: str, parent_fields: Dict, attrs: Dict, meta: ModelMeta, base_class: Type["Model"]) -> None
|
||||
```
|
||||
|
||||
Clones class with Through model for m2m relations, appends child name to the name
|
||||
@ -211,6 +118,7 @@ Removes the original sqlalchemy table from metadata if it was not removed.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `base_class (Type["Model"])`: base class model
|
||||
- `field (Type[ManyToManyField])`: field with relations definition
|
||||
- `field_name (str)`: name of the relation field
|
||||
- `table_name (str)`: name of the table
|
||||
@ -281,6 +189,24 @@ If the class is a ormar.Model it is skipped.
|
||||
|
||||
`(Tuple[Dict, Dict])`: updated attrs and model_fields
|
||||
|
||||
<a name="models.metaclass.update_attrs_and_fields"></a>
|
||||
#### update\_attrs\_and\_fields
|
||||
|
||||
```python
|
||||
update_attrs_and_fields(attrs: Dict, new_attrs: Dict, model_fields: Dict, new_model_fields: Dict, new_fields: Set) -> Dict
|
||||
```
|
||||
|
||||
Updates __annotations__, values of model fields (so pydantic FieldInfos)
|
||||
as well as model.Meta.model_fields definitions from parents.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `attrs (Dict)`: new namespace for class being constructed
|
||||
- `new_attrs (Dict)`: related of the namespace extracted from parent class
|
||||
- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class
|
||||
- `new_model_fields (Dict[str, BaseField])`: ormar fields defined in parent classes
|
||||
- `new_fields (Set[str])`: set of new fields names
|
||||
|
||||
<a name="models.metaclass.ModelMetaclass"></a>
|
||||
## ModelMetaclass Objects
|
||||
|
||||
|
||||
132
docs/api/models/model-row.md
Normal file
132
docs/api/models/model-row.md
Normal file
@ -0,0 +1,132 @@
|
||||
<a name="models.model_row"></a>
|
||||
# models.model\_row
|
||||
|
||||
<a name="models.model_row.ModelRow"></a>
|
||||
## ModelRow Objects
|
||||
|
||||
```python
|
||||
class ModelRow(NewBaseModel)
|
||||
```
|
||||
|
||||
<a name="models.model_row.ModelRow.from_row"></a>
|
||||
#### from\_row
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| from_row(cls, row: sqlalchemy.engine.ResultProxy, source_model: Type["Model"], select_related: List = None, related_models: Any = None, related_field: Type["ForeignKeyField"] = None, excludable: ExcludableItems = None, current_relation_str: str = "", proxy_source_model: Optional[Type["Model"]] = None) -> Optional["Model"]
|
||||
```
|
||||
|
||||
Model method to convert raw sql row from database into ormar.Model instance.
|
||||
Traverses nested models if they were specified in select_related for query.
|
||||
|
||||
Called recurrently and returns model instance if it's present in the row.
|
||||
Note that it's processing one row at a time, so if there are duplicates of
|
||||
parent row that needs to be joined/combined
|
||||
(like parent row in sql join with 2+ child rows)
|
||||
instances populated in this method are later combined in the QuerySet.
|
||||
Other method working directly on raw database results is in prefetch_query,
|
||||
where rows are populated in a different way as they do not have
|
||||
nested models in result.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `proxy_source_model (Optional[Type["ModelRow"]])`: source model from which querysetproxy is constructed
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
- `current_relation_str (str)`: name of the relation field
|
||||
- `source_model (Type[Model])`: model on which relation was defined
|
||||
- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database
|
||||
- `select_related (List)`: list of names of related models fetched from database
|
||||
- `related_models (Union[List, Dict])`: list or dict of related models
|
||||
- `related_field (Type[ForeignKeyField])`: field with relation declaration
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Optional[Model])`: returns model if model is populated from database
|
||||
|
||||
<a name="models.model_row.ModelRow._populate_nested_models_from_row"></a>
|
||||
#### \_populate\_nested\_models\_from\_row
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| _populate_nested_models_from_row(cls, item: dict, row: sqlalchemy.engine.ResultProxy, source_model: Type["Model"], related_models: Any, excludable: ExcludableItems, table_prefix: str, current_relation_str: str = None, proxy_source_model: Type["Model"] = None) -> dict
|
||||
```
|
||||
|
||||
Traverses structure of related models and populates the nested models
|
||||
from the database row.
|
||||
Related models can be a list if only directly related models are to be
|
||||
populated, converted to dict if related models also have their own related
|
||||
models to be populated.
|
||||
|
||||
Recurrently calls from_row method on nested instances and create nested
|
||||
instances. In the end those instances are added to the final model dictionary.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `proxy_source_model (Optional[Type["ModelRow"]])`: source model from which querysetproxy is constructed
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
- `source_model (Type[Model])`: source model from which relation started
|
||||
- `current_relation_str (str)`: joined related parts into one string
|
||||
- `item (Dict)`: dictionary of already populated nested models, otherwise empty dict
|
||||
- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database
|
||||
- `related_models (Union[Dict, List])`: list or dict of related models
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict)`: dictionary with keys corresponding to model fields names
|
||||
and values are database values
|
||||
|
||||
<a name="models.model_row.ModelRow.populate_through_instance"></a>
|
||||
#### populate\_through\_instance
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| populate_through_instance(cls, row: sqlalchemy.engine.ResultProxy, through_name: str, related: str, excludable: ExcludableItems) -> "ModelRow"
|
||||
```
|
||||
|
||||
Initialize the through model from db row.
|
||||
Excluded all relation fields and other exclude/include set in excludable.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `row (sqlalchemy.engine.ResultProxy)`: loaded row from database
|
||||
- `through_name (str)`: name of the through field
|
||||
- `related (str)`: name of the relation
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
|
||||
**Returns**:
|
||||
|
||||
`("ModelRow")`: initialized through model without relation
|
||||
|
||||
<a name="models.model_row.ModelRow.extract_prefixed_table_columns"></a>
|
||||
#### extract\_prefixed\_table\_columns
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| extract_prefixed_table_columns(cls, item: dict, row: sqlalchemy.engine.result.ResultProxy, table_prefix: str, excludable: ExcludableItems) -> Dict
|
||||
```
|
||||
|
||||
Extracts own fields from raw sql result, using a given prefix.
|
||||
Prefix changes depending on the table's position in a join.
|
||||
|
||||
If the table is a main table, there is no prefix.
|
||||
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 related dict later used to construct a Model.
|
||||
|
||||
Used in Model.from_row and PrefetchQuery._populate_rows methods.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
- `item (Dict)`: dictionary of already populated nested models, otherwise empty dict
|
||||
- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database
|
||||
- `table_prefix (str)`: prefix of the table from AliasManager
|
||||
each pair of tables have own prefix (two of them depending on direction) -
|
||||
used in joins to allow multiple joins to the same table.
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict)`: dictionary with keys corresponding to model fields names
|
||||
and values are database values
|
||||
|
||||
@ -5,122 +5,14 @@
|
||||
## Model Objects
|
||||
|
||||
```python
|
||||
class Model(NewBaseModel)
|
||||
class Model(ModelRow)
|
||||
```
|
||||
|
||||
<a name="models.model.Model.from_row"></a>
|
||||
#### from\_row
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| from_row(cls: Type[T], row: sqlalchemy.engine.ResultProxy, 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.
|
||||
Traverses nested models if they were specified in select_related for query.
|
||||
|
||||
Called recurrently and returns model instance if it's present in the row.
|
||||
Note that it's processing one row at a time, so if there are duplicates of
|
||||
parent row that needs to be joined/combined
|
||||
(like parent row in sql join with 2+ child rows)
|
||||
instances populated in this method are later combined in the QuerySet.
|
||||
Other method working directly on raw database results is in prefetch_query,
|
||||
where rows are populated in a different way as they do not have
|
||||
nested models in result.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `current_relation_str (str)`: name of the relation field
|
||||
- `source_model (Type[Model])`: model on which relation was defined
|
||||
- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database
|
||||
- `select_related (List)`: list of names of related models fetched from database
|
||||
- `related_models (Union[List, Dict])`: list or dict of related models
|
||||
- `previous_model (Model class)`: internal param for nested models to specify table_prefix
|
||||
- `related_name (str)`: internal parameter - name of current nested model
|
||||
- `fields (Optional[Union[Dict, Set]])`: fields and related model fields to include
|
||||
if provided only those are included
|
||||
- `exclude_fields (Optional[Union[Dict, Set]])`: fields and related model fields to exclude
|
||||
excludes the fields even if they are provided in fields
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Optional[Model])`: returns model if model is populated from database
|
||||
|
||||
<a name="models.model.Model.populate_nested_models_from_row"></a>
|
||||
#### populate\_nested\_models\_from\_row
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| populate_nested_models_from_row(cls, item: dict, row: sqlalchemy.engine.ResultProxy, 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
|
||||
from the database row.
|
||||
Related models can be a list if only directly related models are to be
|
||||
populated, converted to dict if related models also have their own related
|
||||
models to be populated.
|
||||
|
||||
Recurrently calls from_row method on nested instances and create nested
|
||||
instances. In the end those instances are added to the final model dictionary.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `source_model (Type[Model])`: source model from which relation started
|
||||
- `current_relation_str (str)`: joined related parts into one string
|
||||
- `item (Dict)`: dictionary of already populated nested models, otherwise empty dict
|
||||
- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database
|
||||
- `related_models (Union[Dict, List])`: list or dict of related models
|
||||
- `fields (Optional[Union[Dict, Set]])`: fields and related model fields to include -
|
||||
if provided only those are included
|
||||
- `exclude_fields (Optional[Union[Dict, Set]])`: fields and related model fields to exclude
|
||||
excludes the fields even if they are provided in fields
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict)`: dictionary with keys corresponding to model fields names
|
||||
and values are database values
|
||||
|
||||
<a name="models.model.Model.extract_prefixed_table_columns"></a>
|
||||
#### extract\_prefixed\_table\_columns
|
||||
|
||||
```python
|
||||
| @classmethod
|
||||
| extract_prefixed_table_columns(cls, item: dict, row: sqlalchemy.engine.result.ResultProxy, table_prefix: str, fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None) -> dict
|
||||
```
|
||||
|
||||
Extracts own fields from raw sql result, using a given prefix.
|
||||
Prefix changes depending on the table's position in a join.
|
||||
|
||||
If the table is a main table, there is no prefix.
|
||||
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 related dict later used to construct a Model.
|
||||
|
||||
Used in Model.from_row and PrefetchQuery._populate_rows methods.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `item (Dict)`: dictionary of already populated nested models, otherwise empty dict
|
||||
- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database
|
||||
- `table_prefix (str)`: prefix of the table from AliasManager
|
||||
each pair of tables have own prefix (two of them depending on direction) -
|
||||
used in joins to allow multiple joins to the same table.
|
||||
- `fields (Optional[Union[Dict, Set]])`: fields and related model fields to include -
|
||||
if provided only those are included
|
||||
- `exclude_fields (Optional[Union[Dict, Set]])`: fields and related model fields to exclude
|
||||
excludes the fields even if they are provided in fields
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Dict)`: dictionary with keys corresponding to model fields names
|
||||
and values are database values
|
||||
|
||||
<a name="models.model.Model.upsert"></a>
|
||||
#### upsert
|
||||
|
||||
```python
|
||||
| async upsert(**kwargs: Any) -> T
|
||||
| async upsert(**kwargs: Any) -> "Model"
|
||||
```
|
||||
|
||||
Performs either a save or an update depending on the presence of the pk.
|
||||
@ -139,7 +31,7 @@ For save kwargs are ignored, used only in update if provided.
|
||||
#### save
|
||||
|
||||
```python
|
||||
| async save() -> T
|
||||
| async save() -> "Model"
|
||||
```
|
||||
|
||||
Performs a save of given Model instance.
|
||||
@ -203,7 +95,7 @@ number of updated instances
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| async _update_and_follow(rel: T, follow: bool, visited: Set, update_count: int) -> Tuple[int, Set]
|
||||
| async _update_and_follow(rel: "Model", follow: bool, visited: Set, update_count: int) -> Tuple[int, Set]
|
||||
```
|
||||
|
||||
Internal method used in save_related to follow related models and update numbers
|
||||
@ -227,7 +119,7 @@ number of updated instances
|
||||
#### update
|
||||
|
||||
```python
|
||||
| async update(**kwargs: Any) -> T
|
||||
| async update(**kwargs: Any) -> "Model"
|
||||
```
|
||||
|
||||
Performs update of Model instance in the database.
|
||||
@ -274,7 +166,7 @@ or update and the Model will be saved in database again.
|
||||
#### load
|
||||
|
||||
```python
|
||||
| async load() -> T
|
||||
| async load() -> "Model"
|
||||
```
|
||||
|
||||
Allow to refresh existing Models fields from database.
|
||||
@ -289,3 +181,40 @@ Does NOT refresh the related models fields if they were loaded before.
|
||||
|
||||
`(Model)`: reloaded Model
|
||||
|
||||
<a name="models.model.Model.load_all"></a>
|
||||
#### load\_all
|
||||
|
||||
```python
|
||||
| async load_all(follow: bool = False, exclude: Union[List, str, Set, Dict] = None) -> "Model"
|
||||
```
|
||||
|
||||
Allow to refresh existing Models fields from database.
|
||||
Performs refresh of the related models fields.
|
||||
|
||||
By default loads only self and the directly related ones.
|
||||
|
||||
If follow=True is set it loads also related models of related models.
|
||||
|
||||
To not get stuck in an infinite loop as related models also keep a relation
|
||||
to parent model visited models set is kept.
|
||||
|
||||
That way already visited models that are nested are loaded, but the load do not
|
||||
follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X
|
||||
will load second Model A but will never follow into Model X.
|
||||
Nested relations of those kind need to be loaded manually.
|
||||
|
||||
**Raises**:
|
||||
|
||||
- `NoMatch`: If given pk is not found in database.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `exclude ()`:
|
||||
- `follow (bool)`: flag to trigger deep save -
|
||||
by default only directly related models are saved
|
||||
with follow=True also related models of related models are saved
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Model)`: reloaded Model
|
||||
|
||||
|
||||
@ -146,7 +146,7 @@ Raises exception if model is abstract or has ForwardRefs in relation fields.
|
||||
#### \_extract\_related\_model\_instead\_of\_field
|
||||
|
||||
```python
|
||||
| _extract_related_model_instead_of_field(item: str) -> Optional[Union["T", Sequence["T"]]]
|
||||
| _extract_related_model_instead_of_field(item: str) -> Optional[Union["Model", Sequence["Model"]]]
|
||||
```
|
||||
|
||||
Retrieves the related model/models from RelationshipManager.
|
||||
@ -276,7 +276,7 @@ cause some dialect require different treatment
|
||||
#### remove
|
||||
|
||||
```python
|
||||
| remove(parent: "T", name: str) -> None
|
||||
| remove(parent: "Model", name: str) -> None
|
||||
```
|
||||
|
||||
Removes child from relation with given name in RelationshipManager
|
||||
|
||||
@ -22,11 +22,25 @@ Shortcut for ormar's model AliasManager stored on Meta.
|
||||
|
||||
`(AliasManager)`: alias manager from model's Meta
|
||||
|
||||
<a name="queryset.join.SqlJoin.on_clause"></a>
|
||||
#### on\_clause
|
||||
<a name="queryset.join.SqlJoin.to_table"></a>
|
||||
#### to\_table
|
||||
|
||||
```python
|
||||
| on_clause(previous_alias: str, from_clause: str, to_clause: str) -> text
|
||||
| @property
|
||||
| to_table() -> str
|
||||
```
|
||||
|
||||
Shortcut to table name of the next model
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(str)`: name of the target table
|
||||
|
||||
<a name="queryset.join.SqlJoin._on_clause"></a>
|
||||
#### \_on\_clause
|
||||
|
||||
```python
|
||||
| _on_clause(previous_alias: str, from_clause: str, to_clause: str) -> text
|
||||
```
|
||||
|
||||
Receives aliases and names of both ends of the join and combines them
|
||||
@ -99,11 +113,11 @@ Updated are:
|
||||
- `related_name (str)`: name of the relation to follow
|
||||
- `remainder (Any)`: deeper tables if there are more nested joins
|
||||
|
||||
<a name="queryset.join.SqlJoin.process_m2m_through_table"></a>
|
||||
#### process\_m2m\_through\_table
|
||||
<a name="queryset.join.SqlJoin._process_m2m_through_table"></a>
|
||||
#### \_process\_m2m\_through\_table
|
||||
|
||||
```python
|
||||
| process_m2m_through_table() -> None
|
||||
| _process_m2m_through_table() -> None
|
||||
```
|
||||
|
||||
Process Through table of the ManyToMany relation so that source table is
|
||||
@ -119,11 +133,11 @@ Replaces needed parameters like:
|
||||
|
||||
To point to through model
|
||||
|
||||
<a name="queryset.join.SqlJoin.process_m2m_related_name_change"></a>
|
||||
#### process\_m2m\_related\_name\_change
|
||||
<a name="queryset.join.SqlJoin._process_m2m_related_name_change"></a>
|
||||
#### \_process\_m2m\_related\_name\_change
|
||||
|
||||
```python
|
||||
| process_m2m_related_name_change(reverse: bool = False) -> str
|
||||
| _process_m2m_related_name_change(reverse: bool = False) -> str
|
||||
```
|
||||
|
||||
Extracts relation name to link join through the Through model declared on
|
||||
@ -158,74 +172,21 @@ Updates the used aliases list directly.
|
||||
|
||||
Process order_by causes for non m2m relations.
|
||||
|
||||
<a name="queryset.join.SqlJoin._replace_many_to_many_order_by_columns"></a>
|
||||
#### \_replace\_many\_to\_many\_order\_by\_columns
|
||||
<a name="queryset.join.SqlJoin._get_order_bys"></a>
|
||||
#### \_get\_order\_bys
|
||||
|
||||
```python
|
||||
| _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.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `part (str)`: name of the field with relation
|
||||
- `new_part (str)`: name of the target model
|
||||
|
||||
<a name="queryset.join.SqlJoin._check_if_condition_apply"></a>
|
||||
#### \_check\_if\_condition\_apply
|
||||
|
||||
```python
|
||||
| @staticmethod
|
||||
| _check_if_condition_apply(condition: List, part: str) -> bool
|
||||
```
|
||||
|
||||
Checks filter conditions to find if they apply to current join.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `condition (List[str])`: list of parts of condition split by '__'
|
||||
- `part (str)`: name of the current relation join.
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(bool)`: result of the check
|
||||
|
||||
<a name="queryset.join.SqlJoin.set_aliased_order_by"></a>
|
||||
#### set\_aliased\_order\_by
|
||||
|
||||
```python
|
||||
| set_aliased_order_by(condition: List[str], to_table: str) -> None
|
||||
```
|
||||
|
||||
Substitute hyphens ('-') with descending order.
|
||||
Construct actual sqlalchemy text clause using aliased table and column name.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `condition (List[str])`: list of parts of a current condition split by '__'
|
||||
- `to_table (sqlalchemy.sql.elements.quoted_name)`: target table
|
||||
|
||||
<a name="queryset.join.SqlJoin.get_order_bys"></a>
|
||||
#### get\_order\_bys
|
||||
|
||||
```python
|
||||
| get_order_bys(to_table: str, pkname_alias: str) -> None
|
||||
| _get_order_bys() -> None
|
||||
```
|
||||
|
||||
Triggers construction of order bys if they are given.
|
||||
Otherwise by default each table is sorted by a primary key column asc.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `to_table (sqlalchemy.sql.elements.quoted_name)`: target table
|
||||
- `pkname_alias (str)`: alias of the primary key column
|
||||
|
||||
<a name="queryset.join.SqlJoin.get_to_and_from_keys"></a>
|
||||
#### get\_to\_and\_from\_keys
|
||||
<a name="queryset.join.SqlJoin._get_to_and_from_keys"></a>
|
||||
#### \_get\_to\_and\_from\_keys
|
||||
|
||||
```python
|
||||
| get_to_and_from_keys() -> Tuple[str, str]
|
||||
| _get_to_and_from_keys() -> Tuple[str, str]
|
||||
```
|
||||
|
||||
Based on the relation type, name of the relation and previous models and parts
|
||||
|
||||
@ -1,26 +1,6 @@
|
||||
<a name="queryset.prefetch_query"></a>
|
||||
# queryset.prefetch\_query
|
||||
|
||||
<a name="queryset.prefetch_query.add_relation_field_to_fields"></a>
|
||||
#### add\_relation\_field\_to\_fields
|
||||
|
||||
```python
|
||||
add_relation_field_to_fields(fields: Union[Set[Any], Dict[Any, Any], None], related_field_name: str) -> Union[Set[Any], Dict[Any, Any], None]
|
||||
```
|
||||
|
||||
Adds related field into fields to include as otherwise it would be skipped.
|
||||
Related field is added only if fields are already populated.
|
||||
Empty fields implies all fields.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `fields (Dict)`: Union[Set[Any], Dict[Any, Any], None]
|
||||
- `related_field_name (str)`: name of the field with relation
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(Union[Set[Any], Dict[Any, Any], None])`: updated fields dict
|
||||
|
||||
<a name="queryset.prefetch_query.sort_models"></a>
|
||||
#### sort\_models
|
||||
|
||||
@ -232,7 +212,7 @@ on each of the parent models from list.
|
||||
#### \_extract\_related\_models
|
||||
|
||||
```python
|
||||
| async _extract_related_models(related: str, target_model: Type["Model"], prefetch_dict: Dict, select_dict: Dict, fields: Union[Set[Any], Dict[Any, Any], None], exclude_fields: Union[Set[Any], Dict[Any, Any], None], orders_by: Dict) -> None
|
||||
| async _extract_related_models(related: str, target_model: Type["Model"], prefetch_dict: Dict, select_dict: Dict, excludable: "ExcludableItems", orders_by: Dict) -> None
|
||||
```
|
||||
|
||||
Constructs queries with required ids and extracts data with fields that should
|
||||
@ -261,7 +241,7 @@ Calls itself recurrently to extract deeper nested relations of related model.
|
||||
#### \_run\_prefetch\_query
|
||||
|
||||
```python
|
||||
| async _run_prefetch_query(target_field: Type["BaseField"], fields: Union[Set[Any], Dict[Any, Any], None], exclude_fields: Union[Set[Any], Dict[Any, Any], None], filter_clauses: List) -> Tuple[str, List]
|
||||
| async _run_prefetch_query(target_field: Type["BaseField"], excludable: "ExcludableItems", filter_clauses: List, related_field_name: str) -> Tuple[str, str, List]
|
||||
```
|
||||
|
||||
Actually runs the queries against the database and populates the raw response
|
||||
@ -273,8 +253,6 @@ models.
|
||||
**Arguments**:
|
||||
|
||||
- `target_field (Type["BaseField"])`: ormar field with relation definition
|
||||
- `fields (Union[Set[Any], Dict[Any, Any], None])`: fields to include
|
||||
- `exclude_fields (Union[Set[Any], Dict[Any, Any], None])`: fields to exclude
|
||||
- `filter_clauses (List[sqlalchemy.sql.elements.TextClause])`: list of clauses, actually one clause with ids of relation
|
||||
|
||||
**Returns**:
|
||||
@ -320,7 +298,7 @@ Updates models that are already loaded, usually children of children.
|
||||
#### \_populate\_rows
|
||||
|
||||
```python
|
||||
| _populate_rows(rows: List, target_field: Type["ForeignKeyField"], parent_model: Type["Model"], table_prefix: str, fields: Union[Set[Any], Dict[Any, Any], None], exclude_fields: Union[Set[Any], Dict[Any, Any], None], prefetch_dict: Dict, orders_by: Dict) -> None
|
||||
| _populate_rows(rows: List, target_field: Type["ForeignKeyField"], parent_model: Type["Model"], table_prefix: str, exclude_prefix: str, excludable: "ExcludableItems", prefetch_dict: Dict, orders_by: Dict) -> None
|
||||
```
|
||||
|
||||
Instantiates children models extracted from given relation.
|
||||
@ -334,12 +312,11 @@ and set on the parent model after sorting if needed.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `excludable (ExcludableItems)`: structure of fields to include and exclude
|
||||
- `rows (List[sqlalchemy.engine.result.RowProxy])`: raw sql response from the prefetch query
|
||||
- `target_field (Type["BaseField"])`: field with relation definition from parent model
|
||||
- `parent_model (Type[Model])`: model with relation definition
|
||||
- `table_prefix (str)`: prefix of the target table from current relation
|
||||
- `fields (Union[Set[Any], Dict[Any, Any], None])`: fields to include
|
||||
- `exclude_fields (Union[Set[Any], Dict[Any, Any], None])`: fields to exclude
|
||||
- `prefetch_dict (Dict)`: dictionaries of related models to prefetch
|
||||
- `orders_by (Dict)`: dictionary of order by clauses by model
|
||||
|
||||
|
||||
@ -38,6 +38,16 @@ Shortcut to model class set on QuerySet.
|
||||
|
||||
`(Type[Model])`: model class
|
||||
|
||||
<a name="queryset.queryset.QuerySet.rebuild_self"></a>
|
||||
#### rebuild\_self
|
||||
|
||||
```python
|
||||
| rebuild_self(filter_clauses: List = None, exclude_clauses: List = None, select_related: List = None, limit_count: int = None, offset: int = None, excludable: "ExcludableItems" = None, order_bys: List = None, prefetch_related: List = None, limit_raw_sql: bool = None, proxy_source_model: Optional[Type["Model"]] = None) -> "QuerySet"
|
||||
```
|
||||
|
||||
Method that returns new instance of queryset based on passed params,
|
||||
all not passed params are taken from current values.
|
||||
|
||||
<a name="queryset.queryset.QuerySet._prefetch_related_models"></a>
|
||||
#### \_prefetch\_related\_models
|
||||
|
||||
@ -252,7 +262,7 @@ To chain related `Models` relation use double underscores between names.
|
||||
#### fields
|
||||
|
||||
```python
|
||||
| fields(columns: Union[List, str, Set, Dict]) -> "QuerySet"
|
||||
| fields(columns: Union[List, str, Set, Dict], _is_exclude: bool = False) -> "QuerySet"
|
||||
```
|
||||
|
||||
With `fields()` you can select subset of model columns to limit the data load.
|
||||
@ -293,6 +303,7 @@ To include whole nested model specify model related field name and ellipsis.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `_is_exclude (bool)`: flag if it's exclude or include operation
|
||||
- `columns (Union[List, str, Set, Dict])`: columns to include
|
||||
|
||||
**Returns**:
|
||||
|
||||
@ -17,38 +17,6 @@ class Query()
|
||||
|
||||
Initialize empty order_by dict to be populated later during the query call
|
||||
|
||||
<a name="queryset.query.Query.prefixed_pk_name"></a>
|
||||
#### prefixed\_pk\_name
|
||||
|
||||
```python
|
||||
| @property
|
||||
| prefixed_pk_name() -> str
|
||||
```
|
||||
|
||||
Shortcut for extracting prefixed with alias primary key column name from main
|
||||
model
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(str)`: alias of pk column prefix with table name.
|
||||
|
||||
<a name="queryset.query.Query.alias"></a>
|
||||
#### alias
|
||||
|
||||
```python
|
||||
| alias(name: str) -> str
|
||||
```
|
||||
|
||||
Shortcut to extracting column alias from given master model.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `name (str)`: name of column
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(str)`: alias of given column name
|
||||
|
||||
<a name="queryset.query.Query.apply_order_bys_for_primary_model"></a>
|
||||
#### apply\_order\_bys\_for\_primary\_model
|
||||
|
||||
|
||||
@ -154,7 +154,7 @@ with all children models under their relation keys.
|
||||
#### get\_relationship\_alias\_model\_and\_str
|
||||
|
||||
```python
|
||||
get_relationship_alias_model_and_str(source_model: Type["Model"], related_parts: List) -> Tuple[str, Type["Model"], str]
|
||||
get_relationship_alias_model_and_str(source_model: Type["Model"], related_parts: List) -> Tuple[str, Type["Model"], str, bool]
|
||||
```
|
||||
|
||||
Walks the relation to retrieve the actual model on which the clause should be
|
||||
|
||||
@ -120,7 +120,7 @@ Adds alias to the dictionary of aliases under given key.
|
||||
#### resolve\_relation\_alias
|
||||
|
||||
```python
|
||||
| resolve_relation_alias(from_model: Type["Model"], relation_name: str) -> str
|
||||
| resolve_relation_alias(from_model: Union[Type["Model"], Type["ModelRow"]], relation_name: str) -> str
|
||||
```
|
||||
|
||||
Given model and relation name returns the alias for this relation.
|
||||
@ -134,3 +134,24 @@ Given model and relation name returns the alias for this relation.
|
||||
|
||||
`(str)`: alias of the relation
|
||||
|
||||
<a name="relations.alias_manager.AliasManager.resolve_relation_alias_after_complex"></a>
|
||||
#### resolve\_relation\_alias\_after\_complex
|
||||
|
||||
```python
|
||||
| resolve_relation_alias_after_complex(source_model: Union[Type["Model"], Type["ModelRow"]], relation_str: str, relation_field: Type["ForeignKeyField"]) -> str
|
||||
```
|
||||
|
||||
Given source model and relation string returns the alias for this complex
|
||||
relation if it exists, otherwise fallback to normal relation from a relation
|
||||
field definition.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `relation_field (Type["ForeignKeyField"])`: field with direct relation definition
|
||||
- `source_model (source Model)`: model with query starts
|
||||
- `relation_str (str)`: string with relation joins defined
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(str)`: alias of the relation
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
## QuerysetProxy Objects
|
||||
|
||||
```python
|
||||
class QuerysetProxy(ormar.QuerySetProtocol)
|
||||
class QuerysetProxy()
|
||||
```
|
||||
|
||||
Exposes QuerySet methods on relations, but also handles creating and removing
|
||||
@ -43,7 +43,7 @@ Set's the queryset. Initialized in RelationProxy.
|
||||
#### \_assign\_child\_to\_parent
|
||||
|
||||
```python
|
||||
| _assign_child_to_parent(child: Optional["T"]) -> None
|
||||
| _assign_child_to_parent(child: Optional["Model"]) -> None
|
||||
```
|
||||
|
||||
Registers child in parents RelationManager.
|
||||
@ -56,7 +56,7 @@ Registers child in parents RelationManager.
|
||||
#### \_register\_related
|
||||
|
||||
```python
|
||||
| _register_related(child: Union["T", Sequence[Optional["T"]]]) -> None
|
||||
| _register_related(child: Union["Model", Sequence[Optional["Model"]]]) -> None
|
||||
```
|
||||
|
||||
Registers child/ children in parents RelationManager.
|
||||
@ -78,20 +78,35 @@ Cleans the current list of the related models.
|
||||
#### create\_through\_instance
|
||||
|
||||
```python
|
||||
| async create_through_instance(child: "T") -> None
|
||||
| async create_through_instance(child: "Model", **kwargs: Any) -> None
|
||||
```
|
||||
|
||||
Crete a through model instance in the database for m2m relations.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `kwargs (Any)`: dict of additional keyword arguments for through instance
|
||||
- `child (Model)`: child model instance
|
||||
|
||||
<a name="relations.querysetproxy.QuerysetProxy.update_through_instance"></a>
|
||||
#### update\_through\_instance
|
||||
|
||||
```python
|
||||
| async update_through_instance(child: "Model", **kwargs: Any) -> None
|
||||
```
|
||||
|
||||
Updates a through model instance in the database for m2m relations.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `kwargs (Any)`: dict of additional keyword arguments for through instance
|
||||
- `child (Model)`: child model instance
|
||||
|
||||
<a name="relations.querysetproxy.QuerysetProxy.delete_through_instance"></a>
|
||||
#### delete\_through\_instance
|
||||
|
||||
```python
|
||||
| async delete_through_instance(child: "T") -> None
|
||||
| async delete_through_instance(child: "Model") -> None
|
||||
```
|
||||
|
||||
Removes through model instance from the database for m2m relations.
|
||||
@ -256,6 +271,27 @@ Actual call delegated to QuerySet.
|
||||
|
||||
`(Model)`: created model
|
||||
|
||||
<a name="relations.querysetproxy.QuerysetProxy.update"></a>
|
||||
#### update
|
||||
|
||||
```python
|
||||
| async update(each: bool = False, **kwargs: Any) -> int
|
||||
```
|
||||
|
||||
Updates the model table after applying the filters from kwargs.
|
||||
|
||||
You have to either pass a filter to narrow down a query or explicitly pass
|
||||
each=True flag to affect whole table.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `each (bool)`: flag if whole table should be affected if no filter is passed
|
||||
- `kwargs (Any)`: fields names and proper value types
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(int)`: number of updated rows
|
||||
|
||||
<a name="relations.querysetproxy.QuerysetProxy.get_or_create"></a>
|
||||
#### get\_or\_create
|
||||
|
||||
|
||||
@ -10,37 +10,6 @@ class RelationsManager()
|
||||
|
||||
Manages relations on a Model, each Model has it's own instance.
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager._get_relation_type"></a>
|
||||
#### \_get\_relation\_type
|
||||
|
||||
```python
|
||||
| _get_relation_type(field: Type[BaseField]) -> RelationType
|
||||
```
|
||||
|
||||
Returns type of the relation declared on a field.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (Type[BaseField])`: field with relation declaration
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(RelationType)`: type of the relation defined on field
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager._add_relation"></a>
|
||||
#### \_add\_relation
|
||||
|
||||
```python
|
||||
| _add_relation(field: Type[BaseField]) -> None
|
||||
```
|
||||
|
||||
Registers relation in the manager.
|
||||
Adds Relation instance under field.name.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (Type[BaseField])`: field with relation declaration
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager.__contains__"></a>
|
||||
#### \_\_contains\_\_
|
||||
|
||||
@ -62,7 +31,7 @@ Checks if relation with given name is already registered.
|
||||
#### get
|
||||
|
||||
```python
|
||||
| get(name: str) -> Optional[Union["T", Sequence["T"]]]
|
||||
| get(name: str) -> Optional[Union["Model", Sequence["Model"]]]
|
||||
```
|
||||
|
||||
Returns the related model/models if relation is set.
|
||||
@ -76,23 +45,6 @@ Actual call is delegated to Relation instance registered under relation name.
|
||||
|
||||
`(Optional[Union[Model, List[Model]])`: related model or list of related models if set
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager._get"></a>
|
||||
#### \_get
|
||||
|
||||
```python
|
||||
| _get(name: str) -> Optional[Relation]
|
||||
```
|
||||
|
||||
Returns the actual relation and not the related model(s).
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `name (str)`: name of the relation
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(ormar.relations.relation.Relation)`: Relation instance
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager.add"></a>
|
||||
#### add
|
||||
|
||||
@ -148,3 +100,51 @@ of relation from which you want to remove the parent.
|
||||
- `parent (Model)`: parent Model
|
||||
- `name (str)`: name of the relation
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager._get"></a>
|
||||
#### \_get
|
||||
|
||||
```python
|
||||
| _get(name: str) -> Optional[Relation]
|
||||
```
|
||||
|
||||
Returns the actual relation and not the related model(s).
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `name (str)`: name of the relation
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(ormar.relations.relation.Relation)`: Relation instance
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager._get_relation_type"></a>
|
||||
#### \_get\_relation\_type
|
||||
|
||||
```python
|
||||
| _get_relation_type(field: Type["BaseField"]) -> RelationType
|
||||
```
|
||||
|
||||
Returns type of the relation declared on a field.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (Type[BaseField])`: field with relation declaration
|
||||
|
||||
**Returns**:
|
||||
|
||||
`(RelationType)`: type of the relation defined on field
|
||||
|
||||
<a name="relations.relation_manager.RelationsManager._add_relation"></a>
|
||||
#### \_add\_relation
|
||||
|
||||
```python
|
||||
| _add_relation(field: Type["BaseField"]) -> None
|
||||
```
|
||||
|
||||
Registers relation in the manager.
|
||||
Adds Relation instance under field.name.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `field (Type[BaseField])`: field with relation declaration
|
||||
|
||||
|
||||
@ -131,7 +131,7 @@ will be deleted, and not only removed from relation).
|
||||
#### add
|
||||
|
||||
```python
|
||||
| async add(item: "Model") -> None
|
||||
| async add(item: "Model", **kwargs: Any) -> None
|
||||
```
|
||||
|
||||
Adds child model to relation.
|
||||
@ -140,5 +140,6 @@ For ManyToMany relations through instance is automatically created.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `kwargs (Any)`: dict of additional keyword arguments for through instance
|
||||
- `item (Model)`: child to add to relation
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ Keeps related Models and handles adding/removing of the children.
|
||||
#### \_\_init\_\_
|
||||
|
||||
```python
|
||||
| __init__(manager: "RelationsManager", type_: RelationType, field_name: str, to: Type["T"], through: Type["T"] = None) -> None
|
||||
| __init__(manager: "RelationsManager", type_: RelationType, field_name: str, to: Type["Model"], through: Type["Model"] = None) -> None
|
||||
```
|
||||
|
||||
Initialize the Relation and keep the related models either as instances of
|
||||
@ -73,7 +73,7 @@ Find child model in RelationProxy if exists.
|
||||
#### add
|
||||
|
||||
```python
|
||||
| add(child: "T") -> None
|
||||
| add(child: "Model") -> None
|
||||
```
|
||||
|
||||
Adds child Model to relation, either sets child as related model or adds
|
||||
@ -101,7 +101,7 @@ it from the list in RelationProxy depending on relation type.
|
||||
#### get
|
||||
|
||||
```python
|
||||
| get() -> Optional[Union[List["T"], "T"]]
|
||||
| get() -> Optional[Union[List["Model"], "Model"]]
|
||||
```
|
||||
|
||||
Return the related model or models from RelationProxy.
|
||||
|
||||
@ -306,7 +306,7 @@ async def joins():
|
||||
# visit: https://collerek.github.io/ormar/relations/
|
||||
|
||||
# to read more about joins and subqueries
|
||||
# visit: https://collerek.github.io/ormar/queries/delete/
|
||||
# visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/
|
||||
|
||||
|
||||
async def filter_and_sort():
|
||||
|
||||
@ -27,6 +27,39 @@ await track.album.load()
|
||||
track.album.name # will return 'Malibu'
|
||||
```
|
||||
|
||||
## load_all
|
||||
|
||||
`load_all(follow: bool = False, exclude: Union[List, str, Set, Dict] = None) -> Model`
|
||||
|
||||
Method works like `load()` but also goes through all relations of the `Model` on which the method is called,
|
||||
and reloads them from database.
|
||||
|
||||
By default the `load_all` method loads only models that are directly related (one step away) to the model on which the method is called.
|
||||
|
||||
But you can specify the `follow=True` parameter to traverse through nested models and load all of them in the relation tree.
|
||||
|
||||
!!!warning
|
||||
To avoid circular updates with `follow=True` set, `load_all` keeps a set of already visited Models,
|
||||
and won't perform nested `loads` on Models that were already visited.
|
||||
|
||||
So if you have a diamond or circular relations types you need to perform the loads in a manual way.
|
||||
|
||||
```python
|
||||
# in example like this the second Street (coming from City) won't be load_all, so ZipCode won't be reloaded
|
||||
Street -> District -> City -> Street -> ZipCode
|
||||
```
|
||||
|
||||
Method accepts also optional exclude parameter that works exactly the same as exclude_fields method in `QuerySet`.
|
||||
That way you can remove fields from related models being refreshed or skip whole related models.
|
||||
|
||||
Method performs one database query so it's more efficient than nested calls to `load()` and `all()` on related models.
|
||||
|
||||
!!!tip
|
||||
To read more about `exclude` read [exclude_fields][exclude_fields]
|
||||
|
||||
!!!warning
|
||||
All relations are cleared on `load_all()`, so if you exclude some nested models they will be empty after call.
|
||||
|
||||
## save
|
||||
|
||||
`save() -> self`
|
||||
@ -128,3 +161,4 @@ But you can specify the `follow=True` parameter to traverse through nested model
|
||||
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
||||
[save status]: ../models/index/#model-save-status
|
||||
[Internals]: #internals
|
||||
[exclude_fields]: ../queries/select-columns.md#exclude_fields
|
||||
|
||||
@ -52,7 +52,7 @@ class Department(ormar.Model):
|
||||
|
||||
To define many-to-many relation use `ManyToMany` field.
|
||||
|
||||
```python hl_lines="25-26"
|
||||
```python hl_lines="18"
|
||||
class Category(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "categories"
|
||||
@ -62,13 +62,6 @@ class Category(ormar.Model):
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=40)
|
||||
|
||||
# note: you need to specify through model
|
||||
class PostCategory(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "posts_categories"
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "posts"
|
||||
@ -77,9 +70,7 @@ class Post(ormar.Model):
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
|
||||
Category, through=PostCategory
|
||||
)
|
||||
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
|
||||
```
|
||||
|
||||
|
||||
@ -92,7 +83,52 @@ class Post(ormar.Model):
|
||||
|
||||
It allows you to use `await post.categories.all()` but also `await category.posts.all()` to fetch data related only to specific post, category etc.
|
||||
|
||||
##Self-reference and postponed references
|
||||
## Through fields
|
||||
|
||||
As part of the `ManyToMany` relation you can define a through model, that can contain additional
|
||||
fields that you can use to filter, order etc. Fields defined like this are exposed on the reverse
|
||||
side of the current query for m2m models.
|
||||
|
||||
So if you query from model `A` to model `B`, only model `B` has through field exposed.
|
||||
Which kind of make sense, since it's a one through model/field for each of related models.
|
||||
|
||||
```python hl_lines="10-15"
|
||||
class Category(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "categories"
|
||||
|
||||
id = ormar.Integer(primary_key=True)
|
||||
name = ormar.String(max_length=40)
|
||||
|
||||
# you can specify additional fields on through model
|
||||
class PostCategory(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "posts_x_categories"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
sort_order: int = ormar.Integer(nullable=True)
|
||||
param_name: str = ormar.String(default="Name", max_length=200)
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
pass
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories = ormar.ManyToMany(Category, through=PostCategory)
|
||||
```
|
||||
|
||||
!!!tip
|
||||
To read more about many-to-many relations and through fields visit [many-to-many][many-to-many] section
|
||||
|
||||
|
||||
!!!tip
|
||||
ManyToMany allows you to query the related models with [queryset-proxy][queryset-proxy].
|
||||
|
||||
It allows you to use `await post.categories.all()` but also `await category.posts.all()` to fetch data related only to specific post, category etc.
|
||||
|
||||
## Self-reference and postponed references
|
||||
|
||||
In order to create auto-relation or create two models that reference each other in at least two
|
||||
different relations (remember the reverse side is auto-registered for you), you need to use
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# ManyToMany
|
||||
|
||||
`ManyToMany(to, through)` has required parameters `to` and `through` that takes target and relation `Model` classes.
|
||||
`ManyToMany(to, through)` has required parameters `to` and optional `through` that takes target and relation `Model` classes.
|
||||
|
||||
Sqlalchemy column and Type are automatically taken from target `Model`.
|
||||
|
||||
@ -9,7 +9,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`.
|
||||
|
||||
## Defining Models
|
||||
|
||||
```Python hl_lines="32 49-50"
|
||||
```Python hl_lines="40"
|
||||
--8<-- "../docs_src/relations/docs002.py"
|
||||
```
|
||||
|
||||
@ -20,8 +20,154 @@ post = await Post.objects.create(title="Hello, M2M", author=guido)
|
||||
news = await Category.objects.create(name="News")
|
||||
```
|
||||
|
||||
## Through Model
|
||||
|
||||
Optionally if you want to add additional fields you can explicitly create and pass
|
||||
the through model class.
|
||||
|
||||
```Python hl_lines="14-20 29"
|
||||
--8<-- "../docs_src/relations/docs004.py"
|
||||
```
|
||||
|
||||
!!!warning
|
||||
Note that even of you do not provide through model it's going to be created for you automatically and
|
||||
still has to be included in example in `alembic` migrations.
|
||||
|
||||
!!!tip
|
||||
Note that you need to provide `through` model if you want to
|
||||
customize the `Through` model name or the database table name of this model.
|
||||
|
||||
If you do not provide the Through field it will be generated for you.
|
||||
|
||||
The default naming convention is:
|
||||
|
||||
* for class name it's union of both classes name (parent+other) so in example above
|
||||
it would be `PostCategory`
|
||||
* for table name it similar but with underscore in between and s in the end of class
|
||||
lowercase name, in example above would be `posts_categorys`
|
||||
|
||||
## Through Fields
|
||||
|
||||
The through field is auto added to the reverse side of the relation.
|
||||
|
||||
The exposed field is named as lowercase `Through` class name.
|
||||
|
||||
The exposed field **explicitly has no relations loaded** as the relation is already populated in `ManyToMany` field,
|
||||
so it's useful only when additional fields are provided on `Through` model.
|
||||
|
||||
In a sample model setup as following:
|
||||
|
||||
```Python hl_lines="14-20 29"
|
||||
--8<-- "../docs_src/relations/docs004.py"
|
||||
```
|
||||
|
||||
the through field can be used as a normal model field in most of the QuerySet operations.
|
||||
|
||||
Note that through field is attached only to related side of the query so:
|
||||
|
||||
```python
|
||||
post = await Post.objects.select_related("categories").get()
|
||||
# source model has no through field
|
||||
assert post.postcategory is None
|
||||
# related models have through field
|
||||
assert post.categories[0].postcategory is not None
|
||||
|
||||
# same is applicable for reversed query
|
||||
category = await Category.objects.select_related("posts").get()
|
||||
assert category.postcategory is None
|
||||
assert category.posts[0].postcategory is not None
|
||||
```
|
||||
|
||||
Through field can be used for filtering the data.
|
||||
```python
|
||||
post = (
|
||||
await Post.objects.select_related("categories")
|
||||
.filter(postcategory__sort_order__gt=1)
|
||||
.get()
|
||||
)
|
||||
```
|
||||
|
||||
!!!tip
|
||||
Note that despite that the actual instance is not populated on source model,
|
||||
in queries, order by statements etc you can access through model from both sides.
|
||||
So below query has exactly the same effect (note access through `categories`)
|
||||
|
||||
```python
|
||||
post = (
|
||||
await Post.objects.select_related("categories")
|
||||
.filter(categories__postcategory__sort_order__gt=1)
|
||||
.get()
|
||||
)
|
||||
```
|
||||
|
||||
Through model can be used in order by queries.
|
||||
```python
|
||||
post = (
|
||||
await Post.objects.select_related("categories")
|
||||
.order_by("-postcategory__sort_order")
|
||||
.get()
|
||||
)
|
||||
```
|
||||
|
||||
You can also select subset of the columns in a normal `QuerySet` way with `fields`
|
||||
and `exclude_fields`.
|
||||
|
||||
```python
|
||||
post2 = (
|
||||
await Post.objects.select_related("categories")
|
||||
.exclude_fields("postcategory__param_name")
|
||||
.get()
|
||||
)
|
||||
```
|
||||
|
||||
!!!warning
|
||||
Note that because through fields explicitly nullifies all relation fields, as relation
|
||||
is populated in ManyToMany field, you should not use the standard model methods like
|
||||
`save()` and `update()` before re-loading the field from database.
|
||||
|
||||
If you want to modify the through field in place remember to reload it from database.
|
||||
Otherwise you will set relations to None so effectively make the field useless!
|
||||
|
||||
```python
|
||||
# always reload the field before modification
|
||||
await post2.categories[0].postcategory.load()
|
||||
# only then update the field
|
||||
await post2.categories[0].postcategory.update(sort_order=3)
|
||||
```
|
||||
Note that reloading the model effectively reloads the relations as `pk_only` models
|
||||
(only primary key is set) so they are not fully populated, but it's enough to preserve
|
||||
the relation on update.
|
||||
|
||||
!!!warning
|
||||
If you use i.e. `fastapi` the partially loaded related models on through field might cause
|
||||
`pydantic` validation errors (that's the primary reason why they are not populated by default).
|
||||
So either you need to exclude the related fields in your response, or fully load the related
|
||||
models. In example above it would mean:
|
||||
```python
|
||||
await post2.categories[0].postcategory.post.load()
|
||||
await post2.categories[0].postcategory.category.load()
|
||||
```
|
||||
Alternatively you can use `load_all()`:
|
||||
```python
|
||||
await post2.categories[0].postcategory.load_all()
|
||||
```
|
||||
|
||||
**Preferred way of update is through queryset proxy `update()` method**
|
||||
|
||||
```python
|
||||
# filter the desired related model with through field and update only through field params
|
||||
await post2.categories.filter(name='Test category').update(postcategory={"sort_order": 3})
|
||||
```
|
||||
|
||||
|
||||
## Relation methods
|
||||
|
||||
### add
|
||||
|
||||
`add(item: Model, **kwargs)`
|
||||
|
||||
Allows you to add model to ManyToMany relation.
|
||||
|
||||
```python
|
||||
# Add a category to a post.
|
||||
await post.categories.add(news)
|
||||
@ -30,10 +176,24 @@ await news.posts.add(post)
|
||||
```
|
||||
|
||||
!!!warning
|
||||
In all not None cases the primary key value for related model **has to exist in database**.
|
||||
In all not `None` cases the primary key value for related model **has to exist in database**.
|
||||
|
||||
Otherwise an IntegrityError will be raised by your database driver library.
|
||||
|
||||
If you declare your models with a Through model with additional fields, you can populate them
|
||||
during adding child model to relation.
|
||||
|
||||
In order to do so, pass keyword arguments with field names and values to `add()` call.
|
||||
|
||||
Note that this works only for `ManyToMany` relations.
|
||||
|
||||
```python
|
||||
post = await Post(title="Test post").save()
|
||||
category = await Category(name="Test category").save()
|
||||
# apart from model pass arguments referencing through model fields
|
||||
await post.categories.add(category, sort_order=1, param_name='test')
|
||||
```
|
||||
|
||||
### remove
|
||||
|
||||
Removal of the related model one by one.
|
||||
|
||||
@ -104,6 +104,29 @@ assert len(await post.categories.all()) == 2
|
||||
!!!tip
|
||||
Read more in queries documentation [create][create]
|
||||
|
||||
For `ManyToMany` relations there is an additional functionality of passing parameters
|
||||
that will be used to create a through model if you declared additional fields on explicitly
|
||||
provided Through model.
|
||||
|
||||
Given sample like this:
|
||||
|
||||
```Python hl_lines="14-20, 29"
|
||||
--8<-- "../docs_src/relations/docs004.py"
|
||||
```
|
||||
|
||||
You can populate fields on through model in the `create()` call in a following way:
|
||||
|
||||
```python
|
||||
|
||||
post = await Post(title="Test post").save()
|
||||
await post.categories.create(
|
||||
name="Test category1",
|
||||
# in arguments pass a dictionary with name of the through field and keys
|
||||
# corresponding to through model fields
|
||||
postcategory={"sort_order": 1, "param_name": "volume"},
|
||||
)
|
||||
```
|
||||
|
||||
### get_or_create
|
||||
|
||||
`get_or_create(**kwargs) -> Model`
|
||||
@ -122,6 +145,29 @@ Updates the model, or in case there is no match in database creates a new one.
|
||||
!!!tip
|
||||
Read more in queries documentation [update_or_create][update_or_create]
|
||||
|
||||
### update
|
||||
|
||||
`update(**kwargs, each:bool = False) -> int`
|
||||
|
||||
Updates the related model with provided keyword arguments, return number of updated rows.
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [update][update]
|
||||
|
||||
Note that for `ManyToMany` relations update can also accept an argument with through field
|
||||
name and a dictionary of fields.
|
||||
|
||||
```Python hl_lines="14-20 29"
|
||||
--8<-- "../docs_src/relations/docs004.py"
|
||||
```
|
||||
|
||||
In example above you can update attributes of `postcategory` in a following call:
|
||||
```python
|
||||
await post.categories.filter(name="Test category3").update(
|
||||
postcategory={"sort_order": 4}
|
||||
)
|
||||
```
|
||||
|
||||
## Filtering and sorting
|
||||
|
||||
### filter
|
||||
@ -251,6 +297,7 @@ Returns a bool value to confirm if there are rows matching the given criteria (a
|
||||
[create]: ../queries/create.md#create
|
||||
[get_or_create]: ../queries/read.md#get_or_create
|
||||
[update_or_create]: ../queries/update.md#update_or_create
|
||||
[update]: ../queries/update.md#update
|
||||
[filter]: ../queries/filter-and-sort.md#filter
|
||||
[exclude]: ../queries/filter-and-sort.md#exclude
|
||||
[select_related]: ../queries/joins-and-subqueries.md#select_related
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
# 0.9.5
|
||||
# 0.9.6
|
||||
|
||||
##Important
|
||||
* `Through` model for `ManyToMany` relations now **becomes optional**. It's not a breaking change
|
||||
since if you provide it everything works just fine as it used to. So if you don't want or need any additional
|
||||
fields on `Through` model you can skip it. Note that it's going to be created for you automatically and
|
||||
still has to be included in example in `alembic` migrations.
|
||||
If you want to delete existing one check the default naming convention to adjust your existing database structure.
|
||||
|
||||
Note that you still need to provide it if you want to
|
||||
customize the `Through` model name or the database table name.
|
||||
|
||||
## Features
|
||||
* Add `update` method to `QuerysetProxy` so now it's possible to update related models directly from parent model
|
||||
in `ManyToMany` relations and in reverse `ForeignKey` relations. Note that update like in `QuerySet` `update` returns number of
|
||||
updated models and **does not update related models in place** on praent model. To get the refreshed data on parent model you need to refresh
|
||||
updated models and **does not update related models in place** on parent model. To get the refreshed data on parent model you need to refresh
|
||||
the related models (i.e. `await model_instance.related.all()`)
|
||||
* Added possibility to add more fields on `Through` model for `ManyToMany` relationships:
|
||||
* name of the through model field is the lowercase name of the Through class
|
||||
@ -14,13 +24,21 @@
|
||||
* you can filter on through model fields
|
||||
* you can include and exclude fields on through models
|
||||
* through models are attached only to related models (i.e. if you query from A to B -> only on B)
|
||||
* check the updated docs for more information
|
||||
* note that through models are explicitly loaded without relations -> relation is already populated in ManyToMany field.
|
||||
* note that just like before you cannot declare the relation fields on through model, they will be populated for you by `ormar`
|
||||
* check the updated ManyToMany relation docs for more information
|
||||
|
||||
# Other
|
||||
* Updated docs and api docs
|
||||
* Refactors and optimisations mainly related to filters and order bys
|
||||
* Refactors and optimisations mainly related to filters, exclusions and order bys
|
||||
|
||||
|
||||
# 0.9.5
|
||||
|
||||
## Fixes
|
||||
* Fix creation of `pydantic` FieldInfo after update of `pydantic` to version >=1.8
|
||||
* Pin required dependency versions to avoid such situations in the future
|
||||
|
||||
|
||||
# 0.9.4
|
||||
|
||||
|
||||
@ -29,15 +29,6 @@ class Category(ormar.Model):
|
||||
name: str = ormar.String(max_length=40)
|
||||
|
||||
|
||||
class PostCategory(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "posts_categories"
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
# If there are no additional columns id will be created automatically as Integer
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "posts"
|
||||
@ -46,7 +37,5 @@ class Post(ormar.Model):
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
|
||||
Category, through=PostCategory
|
||||
)
|
||||
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
|
||||
author: Optional[Author] = ormar.ForeignKey(Author)
|
||||
|
||||
29
docs_src/relations/docs004.py
Normal file
29
docs_src/relations/docs004.py
Normal file
@ -0,0 +1,29 @@
|
||||
class BaseMeta(ormar.ModelMeta):
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
|
||||
class Category(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "categories"
|
||||
|
||||
id = ormar.Integer(primary_key=True)
|
||||
name = ormar.String(max_length=40)
|
||||
|
||||
|
||||
class PostCategory(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "posts_x_categories"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
sort_order: int = ormar.Integer(nullable=True)
|
||||
param_name: str = ormar.String(default="Name", max_length=200)
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
pass
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories = ormar.ManyToMany(Category, through=PostCategory)
|
||||
@ -53,9 +53,11 @@ nav:
|
||||
- Relation Mixin: api/models/mixins/relation-mixin.md
|
||||
- Save Prepare Mixin: api/models/mixins/save-prepare-mixin.md
|
||||
- api/models/model.md
|
||||
- Model Row: api/models/model-row.md
|
||||
- New BaseModel: api/models/new-basemodel.md
|
||||
- Model Table Proxy: api/models/model-table-proxy.md
|
||||
- Model Metaclass: api/models/model-metaclass.md
|
||||
- Excludable Items: api/models/excludable-items.md
|
||||
- Fields:
|
||||
- Base Field: api/fields/base-field.md
|
||||
- Model Fields: api/fields/model-fields.md
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import sys
|
||||
from typing import Any, List, Optional, TYPE_CHECKING, Tuple, Type, Union
|
||||
from typing import Any, List, Optional, TYPE_CHECKING, Tuple, Type, Union, cast
|
||||
|
||||
from pydantic.typing import ForwardRef, evaluate_forwardref
|
||||
import ormar # noqa: I100
|
||||
@ -43,7 +43,7 @@ def populate_m2m_params_based_on_to_model(
|
||||
|
||||
def ManyToMany(
|
||||
to: "ToType",
|
||||
through: "ToType",
|
||||
through: Optional["ToType"] = None,
|
||||
*,
|
||||
name: str = None,
|
||||
unique: bool = False,
|
||||
@ -212,3 +212,21 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro
|
||||
:rtype: Type["Model"]
|
||||
"""
|
||||
return cls.through
|
||||
|
||||
@classmethod
|
||||
def create_default_through_model(cls) -> None:
|
||||
"""
|
||||
Creates default empty through model if no additional fields are required.
|
||||
"""
|
||||
owner_name = cls.owner.get_name(lower=False)
|
||||
to_name = cls.to.get_name(lower=False)
|
||||
class_name = f"{owner_name}{to_name}"
|
||||
table_name = f"{owner_name.lower()}s_{to_name.lower()}s"
|
||||
new_meta_namespace = {
|
||||
"tablename": table_name,
|
||||
"database": cls.owner.Meta.database,
|
||||
"metadata": cls.owner.Meta.metadata,
|
||||
}
|
||||
new_meta = type("Meta", (), new_meta_namespace)
|
||||
through_model = type(class_name, (ormar.Model,), {"Meta": new_meta})
|
||||
cls.through = cast(Type["Model"], through_model)
|
||||
|
||||
@ -154,6 +154,8 @@ def sqlalchemy_columns_from_model_fields(
|
||||
pkname = None
|
||||
for field_name, field in model_fields.items():
|
||||
field.owner = new_model
|
||||
if field.is_multi and not field.through:
|
||||
field.create_default_through_model()
|
||||
if field.primary_key:
|
||||
pkname = check_pk_column_validity(field_name, field, pkname)
|
||||
if not field.pydantic_only and not field.virtual and not field.is_multi:
|
||||
|
||||
@ -209,13 +209,14 @@ def update_attrs_from_base_meta( # noqa: CCR001
|
||||
setattr(attrs["Meta"], param, parent_value)
|
||||
|
||||
|
||||
def copy_and_replace_m2m_through_model(
|
||||
def copy_and_replace_m2m_through_model( # noqa: CFQ002
|
||||
field: Type[ManyToManyField],
|
||||
field_name: str,
|
||||
table_name: str,
|
||||
parent_fields: Dict,
|
||||
attrs: Dict,
|
||||
meta: ModelMeta,
|
||||
base_class: Type["Model"],
|
||||
) -> None:
|
||||
"""
|
||||
Clones class with Through model for m2m relations, appends child name to the name
|
||||
@ -229,6 +230,8 @@ def copy_and_replace_m2m_through_model(
|
||||
|
||||
Removes the original sqlalchemy table from metadata if it was not removed.
|
||||
|
||||
:param base_class: base class model
|
||||
:type base_class: Type["Model"]
|
||||
:param field: field with relations definition
|
||||
:type field: Type[ManyToManyField]
|
||||
:param field_name: name of the relation field
|
||||
@ -249,6 +252,10 @@ def copy_and_replace_m2m_through_model(
|
||||
copy_field.related_name = related_name # type: ignore
|
||||
|
||||
through_class = field.through
|
||||
if not through_class:
|
||||
field.owner = base_class
|
||||
field.create_default_through_model()
|
||||
through_class = field.through
|
||||
new_meta: ormar.ModelMeta = type( # type: ignore
|
||||
"Meta", (), dict(through_class.Meta.__dict__),
|
||||
)
|
||||
@ -338,6 +345,7 @@ def copy_data_from_parent_model( # noqa: CCR001
|
||||
parent_fields=parent_fields,
|
||||
attrs=attrs,
|
||||
meta=meta,
|
||||
base_class=base_class, # type: ignore
|
||||
)
|
||||
|
||||
elif field.is_relation and field.related_name:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
from typing import List, Sequence, TYPE_CHECKING
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
import ormar
|
||||
|
||||
@ -17,7 +17,7 @@ class MergeModelMixin:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
|
||||
def merge_instances_list(cls, result_rows: List["Model"]) -> List["Model"]:
|
||||
"""
|
||||
Merges a list of models into list of unique models.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import inspect
|
||||
from typing import List, Optional, Set, TYPE_CHECKING
|
||||
from typing import List, Optional, Set, TYPE_CHECKING, Type, Union
|
||||
|
||||
|
||||
class RelationMixin:
|
||||
@ -8,7 +8,7 @@ class RelationMixin:
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING: # pragma no cover
|
||||
from ormar import ModelMeta
|
||||
from ormar import ModelMeta, Model
|
||||
|
||||
Meta: ModelMeta
|
||||
_related_names: Optional[Set]
|
||||
@ -63,7 +63,7 @@ class RelationMixin:
|
||||
return related_fields
|
||||
|
||||
@classmethod
|
||||
def extract_related_names(cls) -> Set:
|
||||
def extract_related_names(cls) -> Set[str]:
|
||||
"""
|
||||
Returns List of fields names for all relations declared on a model.
|
||||
List is cached in cls._related_names for quicker access.
|
||||
@ -118,3 +118,50 @@ class RelationMixin:
|
||||
name for name in related_names if cls.Meta.model_fields[name].nullable
|
||||
}
|
||||
return related_names
|
||||
|
||||
@classmethod
|
||||
def _iterate_related_models(
|
||||
cls,
|
||||
visited: Set[Union[Type["Model"], Type["RelationMixin"]]] = None,
|
||||
source_relation: str = None,
|
||||
source_model: Union[Type["Model"], Type["RelationMixin"]] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Iterates related models recursively to extract relation strings of
|
||||
nested not visited models.
|
||||
|
||||
:param visited: set of already visited models
|
||||
:type visited: Set[str]
|
||||
:param source_relation: name of the current relation
|
||||
:type source_relation: str
|
||||
:param source_model: model from which relation comes in nested relations
|
||||
:type source_model: Type["Model"]
|
||||
:return: list of relation strings to be passed to select_related
|
||||
:rtype: List[str]
|
||||
"""
|
||||
visited = visited or set()
|
||||
visited.add(cls)
|
||||
relations = cls.extract_related_names()
|
||||
processed_relations = []
|
||||
for relation in relations:
|
||||
target_model = cls.Meta.model_fields[relation].to
|
||||
if source_model and target_model == source_model:
|
||||
continue
|
||||
if target_model not in visited:
|
||||
visited.add(target_model)
|
||||
deep_relations = target_model._iterate_related_models(
|
||||
visited=visited, source_relation=relation, source_model=cls
|
||||
)
|
||||
processed_relations.extend(deep_relations)
|
||||
# TODO add test for circular deps
|
||||
else: # pragma: no cover
|
||||
processed_relations.append(relation)
|
||||
if processed_relations:
|
||||
final_relations = [
|
||||
f"{source_relation + '__' if source_relation else ''}{relation}"
|
||||
for relation in processed_relations
|
||||
]
|
||||
else:
|
||||
final_relations = [source_relation] if source_relation else []
|
||||
|
||||
return final_relations
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import ormar.queryset # noqa I100
|
||||
@ -265,3 +268,45 @@ class Model(ModelRow):
|
||||
self.update_from_dict(kwargs)
|
||||
self.set_save_status(True)
|
||||
return self
|
||||
|
||||
async def load_all(
|
||||
self, follow: bool = False, exclude: Union[List, str, Set, Dict] = None
|
||||
) -> "Model":
|
||||
"""
|
||||
Allow to refresh existing Models fields from database.
|
||||
Performs refresh of the related models fields.
|
||||
|
||||
By default loads only self and the directly related ones.
|
||||
|
||||
If follow=True is set it loads also related models of related models.
|
||||
|
||||
To not get stuck in an infinite loop as related models also keep a relation
|
||||
to parent model visited models set is kept.
|
||||
|
||||
That way already visited models that are nested are loaded, but the load do not
|
||||
follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X
|
||||
will load second Model A but will never follow into Model X.
|
||||
Nested relations of those kind need to be loaded manually.
|
||||
|
||||
:raises NoMatch: If given pk is not found in database.
|
||||
|
||||
:param exclude: related models to exclude
|
||||
:type exclude: Union[List, str, Set, Dict]
|
||||
:param follow: flag to trigger deep save -
|
||||
by default only directly related models are saved
|
||||
with follow=True also related models of related models are saved
|
||||
:type follow: bool
|
||||
:return: reloaded Model
|
||||
:rtype: Model
|
||||
"""
|
||||
relations = list(self.extract_related_names())
|
||||
if follow:
|
||||
relations = self._iterate_related_models()
|
||||
queryset = self.__class__.objects
|
||||
print(relations)
|
||||
if exclude:
|
||||
queryset = queryset.exclude_fields(exclude)
|
||||
instance = await queryset.select_related(relations).get(pk=self.pk)
|
||||
self._orm.clear()
|
||||
self.update_from_dict(instance.dict())
|
||||
return self
|
||||
|
||||
@ -88,6 +88,7 @@ class ModelRow(NewBaseModel):
|
||||
current_relation_str=current_relation_str,
|
||||
source_model=source_model, # type: ignore
|
||||
proxy_source_model=proxy_source_model, # type: ignore
|
||||
table_prefix=table_prefix,
|
||||
)
|
||||
item = cls.extract_prefixed_table_columns(
|
||||
item=item, row=row, table_prefix=table_prefix, excludable=excludable
|
||||
@ -110,6 +111,7 @@ class ModelRow(NewBaseModel):
|
||||
source_model: Type["Model"],
|
||||
related_models: Any,
|
||||
excludable: ExcludableItems,
|
||||
table_prefix: str,
|
||||
current_relation_str: str = None,
|
||||
proxy_source_model: Type["Model"] = None,
|
||||
) -> dict:
|
||||
@ -143,15 +145,20 @@ class ModelRow(NewBaseModel):
|
||||
"""
|
||||
|
||||
for related in related_models:
|
||||
field = cls.Meta.model_fields[related]
|
||||
field = cast(Type["ForeignKeyField"], field)
|
||||
model_cls = field.to
|
||||
model_excludable = excludable.get(
|
||||
model_cls=cast(Type["Model"], cls), alias=table_prefix
|
||||
)
|
||||
if model_excludable.is_excluded(related):
|
||||
return item
|
||||
|
||||
relation_str = (
|
||||
"__".join([current_relation_str, related])
|
||||
if current_relation_str
|
||||
else related
|
||||
)
|
||||
field = cls.Meta.model_fields[related]
|
||||
field = cast(Type["ForeignKeyField"], field)
|
||||
model_cls = field.to
|
||||
|
||||
remainder = None
|
||||
if isinstance(related_models, dict) and related_models[related]:
|
||||
remainder = related_models[related]
|
||||
|
||||
@ -148,8 +148,8 @@ class QuerySet:
|
||||
)
|
||||
|
||||
async def _prefetch_related_models(
|
||||
self, models: Sequence[Optional["Model"]], rows: List
|
||||
) -> Sequence[Optional["Model"]]:
|
||||
self, models: List[Optional["Model"]], rows: List
|
||||
) -> List[Optional["Model"]]:
|
||||
"""
|
||||
Performs prefetch query for selected models names.
|
||||
|
||||
@ -169,7 +169,7 @@ class QuerySet:
|
||||
)
|
||||
return await query.prefetch_related(models=models, rows=rows) # type: ignore
|
||||
|
||||
def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]:
|
||||
def _process_query_result_rows(self, rows: List) -> List[Optional["Model"]]:
|
||||
"""
|
||||
Process database rows and initialize ormar Model from each of the rows.
|
||||
|
||||
|
||||
@ -68,6 +68,14 @@ class Relation:
|
||||
else None
|
||||
)
|
||||
|
||||
def clear(self) -> None:
|
||||
if self._type in (RelationType.PRIMARY, RelationType.THROUGH):
|
||||
self.related_models = None
|
||||
self._owner.__dict__[self.field_name] = None
|
||||
elif self.related_models is not None:
|
||||
self.related_models._clear()
|
||||
self._owner.__dict__[self.field_name] = []
|
||||
|
||||
@property
|
||||
def through(self) -> Type["Model"]:
|
||||
if not self._through: # pragma: no cover
|
||||
|
||||
@ -26,37 +26,6 @@ class RelationsManager:
|
||||
for field in self._related_fields:
|
||||
self._add_relation(field)
|
||||
|
||||
def _get_relation_type(self, field: Type["BaseField"]) -> RelationType:
|
||||
"""
|
||||
Returns type of the relation declared on a field.
|
||||
|
||||
:param field: field with relation declaration
|
||||
:type field: Type[BaseField]
|
||||
:return: type of the relation defined on field
|
||||
:rtype: RelationType
|
||||
"""
|
||||
if field.is_multi:
|
||||
return RelationType.MULTIPLE
|
||||
if field.is_through:
|
||||
return RelationType.THROUGH
|
||||
return RelationType.PRIMARY if not field.virtual else RelationType.REVERSE
|
||||
|
||||
def _add_relation(self, field: Type["BaseField"]) -> None:
|
||||
"""
|
||||
Registers relation in the manager.
|
||||
Adds Relation instance under field.name.
|
||||
|
||||
:param field: field with relation declaration
|
||||
:type field: Type[BaseField]
|
||||
"""
|
||||
self._relations[field.name] = Relation(
|
||||
manager=self,
|
||||
type_=self._get_relation_type(field),
|
||||
field_name=field.name,
|
||||
to=field.to,
|
||||
through=getattr(field, "through", None),
|
||||
)
|
||||
|
||||
def __contains__(self, item: str) -> bool:
|
||||
"""
|
||||
Checks if relation with given name is already registered.
|
||||
@ -68,6 +37,10 @@ class RelationsManager:
|
||||
"""
|
||||
return item in self._related_names
|
||||
|
||||
def clear(self) -> None:
|
||||
for relation in self._relations.values():
|
||||
relation.clear()
|
||||
|
||||
def get(self, name: str) -> Optional[Union["Model", Sequence["Model"]]]:
|
||||
"""
|
||||
Returns the related model/models if relation is set.
|
||||
@ -83,20 +56,6 @@ class RelationsManager:
|
||||
return relation.get()
|
||||
return None # pragma nocover
|
||||
|
||||
def _get(self, name: str) -> Optional[Relation]:
|
||||
"""
|
||||
Returns the actual relation and not the related model(s).
|
||||
|
||||
:param name: name of the relation
|
||||
:type name: str
|
||||
:return: Relation instance
|
||||
:rtype: ormar.relations.relation.Relation
|
||||
"""
|
||||
relation = self._relations.get(name, None)
|
||||
if relation is not None:
|
||||
return relation
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add(parent: "Model", child: "Model", field: Type["ForeignKeyField"],) -> None:
|
||||
"""
|
||||
@ -164,3 +123,48 @@ class RelationsManager:
|
||||
relation_name = item.Meta.model_fields[name].get_related_name()
|
||||
item._orm.remove(name, parent)
|
||||
parent._orm.remove(relation_name, item)
|
||||
|
||||
def _get(self, name: str) -> Optional[Relation]:
|
||||
"""
|
||||
Returns the actual relation and not the related model(s).
|
||||
|
||||
:param name: name of the relation
|
||||
:type name: str
|
||||
:return: Relation instance
|
||||
:rtype: ormar.relations.relation.Relation
|
||||
"""
|
||||
relation = self._relations.get(name, None)
|
||||
if relation is not None:
|
||||
return relation
|
||||
return None
|
||||
|
||||
def _get_relation_type(self, field: Type["BaseField"]) -> RelationType:
|
||||
"""
|
||||
Returns type of the relation declared on a field.
|
||||
|
||||
:param field: field with relation declaration
|
||||
:type field: Type[BaseField]
|
||||
:return: type of the relation defined on field
|
||||
:rtype: RelationType
|
||||
"""
|
||||
if field.is_multi:
|
||||
return RelationType.MULTIPLE
|
||||
if field.is_through:
|
||||
return RelationType.THROUGH
|
||||
return RelationType.PRIMARY if not field.virtual else RelationType.REVERSE
|
||||
|
||||
def _add_relation(self, field: Type["BaseField"]) -> None:
|
||||
"""
|
||||
Registers relation in the manager.
|
||||
Adds Relation instance under field.name.
|
||||
|
||||
:param field: field with relation declaration
|
||||
:type field: Type[BaseField]
|
||||
"""
|
||||
self._relations[field.name] = Relation(
|
||||
manager=self,
|
||||
type_=self._get_relation_type(field),
|
||||
field_name=field.name,
|
||||
to=field.to,
|
||||
through=getattr(field, "through", None),
|
||||
)
|
||||
|
||||
@ -75,6 +75,9 @@ class RelationProxy(list):
|
||||
self._initialize_queryset()
|
||||
return getattr(self.queryset_proxy, item)
|
||||
|
||||
def _clear(self) -> None:
|
||||
super().clear()
|
||||
|
||||
def _initialize_queryset(self) -> None:
|
||||
"""
|
||||
Initializes the QuerySetProxy if not yet initialized.
|
||||
|
||||
@ -21,9 +21,15 @@ renderer:
|
||||
- title: Model
|
||||
contents:
|
||||
- models.model.*
|
||||
- title: Model Row
|
||||
contents:
|
||||
- models.model_row.*
|
||||
- title: New BaseModel
|
||||
contents:
|
||||
- models.newbasemodel.*
|
||||
- title: Excludable Items
|
||||
contents:
|
||||
- models.excludable.*
|
||||
- title: Model Table Proxy
|
||||
contents:
|
||||
- models.modelproxy.*
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional, Union, List
|
||||
from typing import List, Optional
|
||||
|
||||
import databases
|
||||
import pytest
|
||||
@ -23,13 +23,6 @@ class Child(ormar.Model):
|
||||
born_year: int = ormar.Integer(name="year_born", nullable=True)
|
||||
|
||||
|
||||
class ArtistChildren(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "children_x_artists"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
|
||||
class Artist(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "artists"
|
||||
@ -40,9 +33,7 @@ class Artist(ormar.Model):
|
||||
first_name: str = ormar.String(name="fname", max_length=100)
|
||||
last_name: str = ormar.String(name="lname", max_length=100)
|
||||
born_year: int = ormar.Integer(name="year")
|
||||
children: Optional[Union[Child, List[Child]]] = ormar.ManyToMany(
|
||||
Child, through=ArtistChildren
|
||||
)
|
||||
children: Optional[List[Child]] = ormar.ManyToMany(Child)
|
||||
|
||||
|
||||
class Album(ormar.Model):
|
||||
|
||||
@ -42,18 +42,13 @@ class Category(ormar.Model):
|
||||
name: str = ormar.String(max_length=100)
|
||||
|
||||
|
||||
class ItemsXCategories(ormar.Model):
|
||||
class Meta(LocalMeta):
|
||||
tablename = "items_x_categories"
|
||||
|
||||
|
||||
class Item(ormar.Model):
|
||||
class Meta(LocalMeta):
|
||||
pass
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
categories = ormar.ManyToMany(Category, through=ItemsXCategories)
|
||||
categories = ormar.ManyToMany(Category)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
|
||||
@ -121,11 +121,11 @@ class Bus(Car):
|
||||
max_persons: int = ormar.Integer()
|
||||
|
||||
|
||||
class PersonsCar(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "cars_x_persons"
|
||||
metadata = metadata
|
||||
database = db
|
||||
# class PersonsCar(ormar.Model):
|
||||
# class Meta:
|
||||
# tablename = "cars_x_persons"
|
||||
# metadata = metadata
|
||||
# database = db
|
||||
|
||||
|
||||
class Car2(ormar.Model):
|
||||
@ -138,7 +138,9 @@ class Car2(ormar.Model):
|
||||
name: str = ormar.String(max_length=50)
|
||||
owner: Person = ormar.ForeignKey(Person, related_name="owned")
|
||||
co_owners: List[Person] = ormar.ManyToMany(
|
||||
Person, through=PersonsCar, related_name="coowned"
|
||||
Person,
|
||||
# through=PersonsCar,
|
||||
related_name="coowned",
|
||||
)
|
||||
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
|
||||
|
||||
|
||||
171
tests/test_load_all.py
Normal file
171
tests/test_load_all.py
Normal file
@ -0,0 +1,171 @@
|
||||
from typing import List
|
||||
|
||||
import databases
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
|
||||
import ormar
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||
metadata = sqlalchemy.MetaData()
|
||||
|
||||
|
||||
class BaseMeta(ormar.ModelMeta):
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
|
||||
class Language(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "languages"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
level: str = ormar.String(max_length=150, default="Beginner")
|
||||
|
||||
|
||||
class CringeLevel(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "levels"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
language = ormar.ForeignKey(Language)
|
||||
|
||||
|
||||
class NickName(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "nicks"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100, nullable=False, name="hq_name")
|
||||
is_lame: bool = ormar.Boolean(nullable=True)
|
||||
level: CringeLevel = ormar.ForeignKey(CringeLevel)
|
||||
|
||||
|
||||
class HQ(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "hqs"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100, nullable=False, name="hq_name")
|
||||
nicks: List[NickName] = ormar.ManyToMany(NickName)
|
||||
|
||||
|
||||
class Company(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "companies"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100, nullable=False, name="company_name")
|
||||
founded: int = ormar.Integer(nullable=True)
|
||||
hq: HQ = ormar.ForeignKey(HQ, related_name="companies")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def create_test_database():
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
metadata.drop_all(engine)
|
||||
metadata.create_all(engine)
|
||||
yield
|
||||
metadata.drop_all(engine)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_all_fk_rel():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
hq = await HQ.objects.create(name="Main")
|
||||
company = await Company.objects.create(name="Banzai", founded=1988, hq=hq)
|
||||
|
||||
hq = await HQ.objects.get(name="Main")
|
||||
await hq.load_all()
|
||||
|
||||
assert hq.companies[0] == company
|
||||
assert hq.companies[0].name == "Banzai"
|
||||
assert hq.companies[0].founded == 1988
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_all_many_to_many():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
nick1 = await NickName.objects.create(name="BazingaO", is_lame=False)
|
||||
nick2 = await NickName.objects.create(name="Bazinga20", is_lame=True)
|
||||
hq = await HQ.objects.create(name="Main")
|
||||
await hq.nicks.add(nick1)
|
||||
await hq.nicks.add(nick2)
|
||||
|
||||
hq = await HQ.objects.get(name="Main")
|
||||
await hq.load_all()
|
||||
|
||||
assert hq.nicks[0] == nick1
|
||||
assert hq.nicks[0].name == "BazingaO"
|
||||
|
||||
assert hq.nicks[1] == nick2
|
||||
assert hq.nicks[1].name == "Bazinga20"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_loading_reversed_relation():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
hq = await HQ.objects.create(name="Main")
|
||||
await Company.objects.create(name="Banzai", founded=1988, hq=hq)
|
||||
|
||||
company = await Company.objects.get(name="Banzai")
|
||||
await company.load_all()
|
||||
|
||||
assert company.hq == hq
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_loading_nested():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
language = await Language.objects.create(name="English")
|
||||
level = await CringeLevel.objects.create(name="High", language=language)
|
||||
level2 = await CringeLevel.objects.create(name="Low", language=language)
|
||||
nick1 = await NickName.objects.create(
|
||||
name="BazingaO", is_lame=False, level=level
|
||||
)
|
||||
nick2 = await NickName.objects.create(
|
||||
name="Bazinga20", is_lame=True, level=level2
|
||||
)
|
||||
hq = await HQ.objects.create(name="Main")
|
||||
await hq.nicks.add(nick1)
|
||||
await hq.nicks.add(nick2)
|
||||
|
||||
hq = await HQ.objects.get(name="Main")
|
||||
await hq.load_all(follow=True)
|
||||
|
||||
assert hq.nicks[0] == nick1
|
||||
assert hq.nicks[0].name == "BazingaO"
|
||||
assert hq.nicks[0].level.name == "High"
|
||||
assert hq.nicks[0].level.language.name == "English"
|
||||
|
||||
assert hq.nicks[1] == nick2
|
||||
assert hq.nicks[1].name == "Bazinga20"
|
||||
assert hq.nicks[1].level.name == "Low"
|
||||
assert hq.nicks[1].level.language.name == "English"
|
||||
|
||||
await hq.load_all(follow=True, exclude="nicks__level__language")
|
||||
assert len(hq.nicks) == 2
|
||||
assert hq.nicks[0].level.language is None
|
||||
assert hq.nicks[1].level.language is None
|
||||
|
||||
await hq.load_all(follow=True, exclude="nicks__level__language__level")
|
||||
assert len(hq.nicks) == 2
|
||||
assert hq.nicks[0].level.language is not None
|
||||
assert hq.nicks[0].level.language.level is None
|
||||
assert hq.nicks[1].level.language is not None
|
||||
assert hq.nicks[1].level.language.level is None
|
||||
|
||||
await hq.load_all(follow=True, exclude="nicks__level")
|
||||
assert len(hq.nicks) == 2
|
||||
assert hq.nicks[0].level is None
|
||||
assert hq.nicks[1].level is None
|
||||
|
||||
await hq.load_all(follow=True, exclude="nicks")
|
||||
assert len(hq.nicks) == 0
|
||||
@ -288,6 +288,25 @@ async def test_update_through_models_from_queryset_on_through() -> Any:
|
||||
assert post2.categories[2].postcategory.param_name == "area"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_through_model_after_load() -> Any:
|
||||
async with database:
|
||||
post = await Post(title="Test post").save()
|
||||
await post.categories.create(
|
||||
name="Test category1",
|
||||
postcategory={"sort_order": 2, "param_name": "volume"},
|
||||
)
|
||||
post2 = await Post.objects.select_related("categories").get()
|
||||
assert len(post2.categories) == 1
|
||||
|
||||
await post2.categories[0].postcategory.load()
|
||||
await post2.categories[0].postcategory.update(sort_order=3)
|
||||
|
||||
post3 = await Post.objects.select_related("categories").get()
|
||||
assert len(post3.categories) == 1
|
||||
assert post3.categories[0].postcategory.sort_order == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_through_from_related() -> Any:
|
||||
async with database:
|
||||
@ -371,9 +390,10 @@ async def test_excluding_fields_on_through_model() -> Any:
|
||||
# ordering by in order_by (V)
|
||||
# updating in query (V)
|
||||
# updating from querysetproxy (V)
|
||||
# including/excluding in fields?
|
||||
# including/excluding in fields? (V)
|
||||
# make through optional? auto-generated for cases other fields are missing? (V)
|
||||
|
||||
# modifying from instance (both sides?) (X) <= no, the loaded one doesn't have relations
|
||||
# allowing to change fk fields names in through model? (X) <= separate issue
|
||||
|
||||
# allowing to change fk fields names in through model?
|
||||
# make through optional? auto-generated for cases other fields are missing?
|
||||
# prevent adding relation on through field definition
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from typing import List, Union, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import databases
|
||||
import pytest
|
||||
@ -34,13 +34,6 @@ class Category(ormar.Model):
|
||||
name: str = ormar.String(max_length=40)
|
||||
|
||||
|
||||
class PostCategory(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "posts_categories"
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "posts"
|
||||
@ -49,9 +42,7 @@ class Post(ormar.Model):
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
|
||||
Category, through=PostCategory
|
||||
)
|
||||
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
|
||||
author: Optional[Author] = ormar.ForeignKey(Author)
|
||||
|
||||
|
||||
@ -74,6 +65,7 @@ async def create_test_database():
|
||||
async def cleanup():
|
||||
yield
|
||||
async with database:
|
||||
PostCategory = Post.Meta.model_fields["categories"].through
|
||||
await PostCategory.objects.delete(each=True)
|
||||
await Post.objects.delete(each=True)
|
||||
await Category.objects.delete(each=True)
|
||||
|
||||
@ -85,13 +85,6 @@ class Car(ormar.Model):
|
||||
factory: Optional[Factory] = ormar.ForeignKey(Factory)
|
||||
|
||||
|
||||
class UsersCar(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "cars_x_users"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
|
||||
class User(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "users"
|
||||
@ -100,7 +93,7 @@ class User(ormar.Model):
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
cars: List[Car] = ormar.ManyToMany(Car, through=UsersCar)
|
||||
cars: List[Car] = ormar.ManyToMany(Car)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
|
||||
Reference in New Issue
Block a user