update docs part 2
This commit is contained in:
@ -2,205 +2,169 @@
|
||||
|
||||
## Defining a relationship
|
||||
|
||||
### Foreign Key
|
||||
### ForeignKey
|
||||
|
||||
To define a relationship you simply need to create a ForeignKey field on one `Model` and point it to another `Model`.
|
||||
`ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class.
|
||||
|
||||
```Python hl_lines="24"
|
||||
--8<-- "../docs_src/relations/docs001.py"
|
||||
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
|
||||
|
||||
To define a relation add `ForeignKey` field that points to related `Model`.
|
||||
|
||||
```Python hl_lines="27"
|
||||
--8<-- "../docs_src/fields/docs003.py"
|
||||
```
|
||||
|
||||
It automatically creates an sql foreign key constraint on a underlying table as well as nested pydantic model in the definition.
|
||||
#### Reverse Relation
|
||||
|
||||
`ForeignKey` fields are automatically registering reverse side of the relation.
|
||||
|
||||
```Python hl_lines="29 33"
|
||||
--8<-- "../docs_src/relations/docs002.py"
|
||||
```
|
||||
By default it's child (source) `Model` name + s, like courses in snippet below:
|
||||
|
||||
Of course it's handled for you so you don't have to delve deep into this but you can.
|
||||
|
||||
!!!tip
|
||||
Note how by default the relation is optional, you can require the related `Model` by setting `nullable=False` on the `ForeignKey` field.
|
||||
|
||||
### Reverse Relation
|
||||
|
||||
At the same time the reverse relationship is registered automatically on parent model (target of `ForeignKey`).
|
||||
|
||||
By default it's child (source) `Model` name + 's', like courses in snippet below:
|
||||
|
||||
```Python hl_lines="25 31"
|
||||
```Python hl_lines="27 33"
|
||||
--8<-- "../docs_src/fields/docs001.py"
|
||||
```
|
||||
|
||||
#### related_name
|
||||
|
||||
But you can overwrite this name by providing `related_name` parameter like below:
|
||||
|
||||
```Python hl_lines="25 30"
|
||||
```Python hl_lines="27 33"
|
||||
--8<-- "../docs_src/fields/docs002.py"
|
||||
```
|
||||
|
||||
!!!tip
|
||||
Since related models are coming from Relationship Manager the reverse relation on access returns list of `wekref.proxy` to avoid circular references.
|
||||
The reverse relation on access returns list of `wekref.proxy` to avoid circular references.
|
||||
|
||||
|
||||
## Relationship Manager
|
||||
### Relation Setup
|
||||
|
||||
!!!tip
|
||||
This section is more technical so you might want to skip it if you are not interested in implementation details.
|
||||
You have several ways to set-up a relationship connection.
|
||||
|
||||
### Need for a manager?
|
||||
#### `Model` instance
|
||||
|
||||
Since orm uses Sqlalchemy core under the hood to prepare the queries,
|
||||
the orm needs a way to uniquely identify each relationship between the tables to construct working queries.
|
||||
The most obvious one is to pass a related `Model` instance to the constructor.
|
||||
|
||||
Imagine that you have models as following:
|
||||
|
||||
```Python
|
||||
--8<-- "../docs_src/relations/docs003.py"
|
||||
```Python hl_lines="32-33"
|
||||
--8<-- "../docs_src/relations/docs001.py"
|
||||
```
|
||||
|
||||
Now imagine that you want to go from school class to student and his category and to teacher and his category.
|
||||
#### Primary key value
|
||||
|
||||
You can setup the relation also with just the pk column value of the related model.
|
||||
|
||||
```Python hl_lines="35-36"
|
||||
--8<-- "../docs_src/relations/docs001.py"
|
||||
```
|
||||
|
||||
#### Dictionary
|
||||
|
||||
Next option is with a dictionary of key-values of the related model.
|
||||
|
||||
You can build the dictionary yourself or get it from existing model with `dict()` method.
|
||||
|
||||
```Python hl_lines="38-39"
|
||||
--8<-- "../docs_src/relations/docs001.py"
|
||||
```
|
||||
|
||||
#### None
|
||||
|
||||
Finally you can explicitly set it to None (default behavior if no value passed).
|
||||
|
||||
```Python hl_lines="41-42"
|
||||
--8<-- "../docs_src/relations/docs001.py"
|
||||
```
|
||||
|
||||
!!!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.
|
||||
|
||||
|
||||
### Many2Many
|
||||
|
||||
`Many2Many(to, through)` has required parameters `to` and `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
|
||||
classes = await SchoolClass.objects.select_related(
|
||||
["teachers__category", "students__category"]).all()
|
||||
--8<-- "../docs_src/relations/docs002.py"
|
||||
```
|
||||
|
||||
!!!tip
|
||||
To query a chain of models use double underscores between the relation names (`ForeignKeys` or reverse `ForeignKeys`)
|
||||
|
||||
!!!note
|
||||
To select related models use `select_related` method from `Model` `QuerySet`.
|
||||
|
||||
Note that you use relation (`ForeignKey`) names and not the table names.
|
||||
|
||||
Since you join two times to the same table (categories) it won't work by default -> you would need to use aliases for category tables and columns.
|
||||
|
||||
But don't worry - ormar can handle situations like this, as it uses the Relationship Manager which has it's aliases defined for all relationships.
|
||||
|
||||
Each class is registered with the same instance of the AliasManager that you can access like this:
|
||||
|
||||
```python
|
||||
SchoolClass.alias_manager
|
||||
```
|
||||
|
||||
It's the same object for all `Models`
|
||||
|
||||
```python
|
||||
print(Teacher.alias_manager == Student.alias_manager)
|
||||
# will produce: True
|
||||
```
|
||||
|
||||
### Table aliases
|
||||
|
||||
You can even preview the alias used for any relation by passing two tables names.
|
||||
|
||||
```python
|
||||
print(Teacher.alias_manager.resolve_relation_join(
|
||||
'students', 'categories'))
|
||||
# will produce: KId1c6 (sample value)
|
||||
|
||||
print(Teacher.alias_manager.resolve_relation_join(
|
||||
'categories', 'students'))
|
||||
# will produce: EFccd5 (sample value)
|
||||
```
|
||||
|
||||
!!!note
|
||||
The order that you pass the names matters -> as those are 2 different relationships depending on join order.
|
||||
|
||||
As aliases are produced randomly you can be presented with different results.
|
||||
|
||||
### Query automatic construction
|
||||
|
||||
Ormar is using those aliases during queries to both construct a meaningful and valid sql,
|
||||
as well as later use it to extract proper columns for proper nested models.
|
||||
|
||||
Running a previously mentioned query to select school classes and related teachers and students:
|
||||
|
||||
Create sample data:
|
||||
```Python
|
||||
classes = await SchoolClass.objects.select_related(
|
||||
["teachers__category", "students__category"]).all()
|
||||
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")
|
||||
```
|
||||
|
||||
Will result in a query like this (run under the hood):
|
||||
#### Adding related models
|
||||
|
||||
```sql
|
||||
SELECT schoolclasses.id,
|
||||
schoolclasses.name,
|
||||
schoolclasses.department,
|
||||
NZc8e2_students.id as NZc8e2_id,
|
||||
NZc8e2_students.name as NZc8e2_name,
|
||||
NZc8e2_students.schoolclass as NZc8e2_schoolclass,
|
||||
NZc8e2_students.category as NZc8e2_category,
|
||||
MYfe53_categories.id as MYfe53_id,
|
||||
MYfe53_categories.name as MYfe53_name,
|
||||
WA49a3_teachers.id as WA49a3_id,
|
||||
WA49a3_teachers.name as WA49a3_name,
|
||||
WA49a3_teachers.schoolclass as WA49a3_schoolclass,
|
||||
WA49a3_teachers.category as WA49a3_category,
|
||||
WZa13b_categories.id as WZa13b_id,
|
||||
WZa13b_categories.name as WZa13b_name
|
||||
FROM schoolclasses
|
||||
LEFT OUTER JOIN students NZc8e2_students ON NZc8e2_students.schoolclass = schoolclasses.id
|
||||
LEFT OUTER JOIN categories MYfe53_categories ON MYfe53_categories.id = NZc8e2_students.category
|
||||
LEFT OUTER JOIN teachers WA49a3_teachers ON WA49a3_teachers.schoolclass = schoolclasses.id
|
||||
LEFT OUTER JOIN categories WZa13b_categories ON WZa13b_categories.id = WA49a3_teachers.category
|
||||
ORDER BY schoolclasses.id, NZc8e2_students.id, MYfe53_categories.id, WA49a3_teachers.id, WZa13b_categories.id
|
||||
```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.
|
||||
|
||||
#### Creating new related `Model` instances
|
||||
|
||||
```python
|
||||
# Creating columns object from instance:
|
||||
await post.categories.create(name="Tips")
|
||||
assert len(await post.categories.all()) == 2
|
||||
# newly created instance already have relation persisted in the database
|
||||
```
|
||||
|
||||
!!!note
|
||||
As mentioned before the aliases are produced dynamically so the actual result might differ.
|
||||
Note that when accessing QuerySet API methods through Many2Many relation you don't
|
||||
need to use objects attribute like in normal queries.
|
||||
|
||||
Note that aliases are assigned to relations and not the tables, therefore the first table is always without an alias.
|
||||
|
||||
### Returning related Models
|
||||
|
||||
Each object in Relationship Manager is identified by orm_id which you can preview like this
|
||||
To learn more about available QuerySet methods visit [queries][queries]
|
||||
|
||||
#### Removing related models
|
||||
```python
|
||||
category = Category(name='Math')
|
||||
print(category._orm_id)
|
||||
# will produce: c76046d9410c4582a656bf12a44c892c (sample value)
|
||||
# Removal of the relationship by one
|
||||
await news.posts.remove(post)
|
||||
# or all at once
|
||||
await news.posts.clear()
|
||||
```
|
||||
|
||||
Each call to related `Model` is actually coming through the Manager which stores all
|
||||
the relations in a dictionary and returns related `Models` by relation type (name) and by object _orm_id.
|
||||
#### All other queryset methods
|
||||
|
||||
Since we register both sides of the relation the side registering the relation
|
||||
is always registering the other side as concrete model,
|
||||
while the reverse relation is a weakref.proxy to avoid circular references.
|
||||
When access directly the related `Many2Many` field returns the list of related models.
|
||||
|
||||
Sounds complicated but in reality it means something like this:
|
||||
But at the same time it exposes full QuerySet API, so you can filter, create, select related etc.
|
||||
|
||||
```python
|
||||
test_class = await SchoolClass.objects.create(name='Test')
|
||||
student = await Student.objects.create(name='John', schoolclass=test_class)
|
||||
# the relation to schoolsclass from student (i.e. when you call student.schoolclass)
|
||||
# is a concrete one, meaning directy relating the schoolclass `Model` object
|
||||
# On the other side calling test_class.students will result in a list of wekref.proxy objects
|
||||
# Many to many relation exposes a list of columns models
|
||||
# and an API of the Queryset:
|
||||
assert news == await post.categories.get(name="News")
|
||||
|
||||
# with all Queryset methods - filtering, selecting columns, counting etc.
|
||||
await news.posts.filter(title__contains="M2M").all()
|
||||
await Category.objects.filter(posts__author=guido).get()
|
||||
|
||||
# columns models of many to many relation can be prefetched
|
||||
news_posts = await news.posts.select_related("author").all()
|
||||
assert news_posts[0].author == guido
|
||||
```
|
||||
|
||||
!!!tip
|
||||
To learn more about queries and available methods please review [queries][queries] section.
|
||||
|
||||
All relations are kept in lists, meaning that when you access related object the Relationship Manager is
|
||||
searching itself for related models and get a list of them.
|
||||
|
||||
But since child to parent relation is a many to one type,
|
||||
the Manager is unpacking the first (and only) related model from a list and you get an actual `Model` instance instead of a list.
|
||||
|
||||
Coming from parent to child relation (one to many) you always get a list of results.
|
||||
|
||||
Translating this into concrete sample, the same as above:
|
||||
|
||||
```python
|
||||
test_class = await SchoolClass.objects.create(name='Test')
|
||||
student = await Student.objects.create(name='John', schoolclass=test_class)
|
||||
|
||||
student.schoolclass # return a test_class instance extracted from relationship list
|
||||
test_class.students # return a list of related wekref.proxy refering related students `Models`
|
||||
|
||||
```
|
||||
|
||||
!!!tip
|
||||
You can preview all relations currently registered by accessing Relationship Manager on any class/instance `Student._orm_relationship_manager._relations`
|
||||
To learn more about available QuerySet methods visit [queries][queries]
|
||||
|
||||
[queries]: ./queries.md
|
||||
Reference in New Issue
Block a user