From 2e7cad722bba4fa4983e8c838ab694d5605f91d4 Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 10 Dec 2020 18:10:08 +0100 Subject: [PATCH] reorganize docs into sections for easier navigation part 1 --- docs/fastapi.md | 4 +- docs/fields/common-parameters.md | 126 +++++++ docs/{fields.md => fields/field-types.md} | 131 +------ docs/{models.md => models/index.md} | 345 +---------------- docs/models/internals.md | 70 ++++ docs/models/methods.md | 130 +++++++ docs/models/migrations.md | 193 ++++++++++ docs/queries.md | 4 +- docs/relations.md | 430 ---------------------- docs/relations/foreign-key.md | 177 +++++++++ docs/relations/index.md | 7 + docs/relations/many-to-many.md | 81 ++++ docs/relations/queryset-proxy.md | 203 ++++++++++ mkdocs.yml | 22 +- 14 files changed, 1017 insertions(+), 906 deletions(-) create mode 100644 docs/fields/common-parameters.md rename docs/{fields.md => fields/field-types.md} (53%) rename docs/{models.md => models/index.md} (58%) create mode 100644 docs/models/internals.md create mode 100644 docs/models/methods.md create mode 100644 docs/models/migrations.md delete mode 100644 docs/relations.md create mode 100644 docs/relations/foreign-key.md create mode 100644 docs/relations/index.md create mode 100644 docs/relations/many-to-many.md create mode 100644 docs/relations/queryset-proxy.md diff --git a/docs/fastapi.md b/docs/fastapi.md index d451fee..39ed99b 100644 --- a/docs/fastapi.md +++ b/docs/fastapi.md @@ -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 \ No newline at end of file diff --git a/docs/fields/common-parameters.md b/docs/fields/common-parameters.md new file mode 100644 index 0000000..eb47818 --- /dev/null +++ b/docs/fields/common-parameters.md @@ -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 \ No newline at end of file diff --git a/docs/fields.md b/docs/fields/field-types.md similarity index 53% rename from docs/fields.md rename to docs/fields/field-types.md index 0c09d74..10dcc24 100644 --- a/docs/fields.md +++ b/docs/fields/field-types.md @@ -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 \ No newline at end of file diff --git a/docs/models.md b/docs/models/index.md similarity index 58% rename from docs/models.md rename to docs/models/index.md index a29fb70..40ddb63 100644 --- a/docs/models.md +++ b/docs/models/index.md @@ -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': , ` - - `'name': , ` - - `'completed': }` - - -[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 \ No newline at end of file diff --git a/docs/models/internals.md b/docs/models/internals.md new file mode 100644 index 0000000..463d368 --- /dev/null +++ b/docs/models/internals.md @@ -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': , ` + + `'name': , ` + + `'completed': }` + + +[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 diff --git a/docs/models/methods.md b/docs/models/methods.md new file mode 100644 index 0000000..31a0b21 --- /dev/null +++ b/docs/models/methods.md @@ -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 diff --git a/docs/models/migrations.md b/docs/models/migrations.md new file mode 100644 index 0000000..6841014 --- /dev/null +++ b/docs/models/migrations.md @@ -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 diff --git a/docs/queries.md b/docs/queries.md index 1af4382..4191e33 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -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 \ No newline at end of file +[models]: ./models/index.md +[relations]: ./relations/index.md \ No newline at end of file diff --git a/docs/relations.md b/docs/relations.md deleted file mode 100644 index b3e548b..0000000 --- a/docs/relations.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docs/relations/foreign-key.md b/docs/relations/foreign-key.md new file mode 100644 index 0000000..6e16a5c --- /dev/null +++ b/docs/relations/foreign-key.md @@ -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 \ No newline at end of file diff --git a/docs/relations/index.md b/docs/relations/index.md new file mode 100644 index 0000000..465f479 --- /dev/null +++ b/docs/relations/index.md @@ -0,0 +1,7 @@ +# Relations + +## ForeignKey + +## Reverse ForeignKey + +##ManyToMany \ No newline at end of file diff --git a/docs/relations/many-to-many.md b/docs/relations/many-to-many.md new file mode 100644 index 0000000..9464e05 --- /dev/null +++ b/docs/relations/many-to-many.md @@ -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 \ No newline at end of file diff --git a/docs/relations/queryset-proxy.md b/docs/relations/queryset-proxy.md new file mode 100644 index 0000000..315dc68 --- /dev/null +++ b/docs/relations/queryset-proxy.md @@ -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 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 60b42b2..9bfd51e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,9 +3,19 @@ site_description: A simple async ORM with fastapi in mind and pydantic validatio nav: - Overview: index.md - Installation: install.md - - Models: models.md - - Fields: fields.md - - Relations: relations.md + - Models: + - Definition: models/index.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 - Signals: signals.md - Use with Fastapi: fastapi.md @@ -15,9 +25,9 @@ nav: - Release Notes: releases.md repo_name: collerek/ormar repo_url: https://github.com/collerek/ormar -google_analytics: - - UA-72514911-3 - - auto +#google_analytics: +# - UA-72514911-3 +# - auto theme: name: material highlightjs: true