257 lines
8.0 KiB
Markdown
257 lines
8.0 KiB
Markdown
# ManyToMany
|
|
|
|
`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`.
|
|
|
|
* Sqlalchemy column: class of a target `Model` primary key column
|
|
* Type (used for pydantic): type of a target `Model`
|
|
|
|
## Defining Models
|
|
|
|
```Python hl_lines="40"
|
|
--8<-- "../docs_src/relations/docs002.py"
|
|
```
|
|
|
|
Create sample data:
|
|
```Python
|
|
guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
|
|
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)
|
|
# or from the other end:
|
|
await news.posts.add(post)
|
|
```
|
|
|
|
!!!warning
|
|
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.
|
|
|
|
Removes also the relation in the database.
|
|
|
|
```python
|
|
await news.posts.remove(post)
|
|
```
|
|
|
|
### clear
|
|
|
|
Removal of all related models in one call.
|
|
|
|
Removes also the relation in the database.
|
|
|
|
```python
|
|
await news.posts.clear()
|
|
```
|
|
|
|
### QuerysetProxy
|
|
|
|
Reverse relation exposes QuerysetProxy API that allows you to query related model like you would issue a normal Query.
|
|
|
|
To read which methods of QuerySet are available read below [querysetproxy][querysetproxy]
|
|
|
|
## related_name
|
|
|
|
By default, the related_name is generated in the same way as for the `ForeignKey` relation (class.name.lower()+'s'),
|
|
but in the same way you can overwrite this name by providing `related_name` parameter like below:
|
|
|
|
```Python
|
|
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
|
|
Category, through=PostCategory, related_name="new_categories"
|
|
)
|
|
```
|
|
|
|
!!!warning
|
|
When you provide multiple relations to the same model `ormar` can no longer auto generate
|
|
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
|
|
for all but one (one can be default and generated) or all related fields.
|
|
|
|
|
|
[queries]: ./queries.md
|
|
[querysetproxy]: ./queryset-proxy.md
|
|
[get]: ./queries.md#get
|
|
[all]: ./queries.md#all
|
|
[create]: ./queries.md#create
|
|
[get_or_create]: ./queries.md#get_or_create
|
|
[update_or_create]: ./queries.md#update_or_create
|
|
[filter]: ./queries.md#filter
|
|
[exclude]: ./queries.md#exclude
|
|
[select_related]: ./queries.md#select_related
|
|
[prefetch_related]: ./queries.md#prefetch_related
|
|
[limit]: ./queries.md#limit
|
|
[offset]: ./queries.md#offset
|
|
[count]: ./queries.md#count
|
|
[exists]: ./queries.md#exists
|
|
[fields]: ./queries.md#fields
|
|
[exclude_fields]: ./queries.md#exclude_fields
|
|
[order_by]: ./queries.md#order_by |