update docs, add load_all(), tests for load_all, make through field optional

This commit is contained in:
collerek
2021-03-03 19:48:40 +01:00
parent 9ad1528cc0
commit a8ae50276e
56 changed files with 1653 additions and 653 deletions

View File

@ -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():

View File

@ -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

View File

@ -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

View File

@ -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.

View 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

View File

@ -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

View File

@ -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.

View 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)`:

View File

@ -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.

View File

@ -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.

View 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

View File

@ -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**:

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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**:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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():

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

View 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)

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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.

View File

@ -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

View File

@ -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),
)

View File

@ -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.

View File

@ -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.*

View File

@ -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):

View File

@ -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")

View File

@ -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
View 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

View File

@ -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

View File

@ -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)

View File

@ -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")