reorganize docs into sections for easier navigation part 1
This commit is contained in:
@ -133,6 +133,6 @@ def test_all_endpoints():
|
|||||||
You can read more on testing fastapi in [fastapi][fastapi] docs.
|
You can read more on testing fastapi in [fastapi][fastapi] docs.
|
||||||
|
|
||||||
[fastapi]: https://fastapi.tiangolo.com/
|
[fastapi]: https://fastapi.tiangolo.com/
|
||||||
[models]: ./models.md
|
[models]: ./models/index.md
|
||||||
[database initialization]: ../models/#database-initialization-migrations
|
[database initialization]: ./models/migrations.md
|
||||||
[tests]: https://github.com/collerek/ormar/tree/master/tests
|
[tests]: https://github.com/collerek/ormar/tree/master/tests
|
||||||
126
docs/fields/common-parameters.md
Normal file
126
docs/fields/common-parameters.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Common Parameters
|
||||||
|
|
||||||
|
All `Field` types have a set of common parameters.
|
||||||
|
|
||||||
|
## primary_key
|
||||||
|
|
||||||
|
`primary_key`: `bool` = `False` -> by default False.
|
||||||
|
|
||||||
|
Sets the primary key column on a table, foreign keys always refer to the pk of the `Model`.
|
||||||
|
|
||||||
|
Used in sql only.
|
||||||
|
|
||||||
|
## autoincrement
|
||||||
|
|
||||||
|
`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/bigint fields.
|
||||||
|
|
||||||
|
If a field has autoincrement it becomes optional.
|
||||||
|
|
||||||
|
Used both in sql and pydantic (changes pk field to optional for autoincrement).
|
||||||
|
|
||||||
|
## nullable
|
||||||
|
|
||||||
|
`nullable`: `bool` = `not primary_key` -> defaults to False for primary key column, and True for all other.
|
||||||
|
|
||||||
|
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=False`, it becomes required.
|
||||||
|
|
||||||
|
|
||||||
|
!!!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.
|
||||||
|
|
||||||
|
|
||||||
|
## default
|
||||||
|
|
||||||
|
`default`: `Any` = `None` -> defaults to None.
|
||||||
|
|
||||||
|
A default value used if no other value is passed.
|
||||||
|
|
||||||
|
In sql invoked on an insert, used during pydantic model definition.
|
||||||
|
|
||||||
|
If the field has a default value it becomes optional.
|
||||||
|
|
||||||
|
You can pass a static value or a Callable (function etc.)
|
||||||
|
|
||||||
|
Used both in sql and pydantic.
|
||||||
|
|
||||||
|
## server default
|
||||||
|
|
||||||
|
`server_default`: `Any` = `None` -> defaults to None.
|
||||||
|
|
||||||
|
A default value used if no other value is passed.
|
||||||
|
|
||||||
|
In sql invoked on the server side so you can pass i.e. sql function (like now() or query/value wrapped in sqlalchemy text() clause).
|
||||||
|
|
||||||
|
If the field has a server_default value it becomes optional.
|
||||||
|
|
||||||
|
You can pass a static value or a Callable (function etc.)
|
||||||
|
|
||||||
|
Used in sql only.
|
||||||
|
|
||||||
|
Sample usage:
|
||||||
|
|
||||||
|
```Python hl_lines="21-23"
|
||||||
|
--8<-- "../docs_src/fields/docs004.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
`server_default` accepts `str`, `sqlalchemy.sql.elements.ClauseElement` or `sqlalchemy.sql.elements.TextClause`
|
||||||
|
so if you want to set i.e. Integer value you need to wrap it in `sqlalchemy.text()` function like above
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
You can pass also valid sql (dialect specific) wrapped in `sqlalchemy.text()`
|
||||||
|
|
||||||
|
For example `func.now()` above could be exchanged for `text('(CURRENT_TIMESTAMP)')` for sqlite backend
|
||||||
|
|
||||||
|
!!!info
|
||||||
|
`server_default` is passed straight to sqlalchemy table definition so you can read more in [server default][server default] sqlalchemy documentation
|
||||||
|
|
||||||
|
## index
|
||||||
|
|
||||||
|
`index`: `bool` = `False` -> by default False,
|
||||||
|
|
||||||
|
Sets the index on a table's column.
|
||||||
|
|
||||||
|
Used in sql only.
|
||||||
|
|
||||||
|
## unique
|
||||||
|
|
||||||
|
`unique`: `bool` = `False`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
[relations]: ../relations/index.md
|
||||||
|
[queries]: ../queries.md
|
||||||
|
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types
|
||||||
|
[server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions
|
||||||
@ -10,128 +10,6 @@ There are 12 basic model field types and a special `ForeignKey` and `Many2Many`
|
|||||||
Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model.
|
Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model.
|
||||||
|
|
||||||
|
|
||||||
## Common Parameters
|
|
||||||
|
|
||||||
All `Field` types have a set of common parameters.
|
|
||||||
|
|
||||||
### primary_key
|
|
||||||
|
|
||||||
`primary_key`: `bool` = `False` -> by default False.
|
|
||||||
|
|
||||||
Sets the primary key column on a table, foreign keys always refer to the pk of the `Model`.
|
|
||||||
|
|
||||||
Used in sql only.
|
|
||||||
|
|
||||||
### autoincrement
|
|
||||||
|
|
||||||
`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/bigint fields.
|
|
||||||
|
|
||||||
If a field has autoincrement it becomes optional.
|
|
||||||
|
|
||||||
Used both in sql and pydantic (changes pk field to optional for autoincrement).
|
|
||||||
|
|
||||||
### nullable
|
|
||||||
|
|
||||||
`nullable`: `bool` = `not primary_key` -> defaults to False for primary key column, and True for all other.
|
|
||||||
|
|
||||||
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=False`, it becomes required.
|
|
||||||
|
|
||||||
|
|
||||||
!!!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.
|
|
||||||
|
|
||||||
|
|
||||||
### default
|
|
||||||
|
|
||||||
`default`: `Any` = `None` -> defaults to None.
|
|
||||||
|
|
||||||
A default value used if no other value is passed.
|
|
||||||
|
|
||||||
In sql invoked on an insert, used during pydantic model definition.
|
|
||||||
|
|
||||||
If the field has a default value it becomes optional.
|
|
||||||
|
|
||||||
You can pass a static value or a Callable (function etc.)
|
|
||||||
|
|
||||||
Used both in sql and pydantic.
|
|
||||||
|
|
||||||
### server default
|
|
||||||
|
|
||||||
`server_default`: `Any` = `None` -> defaults to None.
|
|
||||||
|
|
||||||
A default value used if no other value is passed.
|
|
||||||
|
|
||||||
In sql invoked on the server side so you can pass i.e. sql function (like now() or query/value wrapped in sqlalchemy text() clause).
|
|
||||||
|
|
||||||
If the field has a server_default value it becomes optional.
|
|
||||||
|
|
||||||
You can pass a static value or a Callable (function etc.)
|
|
||||||
|
|
||||||
Used in sql only.
|
|
||||||
|
|
||||||
Sample usage:
|
|
||||||
|
|
||||||
```Python hl_lines="21-23"
|
|
||||||
--8<-- "../docs_src/fields/docs004.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!warning
|
|
||||||
`server_default` accepts `str`, `sqlalchemy.sql.elements.ClauseElement` or `sqlalchemy.sql.elements.TextClause`
|
|
||||||
so if you want to set i.e. Integer value you need to wrap it in `sqlalchemy.text()` function like above
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
You can pass also valid sql (dialect specific) wrapped in `sqlalchemy.text()`
|
|
||||||
|
|
||||||
For example `func.now()` above could be exchanged for `text('(CURRENT_TIMESTAMP)')` for sqlite backend
|
|
||||||
|
|
||||||
!!!info
|
|
||||||
`server_default` is passed straight to sqlalchemy table definition so you can read more in [server default][server default] sqlalchemy documentation
|
|
||||||
|
|
||||||
### index
|
|
||||||
|
|
||||||
`index`: `bool` = `False` -> by default False,
|
|
||||||
|
|
||||||
Sets the index on a table's column.
|
|
||||||
|
|
||||||
Used in sql only.
|
|
||||||
|
|
||||||
### unique
|
|
||||||
|
|
||||||
`unique`: `bool` = `False`
|
|
||||||
|
|
||||||
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
|
## Fields Types
|
||||||
|
|
||||||
### String
|
### String
|
||||||
@ -261,12 +139,13 @@ You can use either `length` and `precision` parameters or `max_digits` and `deci
|
|||||||
Depending on the format either 32 or 36 char is used in the database.
|
Depending on the format either 32 or 36 char is used in the database.
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
* 'hex' format value = "c616ab438cce49dbbf4380d109251dce" (CHAR(32))
|
|
||||||
* 'string' value = "c616ab43-8cce-49db-bf43-80d109251dce" (CHAR(36))
|
* 'hex' format value = `c616ab438cce49dbbf4380d109251dce` (CHAR(32))
|
||||||
|
* 'string' value = `c616ab43-8cce-49db-bf43-80d109251dce` (CHAR(36))
|
||||||
|
|
||||||
When loaded it's always python UUID so you can compare it and compare two formats values between each other.
|
When loaded it's always python UUID so you can compare it and compare two formats values between each other.
|
||||||
|
|
||||||
[relations]: ./relations.md
|
[relations]: ../relations/index.md
|
||||||
[queries]: ./queries.md
|
[queries]: ../queries.md
|
||||||
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types
|
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types
|
||||||
[server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions
|
[server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions
|
||||||
@ -295,175 +295,6 @@ Note that type hints are **optional** so perfectly valid `ormar` code can look l
|
|||||||
`ormar` construct annotations used by `pydantic` from own fields.
|
`ormar` construct annotations used by `pydantic` from own fields.
|
||||||
|
|
||||||
|
|
||||||
### Database initialization/ migrations
|
|
||||||
|
|
||||||
Note that all examples assume that you already have a database.
|
|
||||||
|
|
||||||
If that is not the case and you need to create your tables, that's super easy as `ormar` is using sqlalchemy for underlying table construction.
|
|
||||||
|
|
||||||
All you have to do is call `create_all()` like in the example below.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import sqlalchemy
|
|
||||||
# get your database url in sqlalchemy format - same as used with databases instance used in Model definition
|
|
||||||
engine = sqlalchemy.create_engine("sqlite:///test.db")
|
|
||||||
# note that this has to be the same metadata that is used in ormar Models definition
|
|
||||||
metadata.create_all(engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also create single tables, sqlalchemy tables are exposed in `ormar.Meta` class.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import sqlalchemy
|
|
||||||
# get your database url in sqlalchemy format - same as used with databases instance used in Model definition
|
|
||||||
engine = sqlalchemy.create_engine("sqlite:///test.db")
|
|
||||||
# Artist is an ormar model from previous examples
|
|
||||||
Artist.Meta.table.create(engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!warning
|
|
||||||
You need to create the tables only once, so use a python console for that or remove the script from your production code after first use.
|
|
||||||
|
|
||||||
Likewise as with tables, since we base tables on sqlalchemy for migrations please use [alembic][alembic].
|
|
||||||
|
|
||||||
Use command line to reproduce this minimalistic example.
|
|
||||||
|
|
||||||
```python
|
|
||||||
alembic init alembic
|
|
||||||
alembic revision --autogenerate -m "made some changes"
|
|
||||||
alembic upgrade head
|
|
||||||
```
|
|
||||||
|
|
||||||
A quick example of alembic migrations should be something similar to:
|
|
||||||
|
|
||||||
When you have application structure like:
|
|
||||||
|
|
||||||
```
|
|
||||||
-> app
|
|
||||||
-> alembic (initialized folder - so run alembic init alembic inside app folder)
|
|
||||||
-> models (here are the models)
|
|
||||||
-> __init__.py
|
|
||||||
-> my_models.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Your `env.py` file (in alembic folder) can look something like:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from logging.config import fileConfig
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
from alembic import context
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
# add app folder to system path (alternative is running it from parent folder with python -m ...)
|
|
||||||
myPath = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, myPath + '/../../')
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
# This line sets up loggers basically.
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
|
|
||||||
# add your model's MetaData object here (the one used in ormar)
|
|
||||||
# for 'autogenerate' support
|
|
||||||
from app.models.my_models import metadata
|
|
||||||
target_metadata = metadata
|
|
||||||
|
|
||||||
|
|
||||||
# set your url here or import from settings
|
|
||||||
# note that by default url is in saved sqlachemy.url variable in alembic.ini file
|
|
||||||
URL = "sqlite:///test.db"
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline():
|
|
||||||
"""Run migrations in 'offline' mode.
|
|
||||||
|
|
||||||
This configures the context with just a URL
|
|
||||||
and not an Engine, though an Engine is acceptable
|
|
||||||
here as well. By skipping the Engine creation
|
|
||||||
we don't even need a DBAPI to be available.
|
|
||||||
|
|
||||||
Calls to context.execute() here emit the given string to the
|
|
||||||
script output.
|
|
||||||
|
|
||||||
"""
|
|
||||||
context.configure(
|
|
||||||
url=URL,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
literal_binds=True,
|
|
||||||
dialect_opts={"paramstyle": "named"},
|
|
||||||
# if you use UUID field set also this param
|
|
||||||
# the prefix has to match sqlalchemy import name in alembic
|
|
||||||
# that can be set by sqlalchemy_module_prefix option (default 'sa.')
|
|
||||||
user_module_prefix='sa.'
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online():
|
|
||||||
"""Run migrations in 'online' mode.
|
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
|
||||||
and associate a connection with the context.
|
|
||||||
|
|
||||||
"""
|
|
||||||
connectable = create_engine(URL)
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
|
||||||
context.configure(
|
|
||||||
connection=connection,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
# if you use UUID field set also this param
|
|
||||||
# the prefix has to match sqlalchemy import name in alembic
|
|
||||||
# that can be set by sqlalchemy_module_prefix option (default 'sa.')
|
|
||||||
user_module_prefix='sa.'
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also include/exclude specific tables with `include_object` parameter passed to `context.configure`. That should be a function returning `True/False` for given objects.
|
|
||||||
|
|
||||||
A sample function excluding tables starting with `data_` in name unless it's 'data_jobs':
|
|
||||||
```python
|
|
||||||
def include_object(object, name, type_, reflected, compare_to):
|
|
||||||
if name and name.startswith('data_') and name not in ['data_jobs']:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!note
|
|
||||||
Function parameters for `include_objects` (you can change the name) are required and defined in alembic
|
|
||||||
to check what they do check the [alembic][alembic] documentation
|
|
||||||
|
|
||||||
And you pass it into context like (both in online and offline):
|
|
||||||
```python
|
|
||||||
context.configure(
|
|
||||||
url=URL,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
literal_binds=True,
|
|
||||||
dialect_opts={"paramstyle": "named"},
|
|
||||||
user_module_prefix='sa.',
|
|
||||||
include_object=include_object
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!info
|
|
||||||
You can read more about table creation, altering and migrations in [sqlalchemy table creation][sqlalchemy table creation] documentation.
|
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
@ -590,175 +421,9 @@ The objects itself have a saved status, which is set as following:
|
|||||||
|
|
||||||
You can check if model is saved with `ModelInstance.saved` property
|
You can check if model is saved with `ModelInstance.saved` property
|
||||||
|
|
||||||
## `Model` methods
|
[fields]: ../fields/field-types.md
|
||||||
|
[relations]: ../relations/index.md
|
||||||
### load
|
[queries]: ../queries.md
|
||||||
|
|
||||||
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. You can load the related model by calling `load()` method.
|
|
||||||
|
|
||||||
`load()` can also be used to refresh the model from the database (if it was changed by some other process).
|
|
||||||
|
|
||||||
```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
|
|
||||||
|
|
||||||
`save() -> self`
|
|
||||||
|
|
||||||
You can create new models by using `QuerySet.create()` method or by initializing your model as a normal pydantic model
|
|
||||||
and later calling `save()` method.
|
|
||||||
|
|
||||||
`save()` can also be used to persist changes that you made to the model, but only if the primary key is not set or the model does not exist in database.
|
|
||||||
|
|
||||||
The `save()` method does not check if the model exists in db, so if it does you will get a integrity error from your selected db backend if trying to save model with already existing primary key.
|
|
||||||
|
|
||||||
```python
|
|
||||||
track = Track(name='The Bird')
|
|
||||||
await track.save() # will persist the model in database
|
|
||||||
|
|
||||||
track = await Track.objects.get(name='The Bird')
|
|
||||||
await track.save() # will raise integrity error as pk is populated
|
|
||||||
```
|
|
||||||
|
|
||||||
### update
|
|
||||||
|
|
||||||
`update(**kwargs) -> self`
|
|
||||||
|
|
||||||
You can update models by using `QuerySet.update()` method or by updating your model attributes (fields) and calling `update()` method.
|
|
||||||
|
|
||||||
If you try to update a model without a primary key set a `ModelPersistenceError` exception will be thrown.
|
|
||||||
|
|
||||||
To persist a newly created model use `save()` or `upsert(**kwargs)` methods.
|
|
||||||
|
|
||||||
```python
|
|
||||||
track = await Track.objects.get(name='The Bird')
|
|
||||||
await track.update(name='The Bird Strikes Again')
|
|
||||||
```
|
|
||||||
|
|
||||||
### upsert
|
|
||||||
|
|
||||||
`upsert(**kwargs) -> self`
|
|
||||||
|
|
||||||
It's an proxy to either `save()` or `update(**kwargs)` methods described above.
|
|
||||||
|
|
||||||
If the primary key is set -> the `update` method will be called.
|
|
||||||
|
|
||||||
If the pk is not set the `save()` method will be called.
|
|
||||||
|
|
||||||
```python
|
|
||||||
track = Track(name='The Bird')
|
|
||||||
await track.upsert() # will call save as the pk is empty
|
|
||||||
|
|
||||||
track = await Track.objects.get(name='The Bird')
|
|
||||||
await track.upsert(name='The Bird Strikes Again') # will call update as pk is already populated
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### delete
|
|
||||||
|
|
||||||
You can delete models by using `QuerySet.delete()` method or by using your model and calling `delete()` method.
|
|
||||||
|
|
||||||
```python
|
|
||||||
track = await Track.objects.get(name='The Bird')
|
|
||||||
await track.delete() # will delete the model from database
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Note that that `track` object stays the same, only record in the database is removed.
|
|
||||||
|
|
||||||
### save_related
|
|
||||||
|
|
||||||
`save_related(follow: bool = False) -> None`
|
|
||||||
|
|
||||||
Method goes through all relations of the `Model` on which the method is called,
|
|
||||||
and calls `upsert()` method on each model that is **not** saved.
|
|
||||||
|
|
||||||
To understand when a model is saved check [save status][save status] section above.
|
|
||||||
|
|
||||||
By default the `save_related` method saved 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 save all of them in the relation tree.
|
|
||||||
|
|
||||||
!!!warning
|
|
||||||
To avoid circular updates with `follow=True` set, `save_related` keeps a set of already visited Models,
|
|
||||||
and won't perform nested `save_related` on Models that were already visited.
|
|
||||||
|
|
||||||
So if you have a diamond or circular relations types you need to perform the updates in a manual way.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# in example like this the second Street (coming from City) won't be save_related, so ZipCode won't be updated
|
|
||||||
Street -> District -> City -> Street -> ZipCode
|
|
||||||
```
|
|
||||||
|
|
||||||
## Internals
|
|
||||||
|
|
||||||
Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals.
|
|
||||||
|
|
||||||
### Pydantic Model
|
|
||||||
|
|
||||||
All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models.
|
|
||||||
|
|
||||||
For example to list pydantic model fields you can:
|
|
||||||
|
|
||||||
```Python hl_lines="20"
|
|
||||||
--8<-- "../docs_src/models/docs003.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Note how the primary key `id` field is optional as `Integer` primary key by default has `autoincrement` set to `True`.
|
|
||||||
|
|
||||||
!!!info
|
|
||||||
For more options visit official [pydantic][pydantic] documentation.
|
|
||||||
|
|
||||||
### Sqlalchemy Table
|
|
||||||
|
|
||||||
To access auto created sqlalchemy table you can use `Model.Meta.table` parameter
|
|
||||||
|
|
||||||
For example to list table columns you can:
|
|
||||||
|
|
||||||
```Python hl_lines="20"
|
|
||||||
--8<-- "../docs_src/models/docs004.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
You can access table primary key name by `Course.Meta.pkname`
|
|
||||||
|
|
||||||
!!!info
|
|
||||||
For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation.
|
|
||||||
|
|
||||||
### Fields Definition
|
|
||||||
|
|
||||||
To access ormar `Fields` you can use `Model.Meta.model_fields` parameter
|
|
||||||
|
|
||||||
For example to list table model fields you can:
|
|
||||||
|
|
||||||
```Python hl_lines="20"
|
|
||||||
--8<-- "../docs_src/models/docs005.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!info
|
|
||||||
Note that fields stored on a model are `classes` not `instances`.
|
|
||||||
|
|
||||||
So if you print just model fields you will get:
|
|
||||||
|
|
||||||
`{'id': <class 'ormar.fields.model_fields.Integer'>, `
|
|
||||||
|
|
||||||
`'name': <class 'ormar.fields.model_fields.String'>, `
|
|
||||||
|
|
||||||
`'completed': <class 'ormar.fields.model_fields.Boolean'>}`
|
|
||||||
|
|
||||||
|
|
||||||
[fields]: ./fields.md
|
|
||||||
[relations]: ./relations.md
|
|
||||||
[queries]: ./queries.md
|
|
||||||
[pydantic]: https://pydantic-docs.helpmanual.io/
|
[pydantic]: https://pydantic-docs.helpmanual.io/
|
||||||
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
||||||
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
|
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
|
||||||
@ -766,5 +431,5 @@ For example to list table model fields you can:
|
|||||||
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||||
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
|
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
|
||||||
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
||||||
[save status]: ../models/#model-save-status
|
[save status]: ../models/index/#model-save-status
|
||||||
[Internals]: #internals
|
[Internals]: ../models/internals.md
|
||||||
70
docs/models/internals.md
Normal file
70
docs/models/internals.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Internals
|
||||||
|
|
||||||
|
Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals.
|
||||||
|
|
||||||
|
## Pydantic Model
|
||||||
|
|
||||||
|
All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models.
|
||||||
|
|
||||||
|
For example to list pydantic model fields you can:
|
||||||
|
|
||||||
|
```Python hl_lines="20"
|
||||||
|
--8<-- "../docs_src/models/docs003.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Note how the primary key `id` field is optional as `Integer` primary key by default has `autoincrement` set to `True`.
|
||||||
|
|
||||||
|
!!!info
|
||||||
|
For more options visit official [pydantic][pydantic] documentation.
|
||||||
|
|
||||||
|
## Sqlalchemy Table
|
||||||
|
|
||||||
|
To access auto created sqlalchemy table you can use `Model.Meta.table` parameter
|
||||||
|
|
||||||
|
For example to list table columns you can:
|
||||||
|
|
||||||
|
```Python hl_lines="20"
|
||||||
|
--8<-- "../docs_src/models/docs004.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
You can access table primary key name by `Course.Meta.pkname`
|
||||||
|
|
||||||
|
!!!info
|
||||||
|
For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation.
|
||||||
|
|
||||||
|
## Fields Definition
|
||||||
|
|
||||||
|
To access ormar `Fields` you can use `Model.Meta.model_fields` parameter
|
||||||
|
|
||||||
|
For example to list table model fields you can:
|
||||||
|
|
||||||
|
```Python hl_lines="20"
|
||||||
|
--8<-- "../docs_src/models/docs005.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!info
|
||||||
|
Note that fields stored on a model are `classes` not `instances`.
|
||||||
|
|
||||||
|
So if you print just model fields you will get:
|
||||||
|
|
||||||
|
`{'id': <class 'ormar.fields.model_fields.Integer'>, `
|
||||||
|
|
||||||
|
`'name': <class 'ormar.fields.model_fields.String'>, `
|
||||||
|
|
||||||
|
`'completed': <class 'ormar.fields.model_fields.Boolean'>}`
|
||||||
|
|
||||||
|
|
||||||
|
[fields]: ./fields.md
|
||||||
|
[relations]: ./relations/index.md
|
||||||
|
[queries]: ./queries.md
|
||||||
|
[pydantic]: https://pydantic-docs.helpmanual.io/
|
||||||
|
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
||||||
|
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
|
||||||
|
[databases]: https://github.com/encode/databases
|
||||||
|
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||||
|
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
|
||||||
|
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
||||||
|
[save status]: ../models/#model-save-status
|
||||||
|
[Internals]: #internals
|
||||||
130
docs/models/methods.md
Normal file
130
docs/models/methods.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Model methods
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Main interaction with the databases is exposed through a `QuerySet` object exposed on
|
||||||
|
each model as `Model.objects` similar to the django orm.
|
||||||
|
|
||||||
|
To read more about **quering, joining tables, excluding fields etc. visit [queries][queries] section.**
|
||||||
|
|
||||||
|
Each model instance have a set of methods to `save`, `update` or `load` itself.
|
||||||
|
|
||||||
|
Available methods are described below.
|
||||||
|
|
||||||
|
## 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. You can load the related model by calling `load()` method.
|
||||||
|
|
||||||
|
`load()` can also be used to refresh the model from the database (if it was changed by some other process).
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
`save() -> self`
|
||||||
|
|
||||||
|
You can create new models by using `QuerySet.create()` method or by initializing your model as a normal pydantic model
|
||||||
|
and later calling `save()` method.
|
||||||
|
|
||||||
|
`save()` can also be used to persist changes that you made to the model, but only if the primary key is not set or the model does not exist in database.
|
||||||
|
|
||||||
|
The `save()` method does not check if the model exists in db, so if it does you will get a integrity error from your selected db backend if trying to save model with already existing primary key.
|
||||||
|
|
||||||
|
```python
|
||||||
|
track = Track(name='The Bird')
|
||||||
|
await track.save() # will persist the model in database
|
||||||
|
|
||||||
|
track = await Track.objects.get(name='The Bird')
|
||||||
|
await track.save() # will raise integrity error as pk is populated
|
||||||
|
```
|
||||||
|
|
||||||
|
## update
|
||||||
|
|
||||||
|
`update(**kwargs) -> self`
|
||||||
|
|
||||||
|
You can update models by using `QuerySet.update()` method or by updating your model attributes (fields) and calling `update()` method.
|
||||||
|
|
||||||
|
If you try to update a model without a primary key set a `ModelPersistenceError` exception will be thrown.
|
||||||
|
|
||||||
|
To persist a newly created model use `save()` or `upsert(**kwargs)` methods.
|
||||||
|
|
||||||
|
```python
|
||||||
|
track = await Track.objects.get(name='The Bird')
|
||||||
|
await track.update(name='The Bird Strikes Again')
|
||||||
|
```
|
||||||
|
|
||||||
|
## upsert
|
||||||
|
|
||||||
|
`upsert(**kwargs) -> self`
|
||||||
|
|
||||||
|
It's an proxy to either `save()` or `update(**kwargs)` methods described above.
|
||||||
|
|
||||||
|
If the primary key is set -> the `update` method will be called.
|
||||||
|
|
||||||
|
If the pk is not set the `save()` method will be called.
|
||||||
|
|
||||||
|
```python
|
||||||
|
track = Track(name='The Bird')
|
||||||
|
await track.upsert() # will call save as the pk is empty
|
||||||
|
|
||||||
|
track = await Track.objects.get(name='The Bird')
|
||||||
|
await track.upsert(name='The Bird Strikes Again') # will call update as pk is already populated
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## delete
|
||||||
|
|
||||||
|
You can delete models by using `QuerySet.delete()` method or by using your model and calling `delete()` method.
|
||||||
|
|
||||||
|
```python
|
||||||
|
track = await Track.objects.get(name='The Bird')
|
||||||
|
await track.delete() # will delete the model from database
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Note that that `track` object stays the same, only record in the database is removed.
|
||||||
|
|
||||||
|
## save_related
|
||||||
|
|
||||||
|
`save_related(follow: bool = False) -> None`
|
||||||
|
|
||||||
|
Method goes through all relations of the `Model` on which the method is called,
|
||||||
|
and calls `upsert()` method on each model that is **not** saved.
|
||||||
|
|
||||||
|
To understand when a model is saved check [save status][save status] section above.
|
||||||
|
|
||||||
|
By default the `save_related` method saved 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 save all of them in the relation tree.
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
To avoid circular updates with `follow=True` set, `save_related` keeps a set of already visited Models,
|
||||||
|
and won't perform nested `save_related` on Models that were already visited.
|
||||||
|
|
||||||
|
So if you have a diamond or circular relations types you need to perform the updates in a manual way.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# in example like this the second Street (coming from City) won't be save_related, so ZipCode won't be updated
|
||||||
|
Street -> District -> City -> Street -> ZipCode
|
||||||
|
```
|
||||||
|
|
||||||
|
[fields]: ../fields.md
|
||||||
|
[relations]: ../relations/index.md
|
||||||
|
[queries]: ../queries.md
|
||||||
|
[pydantic]: https://pydantic-docs.helpmanual.io/
|
||||||
|
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
||||||
|
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
|
||||||
|
[databases]: https://github.com/encode/databases
|
||||||
|
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||||
|
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
|
||||||
|
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
||||||
|
[save status]: ../models/index/#model-save-status
|
||||||
|
[Internals]: #internals
|
||||||
193
docs/models/migrations.md
Normal file
193
docs/models/migrations.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# Migrations
|
||||||
|
|
||||||
|
## Database Initialization
|
||||||
|
|
||||||
|
Note that all examples assume that you already have a database.
|
||||||
|
|
||||||
|
If that is not the case and you need to create your tables, that's super easy as `ormar` is using sqlalchemy for underlying table construction.
|
||||||
|
|
||||||
|
All you have to do is call `create_all()` like in the example below.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlalchemy
|
||||||
|
# get your database url in sqlalchemy format - same as used with databases instance used in Model definition
|
||||||
|
engine = sqlalchemy.create_engine("sqlite:///test.db")
|
||||||
|
# note that this has to be the same metadata that is used in ormar Models definition
|
||||||
|
metadata.create_all(engine)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also create single tables, sqlalchemy tables are exposed in `ormar.Meta` class.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlalchemy
|
||||||
|
# get your database url in sqlalchemy format - same as used with databases instance used in Model definition
|
||||||
|
engine = sqlalchemy.create_engine("sqlite:///test.db")
|
||||||
|
# Artist is an ormar model from previous examples
|
||||||
|
Artist.Meta.table.create(engine)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
You need to create the tables only once, so use a python console for that or remove the script from your production code after first use.
|
||||||
|
|
||||||
|
|
||||||
|
## Alembic usage
|
||||||
|
|
||||||
|
Likewise as with tables, since we base tables on sqlalchemy for migrations please use [alembic][alembic].
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
Use command line to reproduce this minimalistic example.
|
||||||
|
|
||||||
|
```python
|
||||||
|
alembic init alembic
|
||||||
|
alembic revision --autogenerate -m "made some changes"
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample env.py file
|
||||||
|
|
||||||
|
A quick example of alembic migrations should be something similar to:
|
||||||
|
|
||||||
|
When you have application structure like:
|
||||||
|
|
||||||
|
```
|
||||||
|
-> app
|
||||||
|
-> alembic (initialized folder - so run alembic init alembic inside app folder)
|
||||||
|
-> models (here are the models)
|
||||||
|
-> __init__.py
|
||||||
|
-> my_models.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `env.py` file (in alembic folder) can look something like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from logging.config import fileConfig
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
# add app folder to system path (alternative is running it from parent folder with python -m ...)
|
||||||
|
myPath = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, myPath + '/../../')
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here (the one used in ormar)
|
||||||
|
# for 'autogenerate' support
|
||||||
|
from app.models.my_models import metadata
|
||||||
|
target_metadata = metadata
|
||||||
|
|
||||||
|
|
||||||
|
# set your url here or import from settings
|
||||||
|
# note that by default url is in saved sqlachemy.url variable in alembic.ini file
|
||||||
|
URL = "sqlite:///test.db"
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
context.configure(
|
||||||
|
url=URL,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
# if you use UUID field set also this param
|
||||||
|
# the prefix has to match sqlalchemy import name in alembic
|
||||||
|
# that can be set by sqlalchemy_module_prefix option (default 'sa.')
|
||||||
|
user_module_prefix='sa.'
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
connectable = create_engine(URL)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
# if you use UUID field set also this param
|
||||||
|
# the prefix has to match sqlalchemy import name in alembic
|
||||||
|
# that can be set by sqlalchemy_module_prefix option (default 'sa.')
|
||||||
|
user_module_prefix='sa.'
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Excluding tables
|
||||||
|
|
||||||
|
You can also include/exclude specific tables with `include_object` parameter passed to `context.configure`. That should be a function returning `True/False` for given objects.
|
||||||
|
|
||||||
|
A sample function excluding tables starting with `data_` in name unless it's 'data_jobs':
|
||||||
|
```python
|
||||||
|
def include_object(object, name, type_, reflected, compare_to):
|
||||||
|
if name and name.startswith('data_') and name not in ['data_jobs']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
Function parameters for `include_objects` (you can change the name) are required and defined in alembic
|
||||||
|
to check what they do check the [alembic][alembic] documentation
|
||||||
|
|
||||||
|
And you pass it into context like (both in online and offline):
|
||||||
|
```python
|
||||||
|
context.configure(
|
||||||
|
url=URL,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
user_module_prefix='sa.',
|
||||||
|
include_object=include_object
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!info
|
||||||
|
You can read more about table creation, altering and migrations in [sqlalchemy table creation][sqlalchemy table creation] documentation.
|
||||||
|
|
||||||
|
[fields]: ./fields.md
|
||||||
|
[relations]: ./relations/index.md
|
||||||
|
[queries]: ./queries.md
|
||||||
|
[pydantic]: https://pydantic-docs.helpmanual.io/
|
||||||
|
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
||||||
|
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
|
||||||
|
[databases]: https://github.com/encode/databases
|
||||||
|
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||||
|
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
|
||||||
|
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
||||||
|
[save status]: ../models/index/#model-save-status
|
||||||
|
[Internals]: #internals
|
||||||
@ -701,5 +701,5 @@ assert owner.toys[1].name == "Toy 1"
|
|||||||
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
|
[models]: ./models/index.md
|
||||||
[relations]: ./relations.md
|
[relations]: ./relations/index.md
|
||||||
@ -1,430 +0,0 @@
|
|||||||
# Relations
|
|
||||||
|
|
||||||
## Defining a relationship
|
|
||||||
|
|
||||||
### ForeignKey
|
|
||||||
|
|
||||||
`ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class.
|
|
||||||
|
|
||||||
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="29"
|
|
||||||
--8<-- "../docs_src/fields/docs003.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Reverse Relation
|
|
||||||
|
|
||||||
`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="29 35"
|
|
||||||
--8<-- "../docs_src/fields/docs001.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
Reverse relation exposes API to manage related objects also from parent side.
|
|
||||||
|
|
||||||
##### add
|
|
||||||
|
|
||||||
Adding child model from parent side causes adding related model to currently loaded parent relation,
|
|
||||||
as well as sets child's model foreign key value and updates the model.
|
|
||||||
|
|
||||||
```python
|
|
||||||
department = await Department(name="Science").save()
|
|
||||||
course = Course(name="Math", completed=False) # note - not saved
|
|
||||||
|
|
||||||
await department.courses.add(course)
|
|
||||||
assert course.pk is not None # child model was saved
|
|
||||||
# relation on child model is set and FK column saved in db
|
|
||||||
assert courses.department == department
|
|
||||||
# relation on parent model is also set
|
|
||||||
assert department.courses[0] == course
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!warning
|
|
||||||
If you want to add child model on related model the primary key value for parent model **has to exist in database**.
|
|
||||||
|
|
||||||
Otherwise ormar will raise RelationshipInstanceError as it cannot set child's ForeignKey column value
|
|
||||||
if parent model has no primary key value.
|
|
||||||
|
|
||||||
That means that in example above the department has to be saved before you can call `department.courses.add()`.
|
|
||||||
|
|
||||||
##### remove
|
|
||||||
|
|
||||||
Removal of the related model one by one.
|
|
||||||
|
|
||||||
In reverse relation calling `remove()` does not remove the child model, but instead nulls it ForeignKey value.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# continuing from above
|
|
||||||
await department.courses.remove(course)
|
|
||||||
assert len(department.courses) == 0
|
|
||||||
# course still exists and was saved in remove
|
|
||||||
assert course.pk is not None
|
|
||||||
assert course.department is None
|
|
||||||
|
|
||||||
# to remove child from db
|
|
||||||
await course.delete()
|
|
||||||
```
|
|
||||||
|
|
||||||
But if you want to clear the relation and delete the child at the same time you can issue:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# this will not only clear the relation
|
|
||||||
# but also delete related course from db
|
|
||||||
await department.courses.remove(course, keep_reversed=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
##### clear
|
|
||||||
|
|
||||||
Removal of all related models in one call.
|
|
||||||
|
|
||||||
Like remove by default `clear()` nulls the ForeigKey column on child model (all, not matter if they are loaded or not).
|
|
||||||
|
|
||||||
```python
|
|
||||||
# nulls department column on all courses related to this department
|
|
||||||
await department.courses.clear()
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to remove the children altogether from the database, set `keep_reversed=False`
|
|
||||||
|
|
||||||
```python
|
|
||||||
# deletes from db all courses related to this department
|
|
||||||
await department.courses.clear(keep_reversed=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 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
|
|
||||||
|
|
||||||
But you can overwrite this name by providing `related_name` parameter like below:
|
|
||||||
|
|
||||||
```Python hl_lines="29 35"
|
|
||||||
--8<-- "../docs_src/fields/docs002.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
The reverse relation on access returns list of `wekref.proxy` to avoid circular references.
|
|
||||||
|
|
||||||
|
|
||||||
### Relation Setup
|
|
||||||
|
|
||||||
You have several ways to set-up a relationship connection.
|
|
||||||
|
|
||||||
#### `Model` instance
|
|
||||||
|
|
||||||
The most obvious one is to pass a related `Model` instance to the constructor.
|
|
||||||
|
|
||||||
```Python hl_lines="34-35"
|
|
||||||
--8<-- "../docs_src/relations/docs001.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Primary key value
|
|
||||||
|
|
||||||
You can setup the relation also with just the pk column value of the related model.
|
|
||||||
|
|
||||||
```Python hl_lines="37-38"
|
|
||||||
--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="40-41"
|
|
||||||
--8<-- "../docs_src/relations/docs001.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### None
|
|
||||||
|
|
||||||
Finally you can explicitly set it to None (default behavior if no value passed).
|
|
||||||
|
|
||||||
```Python hl_lines="43-44"
|
|
||||||
--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.
|
|
||||||
|
|
||||||
|
|
||||||
### ManyToMany
|
|
||||||
|
|
||||||
`ManyToMany(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
|
|
||||||
--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")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### add
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
#### 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]
|
|
||||||
|
|
||||||
### QuerySetProxy
|
|
||||||
|
|
||||||
When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models.
|
|
||||||
|
|
||||||
But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model.
|
|
||||||
|
|
||||||
!!!note
|
|
||||||
By default exposed QuerySet is already filtered to return only `Models` related to parent `Model`.
|
|
||||||
|
|
||||||
So if you issue `post.categories.all()` you will get all categories related to that post, not all in table.
|
|
||||||
|
|
||||||
!!!note
|
|
||||||
Note that when accessing QuerySet API methods through QuerysetProxy you don't
|
|
||||||
need to use `objects` attribute like in normal queries.
|
|
||||||
|
|
||||||
So note that it's `post.categories.all()` and **not** `post.categories.objects.all()`.
|
|
||||||
|
|
||||||
To learn more about available QuerySet methods visit [queries][queries]
|
|
||||||
|
|
||||||
!!!warning
|
|
||||||
Querying related models from ManyToMany cleans list of related models loaded on parent model:
|
|
||||||
|
|
||||||
Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
|
|
||||||
|
|
||||||
Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()`
|
|
||||||
-> will load only 2 children and now `assert len(post.categories) == 2`
|
|
||||||
|
|
||||||
This happens for all QuerysetProxy methods returning data: `get`, `all` and `first` and in `get_or_create` if model already exists.
|
|
||||||
|
|
||||||
Note that value returned by `create` or created in `get_or_create` and `update_or_create`
|
|
||||||
if model does not exist will be added to relation list (not clearing it).
|
|
||||||
|
|
||||||
#### get
|
|
||||||
|
|
||||||
`get(**kwargs): -> Model`
|
|
||||||
|
|
||||||
To grab just one of related models filtered by name you can use `get(**kwargs)` method.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# grab one category
|
|
||||||
assert news == await post.categories.get(name="News")
|
|
||||||
|
|
||||||
# note that method returns the category so you can grab this value
|
|
||||||
# but it also modifies list of related models in place
|
|
||||||
# so regardless of what was previously loaded on parent model
|
|
||||||
# now it has only one value -> just loaded with get() call
|
|
||||||
assert len(post.categories) == 1
|
|
||||||
assert post.categories[0] == news
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [get][get]
|
|
||||||
|
|
||||||
#### all
|
|
||||||
|
|
||||||
`all(**kwargs) -> List[Optional["Model"]]`
|
|
||||||
|
|
||||||
To get a list of related models use `all()` method.
|
|
||||||
|
|
||||||
Note that you can filter the queryset, select related, exclude fields etc. like in normal query.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 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
|
|
||||||
Read more in queries documentation [all][all]
|
|
||||||
|
|
||||||
#### create
|
|
||||||
|
|
||||||
`create(**kwargs): -> Model`
|
|
||||||
|
|
||||||
Create related `Model` directly from parent `Model`.
|
|
||||||
|
|
||||||
The link table is automatically populated, as well as relation ids in the database.
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [create][create]
|
|
||||||
|
|
||||||
|
|
||||||
#### get_or_create
|
|
||||||
|
|
||||||
`get_or_create(**kwargs) -> Model`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [get_or_create][get_or_create]
|
|
||||||
|
|
||||||
#### update_or_create
|
|
||||||
|
|
||||||
`update_or_create(**kwargs) -> Model`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [update_or_create][update_or_create]
|
|
||||||
|
|
||||||
#### filter
|
|
||||||
|
|
||||||
`filter(**kwargs) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [filter][filter]
|
|
||||||
|
|
||||||
#### exclude
|
|
||||||
|
|
||||||
`exclude(**kwargs) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [exclude][exclude]
|
|
||||||
|
|
||||||
#### select_related
|
|
||||||
|
|
||||||
`select_related(related: Union[List, str]) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [select_related][select_related]
|
|
||||||
|
|
||||||
#### prefetch_related
|
|
||||||
|
|
||||||
`prefetch_related(related: Union[List, str]) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [prefetch_related][prefetch_related]
|
|
||||||
|
|
||||||
#### limit
|
|
||||||
|
|
||||||
`limit(limit_count: int) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [limit][limit]
|
|
||||||
|
|
||||||
#### offset
|
|
||||||
|
|
||||||
`offset(offset: int) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [offset][offset]
|
|
||||||
|
|
||||||
#### count
|
|
||||||
|
|
||||||
`count() -> int`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [count][count]
|
|
||||||
|
|
||||||
#### exists
|
|
||||||
|
|
||||||
`exists() -> bool`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [exists][exists]
|
|
||||||
|
|
||||||
#### fields
|
|
||||||
|
|
||||||
`fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [fields][fields]
|
|
||||||
|
|
||||||
#### exclude_fields
|
|
||||||
|
|
||||||
`exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [exclude_fields][exclude_fields]
|
|
||||||
|
|
||||||
#### order_by
|
|
||||||
|
|
||||||
`order_by(columns:Union[List, str]) -> QuerySet`
|
|
||||||
|
|
||||||
!!!tip
|
|
||||||
Read more in queries documentation [order_by][order_by]
|
|
||||||
|
|
||||||
|
|
||||||
[queries]: ./queries.md
|
|
||||||
[querysetproxy]: ./relations.md#querysetproxy-methods
|
|
||||||
[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
|
|
||||||
177
docs/relations/foreign-key.md
Normal file
177
docs/relations/foreign-key.md
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# ForeignKey
|
||||||
|
|
||||||
|
`ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class.
|
||||||
|
|
||||||
|
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="29"
|
||||||
|
--8<-- "../docs_src/fields/docs003.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reverse Relation
|
||||||
|
|
||||||
|
`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="29 35"
|
||||||
|
--8<-- "../docs_src/fields/docs001.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse relation exposes API to manage related objects also from parent side.
|
||||||
|
|
||||||
|
### add
|
||||||
|
|
||||||
|
Adding child model from parent side causes adding related model to currently loaded parent relation,
|
||||||
|
as well as sets child's model foreign key value and updates the model.
|
||||||
|
|
||||||
|
```python
|
||||||
|
department = await Department(name="Science").save()
|
||||||
|
course = Course(name="Math", completed=False) # note - not saved
|
||||||
|
|
||||||
|
await department.courses.add(course)
|
||||||
|
assert course.pk is not None # child model was saved
|
||||||
|
# relation on child model is set and FK column saved in db
|
||||||
|
assert courses.department == department
|
||||||
|
# relation on parent model is also set
|
||||||
|
assert department.courses[0] == course
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
If you want to add child model on related model the primary key value for parent model **has to exist in database**.
|
||||||
|
|
||||||
|
Otherwise ormar will raise RelationshipInstanceError as it cannot set child's ForeignKey column value
|
||||||
|
if parent model has no primary key value.
|
||||||
|
|
||||||
|
That means that in example above the department has to be saved before you can call `department.courses.add()`.
|
||||||
|
|
||||||
|
### remove
|
||||||
|
|
||||||
|
Removal of the related model one by one.
|
||||||
|
|
||||||
|
In reverse relation calling `remove()` does not remove the child model, but instead nulls it ForeignKey value.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# continuing from above
|
||||||
|
await department.courses.remove(course)
|
||||||
|
assert len(department.courses) == 0
|
||||||
|
# course still exists and was saved in remove
|
||||||
|
assert course.pk is not None
|
||||||
|
assert course.department is None
|
||||||
|
|
||||||
|
# to remove child from db
|
||||||
|
await course.delete()
|
||||||
|
```
|
||||||
|
|
||||||
|
But if you want to clear the relation and delete the child at the same time you can issue:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# this will not only clear the relation
|
||||||
|
# but also delete related course from db
|
||||||
|
await department.courses.remove(course, keep_reversed=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clear
|
||||||
|
|
||||||
|
Removal of all related models in one call.
|
||||||
|
|
||||||
|
Like remove by default `clear()` nulls the ForeigKey column on child model (all, not matter if they are loaded or not).
|
||||||
|
|
||||||
|
```python
|
||||||
|
# nulls department column on all courses related to this department
|
||||||
|
await department.courses.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to remove the children altogether from the database, set `keep_reversed=False`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# deletes from db all courses related to this department
|
||||||
|
await department.courses.clear(keep_reversed=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
But you can overwrite this name by providing `related_name` parameter like below:
|
||||||
|
|
||||||
|
```Python hl_lines="29 35"
|
||||||
|
--8<-- "../docs_src/fields/docs002.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
The reverse relation on access returns list of `wekref.proxy` to avoid circular references.
|
||||||
|
|
||||||
|
|
||||||
|
## Relation Setup
|
||||||
|
|
||||||
|
You have several ways to set-up a relationship connection.
|
||||||
|
|
||||||
|
### `Model` instance
|
||||||
|
|
||||||
|
The most obvious one is to pass a related `Model` instance to the constructor.
|
||||||
|
|
||||||
|
```Python hl_lines="34-35"
|
||||||
|
--8<-- "../docs_src/relations/docs001.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Primary key value
|
||||||
|
|
||||||
|
You can setup the relation also with just the pk column value of the related model.
|
||||||
|
|
||||||
|
```Python hl_lines="37-38"
|
||||||
|
--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="40-41"
|
||||||
|
--8<-- "../docs_src/relations/docs001.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### None
|
||||||
|
|
||||||
|
Finally you can explicitly set it to None (default behavior if no value passed).
|
||||||
|
|
||||||
|
```Python hl_lines="43-44"
|
||||||
|
--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.
|
||||||
|
|
||||||
|
[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
|
||||||
7
docs/relations/index.md
Normal file
7
docs/relations/index.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Relations
|
||||||
|
|
||||||
|
## ForeignKey
|
||||||
|
|
||||||
|
## Reverse ForeignKey
|
||||||
|
|
||||||
|
##ManyToMany
|
||||||
81
docs/relations/many-to-many.md
Normal file
81
docs/relations/many-to-many.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# ManyToMany
|
||||||
|
|
||||||
|
`ManyToMany(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
|
||||||
|
--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")
|
||||||
|
```
|
||||||
|
|
||||||
|
### add
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
### 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]
|
||||||
|
|
||||||
|
|
||||||
|
[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
|
||||||
203
docs/relations/queryset-proxy.md
Normal file
203
docs/relations/queryset-proxy.md
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# QuerySetProxy
|
||||||
|
|
||||||
|
When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models.
|
||||||
|
|
||||||
|
But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model.
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
By default exposed QuerySet is already filtered to return only `Models` related to parent `Model`.
|
||||||
|
|
||||||
|
So if you issue `post.categories.all()` you will get all categories related to that post, not all in table.
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
Note that when accessing QuerySet API methods through QuerysetProxy you don't
|
||||||
|
need to use `objects` attribute like in normal queries.
|
||||||
|
|
||||||
|
So note that it's `post.categories.all()` and **not** `post.categories.objects.all()`.
|
||||||
|
|
||||||
|
To learn more about available QuerySet methods visit [queries][queries]
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
Querying related models from ManyToMany cleans list of related models loaded on parent model:
|
||||||
|
|
||||||
|
Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
|
||||||
|
|
||||||
|
Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()`
|
||||||
|
-> will load only 2 children and now `assert len(post.categories) == 2`
|
||||||
|
|
||||||
|
This happens for all QuerysetProxy methods returning data: `get`, `all` and `first` and in `get_or_create` if model already exists.
|
||||||
|
|
||||||
|
Note that value returned by `create` or created in `get_or_create` and `update_or_create`
|
||||||
|
if model does not exist will be added to relation list (not clearing it).
|
||||||
|
|
||||||
|
## get
|
||||||
|
|
||||||
|
`get(**kwargs): -> Model`
|
||||||
|
|
||||||
|
To grab just one of related models filtered by name you can use `get(**kwargs)` method.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# grab one category
|
||||||
|
assert news == await post.categories.get(name="News")
|
||||||
|
|
||||||
|
# note that method returns the category so you can grab this value
|
||||||
|
# but it also modifies list of related models in place
|
||||||
|
# so regardless of what was previously loaded on parent model
|
||||||
|
# now it has only one value -> just loaded with get() call
|
||||||
|
assert len(post.categories) == 1
|
||||||
|
assert post.categories[0] == news
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [get][get]
|
||||||
|
|
||||||
|
## all
|
||||||
|
|
||||||
|
`all(**kwargs) -> List[Optional["Model"]]`
|
||||||
|
|
||||||
|
To get a list of related models use `all()` method.
|
||||||
|
|
||||||
|
Note that you can filter the queryset, select related, exclude fields etc. like in normal query.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
Read more in queries documentation [all][all]
|
||||||
|
|
||||||
|
## create
|
||||||
|
|
||||||
|
`create(**kwargs): -> Model`
|
||||||
|
|
||||||
|
Create related `Model` directly from parent `Model`.
|
||||||
|
|
||||||
|
The link table is automatically populated, as well as relation ids in the database.
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [create][create]
|
||||||
|
|
||||||
|
|
||||||
|
## get_or_create
|
||||||
|
|
||||||
|
`get_or_create(**kwargs) -> Model`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [get_or_create][get_or_create]
|
||||||
|
|
||||||
|
## update_or_create
|
||||||
|
|
||||||
|
`update_or_create(**kwargs) -> Model`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [update_or_create][update_or_create]
|
||||||
|
|
||||||
|
## filter
|
||||||
|
|
||||||
|
`filter(**kwargs) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [filter][filter]
|
||||||
|
|
||||||
|
## exclude
|
||||||
|
|
||||||
|
`exclude(**kwargs) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [exclude][exclude]
|
||||||
|
|
||||||
|
## select_related
|
||||||
|
|
||||||
|
`select_related(related: Union[List, str]) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [select_related][select_related]
|
||||||
|
|
||||||
|
## prefetch_related
|
||||||
|
|
||||||
|
`prefetch_related(related: Union[List, str]) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [prefetch_related][prefetch_related]
|
||||||
|
|
||||||
|
## limit
|
||||||
|
|
||||||
|
`limit(limit_count: int) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [limit][limit]
|
||||||
|
|
||||||
|
## offset
|
||||||
|
|
||||||
|
`offset(offset: int) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [offset][offset]
|
||||||
|
|
||||||
|
## count
|
||||||
|
|
||||||
|
`count() -> int`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [count][count]
|
||||||
|
|
||||||
|
## exists
|
||||||
|
|
||||||
|
`exists() -> bool`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [exists][exists]
|
||||||
|
|
||||||
|
## fields
|
||||||
|
|
||||||
|
`fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [fields][fields]
|
||||||
|
|
||||||
|
## exclude_fields
|
||||||
|
|
||||||
|
`exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [exclude_fields][exclude_fields]
|
||||||
|
|
||||||
|
## order_by
|
||||||
|
|
||||||
|
`order_by(columns:Union[List, str]) -> QuerySet`
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
Read more in queries documentation [order_by][order_by]
|
||||||
|
|
||||||
|
|
||||||
|
[queries]: ../queries.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
|
||||||
22
mkdocs.yml
22
mkdocs.yml
@ -3,9 +3,19 @@ site_description: A simple async ORM with fastapi in mind and pydantic validatio
|
|||||||
nav:
|
nav:
|
||||||
- Overview: index.md
|
- Overview: index.md
|
||||||
- Installation: install.md
|
- Installation: install.md
|
||||||
- Models: models.md
|
- Models:
|
||||||
- Fields: fields.md
|
- Definition: models/index.md
|
||||||
- Relations: relations.md
|
- Methods: models/methods.md
|
||||||
|
- Migrations: models/migrations.md
|
||||||
|
- Internals: models/internals.md
|
||||||
|
- Fields:
|
||||||
|
- Fields types: fields/field-types.md
|
||||||
|
- Common parameters: fields/common-parameters.md
|
||||||
|
- Relations:
|
||||||
|
- relations/index.md
|
||||||
|
- relations/foreign-key.md
|
||||||
|
- relations/many-to-many.md
|
||||||
|
- relations/queryset-proxy.md
|
||||||
- Queries: queries.md
|
- Queries: queries.md
|
||||||
- Signals: signals.md
|
- Signals: signals.md
|
||||||
- Use with Fastapi: fastapi.md
|
- Use with Fastapi: fastapi.md
|
||||||
@ -15,9 +25,9 @@ nav:
|
|||||||
- Release Notes: releases.md
|
- Release Notes: releases.md
|
||||||
repo_name: collerek/ormar
|
repo_name: collerek/ormar
|
||||||
repo_url: https://github.com/collerek/ormar
|
repo_url: https://github.com/collerek/ormar
|
||||||
google_analytics:
|
#google_analytics:
|
||||||
- UA-72514911-3
|
# - UA-72514911-3
|
||||||
- auto
|
# - auto
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
highlightjs: true
|
highlightjs: true
|
||||||
|
|||||||
Reference in New Issue
Block a user