update docs part 2

This commit is contained in:
collerek
2020-10-07 17:43:03 +02:00
parent ba0990d05b
commit 717feb2c74
18 changed files with 688 additions and 396 deletions

View File

@ -1,7 +1,11 @@
# Fields
There are 11 basic model field types and a special `ForeignKey` field to establish relationships between models.
There are 12 basic model field types and a special `ForeignKey` and `Many2Many` fields to establish relationships between models.
!!!tip
For explanation of `ForeignKey` and `Many2Many` fields check [relations][relations].
Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model.
@ -22,11 +26,11 @@ Used in sql only.
`autoincrement`: `bool` = `primary_key and type == int` -> defaults to True if column is a primary key and of type Integer, otherwise False.
Can be only used with int fields.
Can be only used with int/bigint fields.
If a field has autoincrement it becomes optional.
Used only in sql.
Used both in sql and pydantic (changes pk field to optional for autoincrement).
### nullable
@ -37,13 +41,8 @@ Specifies if field is optional or required, used both with sql and pydantic.
!!!note
By default all `ForeignKeys` are also nullable, meaning the related `Model` is not required.
If you change the `ForeignKey` column to `nullable`, it not only becomes required, it changes also the way in which data is loaded in queries.
If you change the `ForeignKey` column to `nullable=False`, it becomes required.
If you select `Model` without explicitly adding related `Model` assigned by not nullable `ForeignKey`, the `Model` is still gona be appended automatically, see example below.
```Python hl_lines="24 32 33 34 35 37 38 39 40 41"
--8<-- "../docs_src/fields/docs003.py"
```
!!!info
If you want to know more about how you can preload related models during queries and how the relations work read the [queries][queries] and [relations][relations] sections.
@ -93,22 +92,56 @@ Sets the unique constraint on a table's column.
Used in sql only.
### pydantic_only
`pydantic_only`: `bool` = `False`
Prevents creation of a sql column for given field.
Used for data related to given model but not to be stored in the database.
Used in pydantic only.
### choices
`choices`: `Sequence` = `[]`
A set of choices allowed to be used for given field.
Used for data validation on pydantic side.
Prevents insertion of value not present in the choices list.
Used in pydantic only.
## Fields Types
### String
`String(length)` has a required `length` parameter.
`String(max_length,
allow_blank: bool = True,
strip_whitespace: bool = False,
min_length: int = None,
max_length: int = None,
curtail_length: int = None,
regex: str = None,)` has a required `max_length` parameter.
* Sqlalchemy column: `sqlalchemy.String`
* Type (used for pydantic): `str`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Text
`Text()` has no required parameters.
`Text(allow_blank: bool = True, strip_whitespace: bool = False)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Text`
* Type (used for pydantic): `str`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Boolean
`Boolean()` has no required parameters.
@ -118,32 +151,58 @@ Used in sql only.
### Integer
`Integer()` has no required parameters.
`Integer(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Integer`
* Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### BigInteger
`BigInteger()` has no required parameters.
`BigInteger(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.BigInteger`
* Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Float
`Float()` has no required parameters.
`Float(minimum: float = None,
maximum: float = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Float`
* Type (used for pydantic): `float`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Decimal
`Decimal(lenght, precision)` has required `length` and `precision` parameters.
`Decimal(minimum: float = None,
maximum: float = None,
multiple_of: int = None,
precision: int = None,
scale: int = None,
max_digits: int = None,
decimal_places: int = None)` has no required parameters
You can use either `length` and `precision` parameters or `max_digits` and `decimal_places`.
* Sqlalchemy column: `sqlalchemy.DECIMAL`
* Type (used for pydantic): `decimal.Decimal`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Date
`Date()` has no required parameters.
@ -172,35 +231,13 @@ Used in sql only.
* Sqlalchemy column: `sqlalchemy.JSON`
* Type (used for pydantic): `pydantic.Json`
### ForeignKey
### UUID
`ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class.
`UUID()` has no required parameters.
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` primary key column
`ForeignKey` fields are automatically registering reverse side of the relation.
By default it's child (source) `Model` name + s, like courses in snippet below:
```Python hl_lines="25 31"
--8<-- "../docs_src/fields/docs001.py"
```
But you can overwrite this name by providing `related_name` parameter like below:
```Python hl_lines="25 30"
--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.
!!!info
All relations are stored in lists, but when you access parent `Model` the ormar is unpacking the value for you.
Read more in [relations][relations].
* Sqlalchemy column: `ormar.UUID` based on `sqlalchemy.CHAR` field
* Type (used for pydantic): `uuid.UUID`
[relations]: ./relations.md
[queries]: ./queries.md
[queries]: ./queries.md
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types

0
docs/install.md Normal file
View File

View File

@ -82,6 +82,21 @@ You can overwrite this parameter by providing `Meta` class `tablename` argument.
--8<-- "../docs_src/models/docs002.py"
```
### Constraints
On a model level you can also set model-wise constraints on sql columns.
Right now only `UniqueColumns` constraint is present.
!!!tip
To read more about columns constraints like `primary_key`, `unique`, `ForeignKey` etc. visit [fields][fields].
You can set this parameter by providing `Meta` class `constraints` argument.
```Python hl_lines="14-17"
--8<-- "../docs_src/models/docs006.py"
```
## Initialization
There are two ways to create and persist the `Model` instance in the database.
@ -97,6 +112,8 @@ If you plan to modify the instance in the later execution of your program you ca
If you want to initiate your `Model` and at the same time save in in the database use a QuerySet's method `create()`.
For creating multiple objects at once a `bulk_create()` QuerySet's method is available.
Each model has a `QuerySet` initialised as `objects` parameter
```Python hl_lines="23"
@ -104,7 +121,31 @@ Each model has a `QuerySet` initialised as `objects` parameter
```
!!!info
To read more about `QuerySets` and available methods visit [queries][queries]
To read more about `QuerySets` (including bulk operations) and available methods visit [queries][queries]
## `Model` methods
### load
By default when you query a table without prefetching related models, the ormar will still construct
your related models, but populate them only with the pk value.
```python
track = await Track.objects.get(name='The Bird')
track.album.pk # will return malibu album pk (1)
track.album.name # will return None
# you need to actually load the data first
await track.album.load()
track.album.name # will return 'Malibu'
```
### save
### delete
### update
## Internals
@ -114,7 +155,7 @@ Apart from special parameters defined in the `Model` during definition (tablenam
All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models.
For example to list model fields you can:
For example to list pydantic model fields you can:
```Python hl_lines="20"
--8<-- "../docs_src/models/docs003.py"
@ -137,7 +178,7 @@ For example to list table columns you can:
```
!!!tip
You can access table primary key name by `Course.__pkname__`
You can access table primary key name by `Course.Meta.pkname`
!!!info
For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation.

View File

@ -4,17 +4,24 @@
Each Model is auto registered with a QuerySet that represents the underlaying query and it's options.
Most of the methods are also available through many to many relation interface.
Given the Models like this
```Python
--8<-- "../docs_src/relations/docs001.py"
--8<-- "../docs_src/queries/docs001.py"
```
we can demonstrate available methods to fetch and save the data into the database.
### create(**kwargs)
Creates the model instance, saves it in a database and returns the updates model (with pk populated).
### create
`create(**kwargs): -> Model`
Creates the model instance, saves it in a database and returns the updates model
(with pk populated if not passed and autoincrement is set).
The allowed kwargs are `Model` fields names and proper value types.
```python
@ -28,22 +35,12 @@ malibu = Album(name="Malibu")
await malibu.save()
```
### load()
!!!tip
Check other `Model` methods in [models][models]
By default when you query a table without prefetching related models, the ormar will still construct
your related models, but populate them only with the pk value.
### get
```python
track = await Track.objects.get(name='The Bird')
track.album.pk # will return malibu album pk (1)
track.album.name # will return None
# you need to actually load the data first
await track.album.load()
track.album.name # will return 'Malibu'
```
### get(**kwargs)
`get(**kwargs): -> Model`
Get's the first row from the db meeting the criteria set by kwargs.
@ -53,11 +50,193 @@ Passing a criteria is actually calling filter(**kwargs) method described below.
```python
track = await Track.objects.get(name='The Bird')
# note that above is equivalent to await Track.objects.filter(name='The Bird').get()
track2 = track = await Track.objects.get()
track == track2 # True since it's the only row in db
track == track2 # True since it's the only row in db in our example
```
### all()
!!!warning
If no row meets the criteria `NoMatch` exception is raised.
If there are multiple rows meeting the criteria the `MultipleMatches` exception is raised.
### get_or_create
`get_or_create(**kwargs) -> Model`
Combination of create and get methods.
Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates a new one with given kwargs.
```python
album = await Album.objects.get_or_create(name='The Cat')
# object is created as it does not exist
album2 = await Album.objects.get_or_create(name='The Cat')
assert album == album2
# return True as the same db row is returned
```
!!!warning
Despite being a equivalent row from database the `album` and `album2` in example above are 2 different python objects!
Updating one of them will not refresh the second one until you excplicitly load() the fresh data from db.
!!!note
Note that if you want to create a new object you either have to pass pk column value or pk column has to be set as autoincrement
### update
`update(each: bool = False, **kwargs) -> int`
QuerySet level update is used to update multiple records with the same value at once.
You either have to filter the QuerySet first or provide a `each=True` flag to update whole table.
If you do not provide this flag or a filter a `QueryDefinitionError` will be raised.
Return number of rows updated.
```python hl_lines="24-28"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# queryset needs to be filtered before deleting to prevent accidental overwrite
# to update whole database table each=True needs to be provided as a safety switch
await Book.objects.update(each=True, genre='Fiction')
all_books = await Book.objects.filter(genre='Fiction').all()
assert len(all_books) == 3
```
### update_or_create
`update_or_create(**kwargs) -> Model`
### bulk_create
`bulk_create(objects: List["Model"]) -> None`
Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
```python hl_lines="20-26"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class ToDo(ormar.Model):
class Meta:
tablename = "todos"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
text: ormar.String(max_length=500)
completed: ormar.Boolean(default=False)
# create multiple instances at once with bulk_create
await ToDo.objects.bulk_create(
[
ToDo(text="Buy the groceries."),
ToDo(text="Call Mum.", completed=True),
ToDo(text="Send invoices.", completed=True),
]
)
todoes = await ToDo.objects.all()
assert len(todoes) == 3
```
### bulk_update
`bulk_update(objects: List["Model"], columns: List[str] = None) -> None`
Allows to update multiple instance at once.
All `Models` passed need to have primary key column populated.
You can also select which fields to update by passing `columns` list as a list of string names.
```python hl_lines="8"
# continuing the example from bulk_create
# update objects
for todo in todoes:
todo.completed = False
# perform update of all objects at once
# objects need to have pk column set, otherwise exception is raised
await ToDo.objects.bulk_update(todoes)
completed = await ToDo.objects.filter(completed=False).all()
assert len(completed) == 3
```
### delete
`delete(each: bool = False, **kwargs) -> int`
QuerySet level delete is used to delete multiple records at once.
You either have to filter the QuerySet first or provide a `each=True` flag to delete whole table.
If you do not provide this flag or a filter a `QueryDefinitionError` will be raised.
Return number of rows deleted.
```python hl_lines="23-27"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace in Space', author="Tolstoy, Leo", genre='Fantasy')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# delete accepts kwargs that will be used in filter
# acting in same way as queryset.filter(**kwargs).delete()
await Book.objects.delete(genre='Fantasy') # delete all fantasy books
all_books = await Book.objects.all()
assert len(all_books) == 2
```
### all
Returns all rows from a database for given model
@ -66,7 +245,7 @@ tracks = await Track.objects.select_related("album").all()
# will return a list of all Tracks
```
### filter(**kwargs)
### filter
Allows you to filter by any `Model` attribute/field
as well as to fetch instances, with a filter across an FK relationship.
@ -96,7 +275,9 @@ You can use special filter suffix to change the filter operands:
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### select_related(*args)
### exclude
### select_related
Allows to prefetch related models.
@ -127,7 +308,7 @@ classes = await SchoolClass.objects.select_related(
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### limit(int)
### limit
You can limit the results to desired number of rows.
@ -141,7 +322,7 @@ tracks = await Track.objects.limit(1).all()
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### offset(int)
### offset
You can also offset the results by desired number of rows.
@ -150,7 +331,18 @@ tracks = await Track.objects.offset(1).limit(1).all()
# will return just one Track, but this time the second one
```
### count
### exists
### fields
!!!note
`filter()`, `select_related()`, `limit()` and `offset()` returns a QueySet instance so you can chain them together.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
[models]: ./models.md

View File

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

0
docs/testing.md Normal file
View File