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.
|
||||
|
||||
[fastapi]: https://fastapi.tiangolo.com/
|
||||
[models]: ./models.md
|
||||
[database initialization]: ../models/#database-initialization-migrations
|
||||
[models]: ./models/index.md
|
||||
[database initialization]: ./models/migrations.md
|
||||
[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.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
[relations]: ./relations.md
|
||||
[queries]: ./queries.md
|
||||
[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
|
||||
@ -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.
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
@ -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
|
||||
|
||||
## `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. 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
|
||||
[fields]: ../fields/field-types.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
|
||||
@ -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 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
|
||||
[save status]: ../models/index/#model-save-status
|
||||
[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()`
|
||||
|
||||
|
||||
[models]: ./models.md
|
||||
[relations]: ./relations.md
|
||||
[models]: ./models/index.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
|
||||
Reference in New Issue
Block a user