WIP - Pydantic v2 support (#1238)

* WIP

* WIP - make test_model_definition tests pass

* WIP - make test_model_methods pass

* WIP - make whole test suit at least run - failing 49/443 tests

* WIP fix part of the getting pydantic tests as types of fields are now kept in core schema and not on fieldsinfo

* WIP fix validation in update by creating individual fields validators, failing 36/443

* WIP fix __pydantic_extra__ in intializing model, fix test related to pydantic config checks, failing 32/442

* WIP - fix enum schema in model_json_schema, failing 31/442

* WIP - fix copying through model, fix setting pydantic fields on through, fix default config and inheriting from it, failing 26/442

* WIP fix tests checking pydantic schema, fix excluding parent fields, failing 21/442

* WIP some missed files

* WIP - fix validators inheritance and fix validators in generated pydantic, failing 17/442

* WIP - fix through models setting - only on reverse side of relation, but always on reverse side, failing 15/442

* WIP - fix through models setting - only on reverse side of relation, but always on reverse side, failing 15/442

* WIP - working on proper populating __dict__ for relations for new schema dumping, some work on openapi docs, failing 13/442

* WIP - remove property fields as pydantic has now computed_field on its own, failing 9/442

* WIP - fixes in docs, failing 8/442

* WIP - fix tests for largebinary schema, wrapped bytes fields fail in pydantic, will be fixed in pydantic-core, remaining is circural schema for related models, failing 6/442

* WIP - fix to pk only models in schemas

* Getting test suites to pass (#1249)

* wip, fixing tests

* iteration, fixing some more tests

* iteration, fixing some more tests

* adhere to comments

* adhere to comments

* remove unnecessary dict call, re-add getattribute for testing

* todo for reverse relationship

* adhere to comments, remove prints

* solve circular refs

* all tests pass 🎉

* remove 3.7 from tests

* add lint and type check jobs

* reforat with ruff, fix jobs

* rename jobs

* fix imports

* fix evaluate in py3.8

* partially fix coverage

* fix coverage, add more tests

* fix test ids

* fix test ids

* fix lint, fix docs, make docs fully working scripts, add test docs job

* fix pyproject

* pin py ver in test docs

* change dir in test docs

* fix pydantic warning hack

* rm poetry call in test_docs

* switch to pathlib in test docs

* remove coverage req test docs

* fix type check tests, fix part of types

* fix/skip next part of types

* fix next part of types

* fix next part of types

* fix coverage

* fix coverage

* fix type (bit dirty 🤷)

* fix some code smells

* change pre-commit

* tweak workflows

* remove no root from tests

* switch to full python path by passing sys.executable

* some small refactor in new base model, one sample test, change makefile

* small refactors to reduce complexity of methods

* temp add tests for prs against pydantic_v2

* remove all references to __fields__

* remove all references to construct, deprecate the method and update model_construct to be in line with pydantic

* deprecate dict and add model_dump, todo switch to model_dict in calls

* fix tests

* change to union

* change to union

* change to model_dump and model_dump_json from dict and json deprecated methods, deprecate them in ormar too

* finish switching dict() -> model_dump()

* finish switching json() -> model_dump_json()

* remove fully pydantic_only

* switch to extra for payment card, change missed json calls

* fix coverage - no more warnings internal

* fix coverage - no more warnings internal - part 2

* split model_construct into own and pydantic parts

* split determine pydantic field type

* change to new field validators

* fix benchmarks, add codspeed instead of pytest-benchmark, add action and gh workflow

* restore pytest-benchmark

* remove codspeed

* pin pydantic version, restore codspeed

* change on push to pydantic_v2 to trigger first one

* Use lifespan function instead of event (#1259)

* check return types

* fix imports order, set warnings=False on json that passes the dict, fix unnecessary loop in one of the test

* remove references to model's meta as it's now ormar config, rename related methods too

* filter out pydantic serializer warnings

* remove choices leftovers

* remove leftovers after property_fields, keep only enough to exclude them in initialization

* add migration guide

* fix meta references

* downgrade databases for now

* Change line numbers in documentation (#1265)

* proofread and fix the docs, part 1

* proofread and fix the docs for models

* proofread and fix the docs for fields

* proofread and fix the docs for relations

* proofread and fix rest of the docs, add release notes for 0.20

* create tables in new docs src

* cleanup old deps, uncomment docs publish on tag

* fix import reorder

---------

Co-authored-by: TouwaStar <30479449+TouwaStar@users.noreply.github.com>
Co-authored-by: Goran Mekić <meka@tilda.center>
This commit is contained in:
collerek
2024-03-23 19:28:28 +01:00
committed by GitHub
parent 3a206dd8dc
commit 500625f0ec
294 changed files with 8132 additions and 9311 deletions

View File

@ -9,7 +9,7 @@ They are being managed in the background and you do not have to create them on y
To build an ormar model you simply need to inherit a `ormar.Model` class.
```Python hl_lines="10"
```Python hl_lines="9"
--8<-- "../docs_src/models/docs001.py"
```
@ -23,7 +23,7 @@ Each table **has to** have a primary key column, which you specify by setting `p
Only one primary key column is allowed.
```Python hl_lines="15 16 17"
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
@ -42,15 +42,15 @@ id: int = ormar.Integer(primary_key=True, autoincrement=False)
#### Non Database Fields
Note that if you need a normal pydantic field in your model (used to store value on model or pass around some value) you can define a
field with parameter `pydantic_only=True`.
field like usual in pydantic.
Fields created like this are added to the `pydantic` model fields -> so are subject to validation according to `Field` type,
also appear in `dict()` and `json()` result.
also appear in `model_dump()` and `model_dump_json()` result.
The difference is that **those fields are not saved in the database**. So they won't be included in underlying sqlalchemy `columns`,
or `table` variables (check [Internals][Internals] section below to see how you can access those if you need).
Subsequently `pydantic_only` fields won't be included in migrations or any database operation (like `save`, `update` etc.)
Subsequently, pydantic fields won't be included in migrations or any database operation (like `save`, `update` etc.)
Fields like those can be passed around into payload in `fastapi` request and will be returned in `fastapi` response
(of course only if you set their value somewhere in your code as the value is **not** fetched from the db.
@ -58,30 +58,32 @@ If you pass a value in `fastapi` `request` and return the same instance that `fa
you should get back exactly same value in `response`.).
!!!warning
`pydantic_only=True` fields are always **Optional** and it cannot be changed (otherwise db load validation would fail)
pydantic fields have to be always **Optional** and it cannot be changed (otherwise db load validation would fail)
!!!tip
`pydantic_only=True` fields are a good solution if you need to pass additional information from outside of your API
(i.e. frontend). They are not stored in db but you can access them in your `APIRoute` code and they also have `pydantic` validation.
```Python hl_lines="18"
```Python hl_lines="19"
--8<-- "../docs_src/models/docs014.py"
```
If you combine `pydantic_only=True` field with `default` parameter and do not pass actual value in request you will always get default value.
If you set pydantic field with `default` parameter and do not pass actual value in request you will always get default value.
Since it can be a function you can set `default=datetime.datetime.now` and get current timestamp each time you call an endpoint etc.
#### Non Database Fields in Fastapi
!!!note
Note that both `pydantic_only` and `property_field` decorated field can be included/excluded in both `dict()` and `fastapi`
Note, that both pydantic and calculated_fields decorated field can be included/excluded in both `model_dump()` and `fastapi`
response with `include`/`exclude` and `response_model_include`/`response_model_exclude` accordingly.
```python
# <==related of code removed for clarity==>
# <==part of related code removed for clarity==>
base_ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
engine=sqlalchemy.create_engine(DATABASE_URL),
)
class User(ormar.Model):
class Meta:
tablename: str = "users2"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="users2")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255, nullable=False)
@ -89,18 +91,18 @@ class User(ormar.Model):
first_name: str = ormar.String(max_length=255)
last_name: str = ormar.String(max_length=255)
category: str = ormar.String(max_length=255, nullable=True)
timestamp: datetime.datetime = ormar.DateTime(
pydantic_only=True, default=datetime.datetime.now
timestamp: datetime.datetime = pydantic.Field(
default=datetime.datetime.now
)
# <==related of code removed for clarity==>
app =FastAPI()
# <==part of related code removed for clarity==>
app = FastAPI()
@app.post("/users/")
async def create_user(user: User):
return await user.save()
# <==related of code removed for clarity==>
# <==part of related code removed for clarity==>
def test_excluding_fields_in_endpoints():
client = TestClient(app)
@ -127,121 +129,7 @@ def test_excluding_fields_in_endpoints():
assert response.json().get("timestamp") == str(timestamp).replace(" ", "T")
# <==related of code removed for clarity==>
```
#### Property fields
Sometimes it's desirable to do some kind of calculation on the model instance. One of the most common examples can be concatenating
two or more fields. Imagine you have `first_name` and `last_name` fields on your model, but would like to have `full_name` in the result
of the `fastapi` query.
You can create a new `pydantic` model with a `method` that accepts only `self` (so like default python `@property`)
and populate it in your code.
But it's so common that `ormar` has you covered. You can "materialize" a `property_field` on you `Model`.
!!!warning
`property_field` fields are always **Optional** and it cannot be changed (otherwise db load validation would fail)
```Python hl_lines="20-22"
--8<-- "../docs_src/models/docs015.py"
```
!!!warning
The decorated function has to accept only one parameter, and that parameter have to be `self`.
If you try to decorate a function with more parameters `ormar` will raise `ModelDefinitionError`.
Sample:
```python
# will raise ModelDefinitionError
@property_field
def prefixed_name(self, prefix="prefix_"):
return 'custom_prefix__' + self.name
# will raise ModelDefinitionError
# (calling first param something else than 'self' is a bad practice anyway)
@property_field
def prefixed_name(instance):
return 'custom_prefix__' + self.name
```
Note that `property_field` decorated methods do not go through verification (but that might change in future) and are only available
in the response from `fastapi` and `dict()` and `json()` methods. You cannot pass a value for this field in the request
(or rather you can but it will be discarded by ormar so really no point but no Exception will be raised).
!!!note
Note that both `pydantic_only` and `property_field` decorated field can be included/excluded in both `dict()` and `fastapi`
response with `include`/`exclude` and `response_model_include`/`response_model_exclude` accordingly.
!!!tip
Note that `@property_field` decorator is designed to replace the python `@property` decorator, you do not have to combine them.
In theory you can cause `ormar` have a failsafe mechanism, but note that i.e. `mypy` will complain about re-decorating a property.
```python
# valid and working but unnecessary and mypy will complain
@property_field
@property
def prefixed_name(self):
return 'custom_prefix__' + self.name
```
```python
# <==related of code removed for clarity==>
def gen_pass(): # note: NOT production ready
choices = string.ascii_letters + string.digits + "!@#$%^&*()"
return "".join(random.choice(choices) for _ in range(20))
class RandomModel(ormar.Model):
class Meta:
tablename: str = "random_users"
metadata = metadata
database = database
include_props_in_dict = True
id: int = ormar.Integer(primary_key=True)
password: str = ormar.String(max_length=255, default=gen_pass)
first_name: str = ormar.String(max_length=255, default="John")
last_name: str = ormar.String(max_length=255)
created_date: datetime.datetime = ormar.DateTime(
server_default=sqlalchemy.func.now()
)
@property_field
def full_name(self) -> str:
return " ".join([self.first_name, self.last_name])
# <==related of code removed for clarity==>
app =FastAPI()
# explicitly exclude property_field in this endpoint
@app.post("/random/", response_model=RandomModel, response_model_exclude={"full_name"})
async def create_user(user: RandomModel):
return await user.save()
# <==related of code removed for clarity==>
def test_excluding_property_field_in_endpoints2():
client = TestClient(app)
with client as client:
RandomModel.Meta.include_props_in_dict = True
user3 = {"last_name": "Test"}
response = client.post("/random3/", json=user3)
assert list(response.json().keys()) == [
"id",
"password",
"first_name",
"last_name",
"created_date",
]
# despite being decorated with property_field if you explicitly exclude it it will be gone
assert response.json().get("full_name") is None
# <==related of code removed for clarity==>
# <==part of related code removed for clarity==>
```
#### Fields names vs Column names
@ -252,25 +140,25 @@ If for whatever reason you prefer to change the name in the database but keep th
with specifying `name` parameter during Field declaration
Here you have a sample model with changed names
```Python hl_lines="16-19"
```Python hl_lines="18-21"
--8<-- "../docs_src/models/docs008.py"
```
Note that you can also change the ForeignKey column name
```Python hl_lines="21"
```Python hl_lines="34"
--8<-- "../docs_src/models/docs009.py"
```
But for now you cannot change the ManyToMany column names as they go through other Model anyway.
```Python hl_lines="28"
```Python hl_lines="43"
--8<-- "../docs_src/models/docs010.py"
```
## Overwriting the default QuerySet
### Overwriting the default QuerySet
If you want to customize the queries run by ormar you can define your own queryset class (that extends the ormar `QuerySet`) in your model class, default one is simply the `QuerySet`
You can provide a new class in `Meta` configuration of your class as `queryset_class` parameter.
You can provide a new class in `ormar_config` of your class as `queryset_class` parameter.
```python
import ormar
@ -288,12 +176,10 @@ class MyQuerySetClass(QuerySet):
class Book(ormar.Model):
class Meta(ormar.ModelMeta):
metadata = metadata
database = database
tablename = "book"
queryset_class = MyQuerySetClass
ormar_config = base_ormar_config.copy(
queryset_class=MyQuerySetClass,
tablename="book",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=32)
@ -304,17 +190,9 @@ book = await Book.objects.first_or_404(name="123")
```
### Type Hints & Legacy
### Type Hints
Before version 0.4.0 `ormar` supported only one way of defining `Fields` on a `Model` using python type hints as pydantic.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs011.py"
```
But that didn't play well with static type checkers like `mypy` and `pydantic` PyCharm plugin.
Therefore from version >=0.4.0 `ormar` switched to new notation.
Note that for better IDE support and mypy checks you can provide type hints.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
@ -343,9 +221,9 @@ and table creation you need to assign each `Model` with two special parameters.
One is `Database` instance created with your database url in [sqlalchemy connection string][sqlalchemy connection string] format.
Created instance needs to be passed to every `Model` with `Meta` class `database` parameter.
Created instance needs to be passed to every `Model` with `ormar_config` object `database` parameter.
```Python hl_lines="1 6 12"
```Python hl_lines="1 5 11"
--8<-- "../docs_src/models/docs001.py"
```
@ -357,9 +235,9 @@ Created instance needs to be passed to every `Model` with `Meta` class `database
Second dependency is sqlalchemy `MetaData` instance.
Created instance needs to be passed to every `Model` with `Meta` class `metadata` parameter.
Created instance needs to be passed to every `Model` with `ormar_config` object `metadata` parameter.
```Python hl_lines="2 7 13"
```Python hl_lines="3 6 12"
--8<-- "../docs_src/models/docs001.py"
```
@ -369,25 +247,22 @@ Created instance needs to be passed to every `Model` with `Meta` class `metadata
#### Best practice
Only thing that `ormar` expects is a class with name `Meta` and two class variables: `metadata` and `databases`.
Note that `ormar` expects the field with name `ormar_config` that is an instance of `OrmarConfig` class.
To ease the config management, the `OrmarConfig` class provide `copy` method.
So instead of providing the same parameters over and over again for all models
you should create a base object and use its copy in all models.
So instead of providing the same parameters over and over again for all models you should creata a class and subclass it in all models.
```Python hl_lines="14 20 33"
```Python hl_lines="9-12 19 28"
--8<-- "../docs_src/models/docs013.py"
```
!!!warning
You need to subclass your `MainMeta` class in each `Model` class as those classes store configuration variables
that otherwise would be overwritten by each `Model`.
### Table Names
By default table name is created from Model class name as lowercase name plus 's'.
You can overwrite this parameter by providing `Meta` class `tablename` argument.
You can overwrite this parameter by providing `ormar_config` object's `tablename` argument.
```Python hl_lines="12 13 14"
```Python hl_lines="14-16"
--8<-- "../docs_src/models/docs002.py"
```
@ -395,74 +270,72 @@ You can overwrite this parameter by providing `Meta` class `tablename` argument.
On a model level you can also set model-wise constraints on sql columns.
Right now only `IndexColumns` and `UniqueColumns` constraints are supported.
Right now only `IndexColumns`, `UniqueColumns` and `CheckColumns` constraints are supported.
!!!note
Note that both constraints should be used only if you want to set a name on constraint or want to set the index on multiple columns, otherwise `index` and `unique` properties on ormar fields are preferred.
Note that both constraints should be used only if you want to set a name on constraint or want to set the index on multiple columns, otherwise `index` and `unique` properties on ormar fields are preferred.
!!!tip
To read more about columns constraints like `primary_key`, `unique`, `ForeignKey` etc. visit [fields][fields].
#### UniqueColumns
You can set this parameter by providing `Meta` class `constraints` argument.
You can set this parameter by providing `ormar_config` object `constraints` argument.
```Python hl_lines="14-17"
```Python hl_lines="13-16"
--8<-- "../docs_src/models/docs006.py"
```
!!!note
Note that constraints are meant for combination of columns that should be unique.
To set one column as unique use [`unique`](../fields/common-parameters.md#unique) common parameter.
Of course you can set many columns as unique with this param but each of them will be checked separately.
Note that constraints are meant for combination of columns that should be unique.
To set one column as unique use [`unique`](../fields/common-parameters.md#unique) common parameter.
Of course you can set many columns as unique with this param but each of them will be checked separately.
#### IndexColumns
You can set this parameter by providing `Meta` class `constraints` argument.
You can set this parameter by providing `ormar_config` object `constraints` argument.
```Python hl_lines="14-17"
```Python hl_lines="13-16"
--8<-- "../docs_src/models/docs017.py"
```
!!!note
Note that constraints are meant for combination of columns that should be in the index.
To set one column index use [`unique`](../fields/common-parameters.md#index) common parameter.
Of course, you can set many columns as indexes with this param but each of them will be a separate index.
Note that constraints are meant for combination of columns that should be in the index.
To set one column index use [`unique`](../fields/common-parameters.md#index) common parameter.
Of course, you can set many columns as indexes with this param but each of them will be a separate index.
#### CheckColumns
You can set this parameter by providing `Meta` class `constraints` argument.
You can set this parameter by providing `ormar_config` object `constraints` argument.
```Python hl_lines="14-17"
```Python hl_lines="15-20"
--8<-- "../docs_src/models/docs018.py"
```
!!!note
Note that some databases do not actively support check constraints such as MySQL.
Note that some databases do not actively support check constraints (such as MySQL).
### Pydantic configuration
As each `ormar.Model` is also a `pydantic` model, you might want to tweak the settings of the pydantic configuration.
The way to do this in pydantic is to adjust the settings on the `Config` class provided to your model, and it works exactly the same for ormar models.
The way to do this in pydantic is to adjust the settings on the `model_config` dictionary provided to your model, and it works exactly the same for ormar models.
So in order to set your own preferences you need to provide not only the `Meta` class but also the `Config` class to your model.
So in order to set your own preferences you need to provide not only the `ormar_config` class but also the `model_config = ConfigDict()` class to your model.
!!!note
To read more about available settings visit the [pydantic](https://pydantic-docs.helpmanual.io/usage/model_config/) config page.
To read more about available settings visit the [pydantic](https://pydantic-docs.helpmanual.io/usage/model_config/) config page.
Note that if you do not provide your own configuration, ormar will do it for you.
The default config provided is as follows:
```python
class Config(pydantic.BaseConfig):
orm_mode = True
validate_assignment = True
model_config = ConfigDict(validate_assignment=True, ser_json_bytes="base64")
```
So to overwrite setting or provide your own a sample model can look like following:
```Python hl_lines="15-16"
```Python hl_lines="16"
--8<-- "../docs_src/models/docs016.py"
```
@ -474,69 +347,64 @@ If you try to do so the `ModelError` will be raised.
Since the extra fields cannot be saved in the database the default to disallow such fields seems a feasible option.
On the contrary in `pydantic` the default option is to ignore such extra fields, therefore `ormar` provides an `Meta.extra` setting to behave in the same way.
On the contrary in `pydantic` the default option is to ignore such extra fields, therefore `ormar` provides an `ormar_config.extra` setting to behave in the same way.
To ignore extra fields passed to `ormar` set this setting to `Extra.ignore` instead of default `Extra.forbid`.
Note that `ormar` does not allow accepting extra fields, you can only ignore them or forbid them (raise exception if present)
```python
from ormar import Extra
from ormar import Extra, OrmarConfig
class Child(ormar.Model):
class Meta(ormar.ModelMeta):
tablename = "children"
metadata = metadata
database = database
extra = Extra.ignore # set extra setting to prevent exceptions on extra fields presence
ormar_config = OrmarConfig(
tablename="children",
extra=Extra.ignore # set extra setting to prevent exceptions on extra fields presence
)
id: int = ormar.Integer(name="child_id", primary_key=True)
first_name: str = ormar.String(name="fname", max_length=100)
last_name: str = ormar.String(name="lname", max_length=100)
```
To set the same setting on all model check the [best practices]("../models/index/#best-practice") and `BaseMeta` concept.
To set the same setting on all model check the [best practices]("../models/index/#best-practice") and `base_ormar_config` concept.
## Model sort order
When querying the database with given model by default the Model is ordered by the `primary_key`
column ascending. If you wish to change the default behaviour you can do it by providing `orders_by`
parameter to model `Meta` class.
parameter to model `ormar_config` object.
Sample default ordering:
Sample default ordering (not specified - so by primary key):
```python
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
# default sort by column id ascending
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
ormar_config = base_ormar_config.copy(
tablename="authors",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
Modified
```python
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
```python hl_lines="9"
base_ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# now default sort by name descending
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
orders_by = ["-name"]
ormar_config = base_ormar_config.copy(
orders_by = ["-name"],
tablename="authors",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -546,12 +414,9 @@ class Author(ormar.Model):
There are two ways to create and persist the `Model` instance in the database.
!!!tip
Use `ipython` to try this from the console, since it supports `await`.
If you plan to modify the instance in the later execution of your program you can initiate your `Model` as a normal class and later await a `save()` call.
```Python hl_lines="20 21"
```Python hl_lines="25-26"
--8<-- "../docs_src/models/docs007.py"
```
@ -561,7 +426,7 @@ For creating multiple objects at once a `bulk_create()` QuerySet's method is ava
Each model has a `QuerySet` initialised as `objects` parameter
```Python hl_lines="23"
```Python hl_lines="28"
--8<-- "../docs_src/models/docs007.py"
```

View File

@ -7,7 +7,7 @@ Out of various types of ORM models inheritance `ormar` currently supports two of
## Types of inheritance
The short summary of different types of inheritance is:
The short summary of different types of inheritance:
* **Mixins [SUPPORTED]** - don't subclass `ormar.Model`, just define fields that are
later used on different models (like `created_date` and `updated_date` on each model),
@ -32,6 +32,13 @@ To use Mixins just define a class that is not inheriting from an `ormar.Model` b
defining `ormar.Fields` as class variables.
```python
base_ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
engine=sqlalchemy.create_engine(DATABASE_URL),
)
# a mixin defines the fields but is a normal python class
class AuditMixin:
created_by: str = ormar.String(max_length=100)
@ -45,10 +52,7 @@ class DateFieldsMixins:
# a models can inherit from one or more mixins
class Category(ormar.Model, DateFieldsMixins, AuditMixin):
class Meta(ormar.ModelMeta):
tablename = "categories"
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)
@ -57,7 +61,7 @@ class Category(ormar.Model, DateFieldsMixins, AuditMixin):
!!!tip
Note that Mixins are **not** models, so you still need to inherit
from `ormar.Model` as well as define `Meta` class in the **final** model.
from `ormar.Model` as well as define `ormar_config` field in the **final** model.
A Category class above will have four additional fields: `created_date`, `updated_date`,
`created_by` and `updated_by`.
@ -73,11 +77,11 @@ In concept concrete table inheritance is very similar to Mixins, but uses
actual `ormar.Models` as base classes.
!!!warning
Note that base classes have `abstract=True` set in `Meta` class, if you try
Note that base classes have `abstract=True` set in `ormar_config` object, if you try
to inherit from non abstract marked class `ModelDefinitionError` will be raised.
Since this abstract Model will never be initialized you can skip `metadata`
and `database` in it's `Meta` definition.
and `database` in it's `ormar_config` definition.
But if you provide it - it will be inherited, that way you do not have to
provide `metadata` and `databases` in the final/concrete class
@ -91,8 +95,7 @@ otherwise an error will be raised.
# note that base classes have abstract=True
# since this model will never be initialized you can skip metadata and database
class AuditModel(ormar.Model):
class Meta:
abstract = True
ormar_config = base_ormar_config.copy(abstract=True)
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
@ -100,10 +103,11 @@ class AuditModel(ormar.Model):
# but if you provide it it will be inherited - DRY (Don't Repeat Yourself) in action
class DateFieldsModel(ormar.Model):
class Meta:
abstract = True
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(
abstract=True,
metadata=metadata,
database=db,
)
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
@ -111,8 +115,7 @@ class DateFieldsModel(ormar.Model):
# that way you do not have to provide metadata and databases in concrete class
class Category(DateFieldsModel, AuditModel):
class Meta(ormar.ModelMeta):
tablename = "categories"
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)
@ -123,8 +126,6 @@ class Category(DateFieldsModel, AuditModel):
The list of inherited options/settings is as follows: `metadata`, `database`
and `constraints`.
Also methods decorated with `@property_field` decorator will be inherited/recognized.
Of course apart from that all fields from base classes are combined and created in the
concrete table of the final Model.
@ -140,15 +141,16 @@ inheritance.
Whenever you define a field with same name and new definition it will completely replace
the previously defined one.
```python
```python hl_lines="28"
# base class
class DateFieldsModel(ormar.Model):
class Meta:
abstract = True
metadata = metadata
database = db
ormar_config = OrmarConfig(
abstract=True,
metadata=metadata,
database=db,
# note that UniqueColumns need sqlalchemy db columns names not the ormar ones
constraints = [ormar.UniqueColumns("creation_date", "modification_date")]
constraints=[ormar.UniqueColumns("creation_date", "modification_date")]
)
created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
@ -159,10 +161,11 @@ class DateFieldsModel(ormar.Model):
class RedefinedField(DateFieldsModel):
class Meta(ormar.ModelMeta):
tablename = "redefines"
metadata = metadata
database = db
ormar_config = OrmarConfig(
tablename="redefines",
metadata=metadata,
database=db,
)
id: int = ormar.Integer(primary_key=True)
# here the created_date is replaced by the String field
@ -170,12 +173,12 @@ class RedefinedField(DateFieldsModel):
# you can verify that the final field is correctly declared and created
changed_field = RedefinedField.Meta.model_fields["created_date"]
changed_field = RedefinedField.ormar_config.model_fields["created_date"]
assert changed_field.default is None
assert changed_field.alias == "creation_date"
assert any(x.name == "creation_date" for x in RedefinedField.Meta.table.columns)
assert any(x.name == "creation_date" for x in RedefinedField.ormar_config.table.columns)
assert isinstance(
RedefinedField.Meta.table.columns["creation_date"].type,
RedefinedField.ormar_config.table.columns["creation_date"].type,
sqlalchemy.sql.sqltypes.String,
)
```
@ -225,9 +228,7 @@ That might sound complicated but let's look at the following example:
```python
# normal model used in relation
class Person(ormar.Model):
class Meta:
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -235,10 +236,7 @@ class Person(ormar.Model):
# parent model - needs to be abstract
class Car(ormar.Model):
class Meta:
abstract = True
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(abstract=True)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
@ -249,16 +247,13 @@ class Car(ormar.Model):
class Truck(Car):
class Meta:
pass
ormar_config = base_ormar_config.copy()
max_capacity: int = ormar.Integer()
class Bus(Car):
class Meta:
# default naming is name.lower()+'s' so it's ugly for buss ;)
tablename = "buses"
ormar_config = base_ormar_config.copy(tablename="buses")
max_persons: int = ormar.Integer()
```
@ -266,7 +261,7 @@ class Bus(Car):
Now when you will inspect the fields on Person model you will get:
```python
Person.Meta.model_fields
Person.ormar_config.model_fields
"""
{'id': <class 'ormar.fields.model_fields.Integer'>,
'name': <class 'ormar.fields.model_fields.String'>,
@ -293,8 +288,7 @@ different `related_name` parameter.
```python
# rest of the above example remains the same
class Bus(Car):
class Meta:
tablename = "buses"
ormar_config = base_ormar_config.copy(tablename="buses")
# new field that changes the related_name
owner: Person = ormar.ForeignKey(Person, related_name="buses")
@ -304,7 +298,7 @@ class Bus(Car):
Now the columns looks much better.
```python
Person.Meta.model_fields
Person.ormar_config.model_fields
"""
{'id': <class 'ormar.fields.model_fields.Integer'>,
'name': <class 'ormar.fields.model_fields.String'>,
@ -328,7 +322,7 @@ Person.Meta.model_fields
Similarly, you can inherit from Models that have ManyToMany relations declared but
there is one, but substantial difference - the Through model.
Since in the future the Through model will be able to hold additional fields and now it links only two Tables
Since the Through model will be able to hold additional fields, and now it links only two Tables
(`from` and `to` ones), each child that inherits the m2m relation field has to have separate
Through model.
@ -344,27 +338,18 @@ We will modify the previous example described above to use m2m relation for co_o
```python
# person remain the same as above
class Person(ormar.Model):
class Meta:
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# new through model between Person and Car2
class PersonsCar(ormar.Model):
class Meta:
tablename = "cars_x_persons"
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(tablename="cars_x_persons")
# note how co_owners is now ManyToMany relation
class Car2(ormar.Model):
class Meta:
# parent class needs to be marked abstract
abstract = True
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(abstract=True)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
@ -379,16 +364,13 @@ class Car2(ormar.Model):
# child models define only additional Fields
class Truck2(Car2):
class Meta:
# note how you don't have to provide inherited Meta params
tablename = "trucks2"
ormar_config = base_ormar_config.copy(tablename="trucks2")
max_capacity: int = ormar.Integer()
class Bus2(Car2):
class Meta:
tablename = "buses2"
ormar_config = base_ormar_config.copy(tablename="buses2")
max_persons: int = ormar.Integer()
```
@ -402,7 +384,7 @@ That way for class Truck2 the relation defined in
You can verify the names by inspecting the list of fields present on `Person` model.
```python
Person.Meta.model_fields
Person.ormar_config.model_fields
{
# note how all relation fields need to be unique on Person
# regardless if autogenerated or manually overwritten
@ -425,14 +407,14 @@ But that's not all. It's kind of internal to `ormar` but affects the data struct
so let's examine the through models for both `Bus2` and `Truck2` models.
```python
Bus2.Meta.model_fields['co_owners'].through
Bus2.ormar_config.model_fields['co_owners'].through
<class 'abc.PersonsCarBus2'>
Bus2.Meta.model_fields['co_owners'].through.Meta.tablename
Bus2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_buses2'
Truck2.Meta.model_fields['co_owners'].through
Truck2.ormar_config.model_fields['co_owners'].through
<class 'abc.PersonsCarTruck2'>
Truck2.Meta.model_fields['co_owners'].through.Meta.tablename
Truck2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_trucks2'
```
@ -443,7 +425,7 @@ the name of the **table** from the child is used.
Note that original model is not only not used, the table for this model is removed from metadata:
```python
Bus2.Meta.metadata.tables.keys()
Bus2.ormar_config.metadata.tables.keys()
dict_keys(['test_date_models', 'categories', 'subjects', 'persons', 'trucks', 'buses',
'cars_x_persons_trucks2', 'trucks2', 'cars_x_persons_buses2', 'buses2'])
```
@ -469,26 +451,24 @@ Ormar allows you to skip certain fields in inherited model that are coming from
!!!Note
Note that the same behaviour can be achieved by splitting the model into more abstract models and mixins - which is a preferred way in normal circumstances.
To skip certain fields from a child model, list all fields that you want to skip in `model.Meta.exclude_parent_fields` parameter like follows:
To skip certain fields from a child model, list all fields that you want to skip in `model.ormar_config.exclude_parent_fields` parameter like follows:
```python
metadata = sa.MetaData()
db = databases.Database(DATABASE_URL)
base_ormar_config = OrmarConfig(
metadata=sa.MetaData(),
database=databases.Database(DATABASE_URL),
)
class AuditModel(ormar.Model):
class Meta:
abstract = True
ormar_config = base_ormar_config.copy(abstract=True)
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
class DateFieldsModel(ormar.Model):
class Meta(ormar.ModelMeta):
abstract = True
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(abstract=True)
created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
@ -499,10 +479,11 @@ class DateFieldsModel(ormar.Model):
class Category(DateFieldsModel, AuditModel):
class Meta(ormar.ModelMeta):
tablename = "categories"
ormar_config = base_ormar_config.copy(
tablename="categories",
# set fields that should be skipped
exclude_parent_fields = ["updated_by", "updated_date"]
exclude_parent_fields=["updated_by", "updated_date"],
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)
@ -523,38 +504,32 @@ Note how you simply need to provide field names and it will exclude the parent f
The same effect can be achieved by splitting base classes like:
```python
metadata = sa.MetaData()
db = databases.Database(DATABASE_URL)
base_ormar_config = OrmarConfig(
metadata=sa.MetaData(),
database=databases.Database(DATABASE_URL),
)
class AuditCreateModel(ormar.Model):
class Meta:
abstract = True
ormar_config = base_ormar_config.copy(abstract=True)
created_by: str = ormar.String(max_length=100)
class AuditUpdateModel(ormar.Model):
class Meta:
abstract = True
ormar_config = base_ormar_config.copy(abstract=True)
updated_by: str = ormar.String(max_length=100, default="Sam")
class CreateDateFieldsModel(ormar.Model):
class Meta(ormar.ModelMeta):
abstract = True
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(abstract=True)
created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
)
class UpdateDateFieldsModel(ormar.Model):
class Meta(ormar.ModelMeta):
abstract = True
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(abstract=True)
updated_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="modification_date"
@ -562,8 +537,7 @@ class UpdateDateFieldsModel(ormar.Model):
class Category(CreateDateFieldsModel, AuditCreateModel):
class Meta(ormar.ModelMeta):
tablename = "categories"
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)

View File

@ -20,27 +20,27 @@ For example to list pydantic model fields you can:
## Sqlalchemy Table
To access auto created sqlalchemy table you can use `Model.Meta.table` parameter
To access auto created sqlalchemy table you can use `Model.ormar_config.table` parameter
For example to list table columns you can:
```Python hl_lines="20"
```Python hl_lines="24"
--8<-- "../docs_src/models/docs004.py"
```
!!!tip
You can access table primary key name by `Course.Meta.pkname`
You can access table primary key name by `Course.ormar_config.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
To access ormar `Fields` you can use `Model.ormar_config.model_fields` parameter
For example to list table model fields you can:
```Python hl_lines="20"
```Python hl_lines="22"
--8<-- "../docs_src/models/docs005.py"
```

View File

@ -13,37 +13,37 @@ Available methods are described below.
## `pydantic` methods
Note that each `ormar.Model` is also a `pydantic.BaseModel`, so all `pydantic` methods are also available on a model,
especially `dict()` and `json()` methods that can also accept `exclude`, `include` and other parameters.
especially `model_dump()` and `model_dump_json()` methods that can also accept `exclude`, `include` and other parameters.
To read more check [pydantic][pydantic] documentation
## construct
## model_construct()
`construct` is a raw equivalent of `__init__` method used for construction of new instances.
`model_construct` is a raw equivalent of `__init__` method used for construction of new instances.
The difference is that `construct` skips validations, so it should be used when you know that data is correct and can be trusted.
The difference is that `model_construct` skips validations, so it should be used when you know that data is correct and can be trusted.
The benefit of using construct is the speed of execution due to skipped validation.
!!!note
Note that in contrast to `pydantic.construct` method - the `ormar` equivalent will also process the nested related models.
Note that in contrast to `pydantic.model_construct` method - the `ormar` equivalent will also process the nested related models.
!!!warning
Bear in mind that due to skipped validation the `construct` method does not perform any conversions, checks etc.
So it's your responsibility to provide that data that is valid and can be consumed by the database.
The only two things that construct still performs are:
Bear in mind that due to skipped validation the `construct` method does not perform any conversions, checks etc.
So it's your responsibility to provide that data that is valid and can be consumed by the database.
The only two things that construct still performs are:
* Providing a `default` value for not set fields
* Initialize nested ormar models if you pass a dictionary or a primary key value
* Providing a `default` value for not set fields
* Initialize nested ormar models if you pass a dictionary or a primary key value
## dict
## model_dump()
`dict` is a method inherited from `pydantic`, yet `ormar` adds its own parameters and has some nuances when working with default values,
`model_dump` is a method inherited from `pydantic`, yet `ormar` adds its own parameters and has some nuances when working with default values,
therefore it's listed here for clarity.
`dict` as the name suggests export data from model tree to dictionary.
`model_dump` as the name suggests export data from model tree to dictionary.
Explanation of dict parameters:
Explanation of model_dump parameters:
### include (`ormar` modified)
@ -55,14 +55,14 @@ Note that `pydantic` has an uncommon pattern of including/ excluding fields in l
And if you want to exclude the field in all children you need to pass a `__all__` key to dictionary.
You cannot exclude nested models in `Set`s in `pydantic` but you can in `ormar`
(by adding double underscore on relation name i.e. to exclude name of category for a book you cen use `exclude={"book__category__name"}`)
(by adding double underscore on relation name i.e. to exclude name of category for a book you can use `exclude={"book__category__name"}`)
`ormar` does not support by index exclusion/ inclusions and accepts a simplified and more user-friendly notation.
To check how you can include/exclude fields, including nested fields check out [fields](../queries/select-columns.md#fields) section that has an explanation and a lot of samples.
!!!note
The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi!
The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi!
### exclude (`ormar` modified)
@ -81,7 +81,7 @@ You cannot exclude nested models in `Set`s in `pydantic` but you can in `ormar`
To check how you can include/exclude fields, including nested fields check out [fields](../queries/select-columns.md#fields) section that has an explanation and a lot of samples.
!!!note
The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi!
The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi!
### exclude_unset
@ -90,16 +90,13 @@ To check how you can include/exclude fields, including nested fields check out [
Flag indicates whether fields which were not explicitly set when creating the model should be excluded from the returned dictionary.
!!!warning
Note that after you save data into database each field has its own value -> either provided by you, default, or `None`.
That means that when you load the data from database, **all** fields are set, and this flag basically stop working!
Note that after you save data into database each field has its own value -> either provided by you, default, or `None`.
That means that when you load the data from database, **all** fields are set, and this flag basically stop working!
```python
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, default="Test")
@ -107,10 +104,7 @@ class Category(ormar.Model):
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -118,17 +112,17 @@ class Item(ormar.Model):
categories: List[Category] = ormar.ManyToMany(Category)
category = Category(name="Test 2")
assert category.dict() == {'id': None, 'items': [], 'name': 'Test 2',
assert category.model_dump() == {'id': None, 'items': [], 'name': 'Test 2',
'visibility': True}
assert category.dict(exclude_unset=True) == {'items': [], 'name': 'Test 2'}
assert category.model_dump(exclude_unset=True) == {'items': [], 'name': 'Test 2'}
await category.save()
category2 = await Category.objects.get()
assert category2.dict() == {'id': 1, 'items': [], 'name': 'Test 2',
assert category2.model_dump() == {'id': 1, 'items': [], 'name': 'Test 2',
'visibility': True}
# NOTE how after loading from db all fields are set explicitly
# as this is what happens when you populate a model from db
assert category2.dict(exclude_unset=True) == {'id': 1, 'items': [],
assert category2.model_dump(exclude_unset=True) == {'id': 1, 'items': [],
'name': 'Test 2', 'visibility': True}
```
@ -140,20 +134,14 @@ Flag indicates are equal to their default values (whether set or otherwise) shou
```python
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, default="Test")
visibility: bool = ormar.Boolean(default=True)
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -162,15 +150,15 @@ class Item(ormar.Model):
category = Category()
# note that Integer pk is by default autoincrement so optional
assert category.dict() == {'id': None, 'items': [], 'name': 'Test', 'visibility': True}
assert category.dict(exclude_defaults=True) == {'items': []}
assert category.model_dump() == {'id': None, 'items': [], 'name': 'Test', 'visibility': True}
assert category.model_dump(exclude_defaults=True) == {'items': []}
# save and reload the data
await category.save()
category2 = await Category.objects.get()
assert category2.dict() == {'id': 1, 'items': [], 'name': 'Test', 'visibility': True}
assert category2.dict(exclude_defaults=True) == {'id': 1, 'items': []}
assert category2.model_dump() == {'id': 1, 'items': [], 'name': 'Test', 'visibility': True}
assert category2.model_dump(exclude_defaults=True) == {'id': 1, 'items': []}
```
### exclude_none
@ -181,10 +169,7 @@ Flag indicates whether fields which are equal to `None` should be excluded from
```python
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, default="Test", nullable=True)
@ -192,10 +177,7 @@ class Category(ormar.Model):
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -204,16 +186,16 @@ class Item(ormar.Model):
category = Category(name=None)
assert category.dict() == {'id': None, 'items': [], 'name': None,
assert category.model_dump() == {'id': None, 'items': [], 'name': None,
'visibility': True}
# note the id is not set yet so None and excluded
assert category.dict(exclude_none=True) == {'items': [], 'visibility': True}
assert category.model_dump(exclude_none=True) == {'items': [], 'visibility': True}
await category.save()
category2 = await Category.objects.get()
assert category2.dict() == {'id': 1, 'items': [], 'name': None,
assert category2.model_dump() == {'id': 1, 'items': [], 'name': None,
'visibility': True}
assert category2.dict(exclude_none=True) == {'id': 1, 'items': [],
assert category2.model_dump(exclude_none=True) == {'id': 1, 'items': [],
'visibility': True}
```
@ -226,17 +208,14 @@ Setting flag to `True` will exclude all primary key columns in a tree, including
```python
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
item1 = Item(id=1, name="Test Item")
assert item1.dict() == {"id": 1, "name": "Test Item"}
assert item1.dict(exclude_primary_keys=True) == {"name": "Test Item"}
assert item1.model_dump() == {"id": 1, "name": "Test Item"}
assert item1.model_dump(exclude_primary_keys=True) == {"name": "Test Item"}
```
### exclude_through_models (`ormar` only)
@ -249,20 +228,14 @@ Setting the `exclude_through_models=True` will exclude all through models, inclu
```python
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -280,7 +253,7 @@ await Item(**item_dict).save_related(follow=True, save_all=True)
item = await Item.objects.select_related("categories").get()
# by default you can see the through models (itemcategory)
assert item.dict() == {'id': 1, 'name': 'test',
assert item.model_dump() == {'id': 1, 'name': 'test',
'categories': [
{'id': 1, 'name': 'test cat',
'itemcategory': {'id': 1, 'category': None, 'item': None}},
@ -289,7 +262,7 @@ assert item.dict() == {'id': 1, 'name': 'test',
]}
# you can exclude those fields/ models
assert item.dict(exclude_through_models=True) == {
assert item.model_dump(exclude_through_models=True) == {
'id': 1, 'name': 'test',
'categories': [
{'id': 1, 'name': 'test cat'},
@ -297,49 +270,45 @@ assert item.dict(exclude_through_models=True) == {
]}
```
## json
## model_dump_json()
`json()` has exactly the same parameters as `dict()` so check above.
`model_dump_json()` has exactly the same parameters as `model_dump()` so check above.
Of course the end result is a string with json representation and not a dictionary.
## get_pydantic
## get_pydantic()
`get_pydantic(include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None)`
This method allows you to generate `pydantic` models from your ormar models without you needing to retype all the fields.
Note that if you have nested models, it **will generate whole tree of pydantic models for you!**
Note that if you have nested models, it **will generate whole tree of pydantic models for you!** but in a way that prevents cyclic references issues.
Moreover, you can pass `exclude` and/or `include` parameters to keep only the fields that you want to, including in nested models.
That means that this way you can effortlessly create pydantic models for requests and responses in `fastapi`.
!!!Note
To read more about possible excludes/includes and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation
To read more about possible excludes/includes and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation
Given sample ormar models like follows:
```python
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL, force_rollback=True)
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL, force_rollback=True),
)
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Category(ormar.Model):
class Meta(BaseMeta):
tablename = "categories"
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Item(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, default="test")
@ -361,8 +330,8 @@ class Category(BaseModel):
```
!!!warning
Note that it's not a good practice to have several classes with same name in one module, as well as it would break `fastapi` docs.
Thats's why ormar adds random 3 uppercase letters to the class name. In example above it means that in reality class would be named i.e. `Category_XIP(BaseModel)`.
Note that it's not a good practice to have several classes with same name in one module, as well as it would break `fastapi` docs.
Thats's why ormar adds random 3 uppercase letters to the class name. In example above it means that in reality class would be named i.e. `Category_XIP(BaseModel)`.
To exclude or include nested fields you can use dict or double underscores.
@ -382,19 +351,19 @@ class Category(BaseModel):
items: Optional[List[Item]]
```
Of course, you can use also deeply nested structures and ormar will generate it pydantic equivalent you (in a way that exclude loops).
Of course, you can use also deeply nested structures and ormar will generate it's pydantic equivalent for you (in a way that exclude loops).
Note how `Item` model above does not have a reference to `Category` although in ormar the relation is bidirectional (and `ormar.Item` has `categories` field).
!!!warning
Note that the generated pydantic model will inherit all **field** validators from the original `ormar` model, that includes the ormar choices validator as well as validators defined with `pydantic.validator` decorator.
But, at the same time all root validators present on `ormar` models will **NOT** be copied to the generated pydantic model. Since root validator can operate on all fields and a user can exclude some fields during generation of pydantic model it's not safe to copy those validators.
If required, you need to redefine/ manually copy them to generated pydantic model.
Note that the generated pydantic model will inherit all **field** validators from the original `ormar` model, that includes the ormar choices validator as well as validators defined with `pydantic.validator` decorator.
But, at the same time all root validators present on `ormar` models will **NOT** be copied to the generated pydantic model. Since root validator can operate on all fields and a user can exclude some fields during generation of pydantic model it's not safe to copy those validators.
If required, you need to redefine/ manually copy them to generated pydantic model.
## load
## load()
By default when you query a table without prefetching related models, the ormar will still construct
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).
@ -409,14 +378,14 @@ await track.album.load()
track.album.name # will return 'Malibu'
```
## load_all
## load_all()
`load_all(follow: bool = False, exclude: Union[List, str, Set, Dict] = None) -> Model`
Method works like `load()` but also goes through all relations of the `Model` on which the method is called,
and reloads them from database.
By default the `load_all` method loads only models that are directly related (one step away) to the model on which the method is called.
By default, the `load_all` method loads 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 load all of them in the relation tree.
@ -442,7 +411,7 @@ Method performs one database query so it's more efficient than nested calls to `
!!!warning
All relations are cleared on `load_all()`, so if you exclude some nested models they will be empty after call.
## save
## save()
`save() -> self`
@ -461,7 +430,7 @@ track = await Track.objects.get(name='The Bird')
await track.save() # will raise integrity error as pk is populated
```
## update
## update()
`update(_columns: List[str] = None, **kwargs) -> self`
@ -480,12 +449,9 @@ To update only selected columns from model into the database provide a list of c
In example:
```python
```python hl_lines="16"
class Movie(ormar.Model):
class Meta:
tablename = "movies"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="title")
@ -510,9 +476,9 @@ assert terminator.year == 1984
```
!!!warning
Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database!
Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database!
## upsert
## upsert()
`upsert(**kwargs) -> self`
@ -531,7 +497,7 @@ await track.upsert(name='The Bird Strikes Again') # will call update as pk is al
```
## delete
## delete()
You can delete models by using `QuerySet.delete()` method or by using your model and calling `delete()` method.
@ -543,7 +509,7 @@ 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()
`save_related(follow: bool = False, save_all: bool = False, exclude=Optional[Union[Set, Dict]]) -> None`
@ -566,11 +532,11 @@ If you want to skip saving some of the relations you can pass `exclude` paramete
or it can be a dictionary that can also contain nested items.
!!!note
Note that `exclude` parameter in `save_related` accepts only relation fields names, so
if you pass any other fields they will be saved anyway
Note that `exclude` parameter in `save_related` accepts only relation fields names, so
if you pass any other fields they will be saved anyway
!!!note
To read more about the structure of possible values passed to `exclude` check `Queryset.fields` method documentation.
To read more about the structure of possible values passed to `exclude` check `Queryset.fields` method documentation.
!!!warning
To avoid circular updates with `follow=True` set, `save_related` keeps a set of already visited Models on each branch of relation tree,
@ -584,18 +550,14 @@ Example:
```python
class Department(ormar.Model):
class Meta:
database = database
metadata = metadata
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
department_name: str = ormar.String(max_length=100)
class Course(ormar.Model):
class Meta:
database = database
metadata = metadata
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
course_name: str = ormar.String(max_length=100)
@ -604,9 +566,7 @@ class Course(ormar.Model):
class Student(ormar.Model):
class Meta:
database = database
metadata = metadata
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -647,7 +607,7 @@ to_exclude = {
}
# after excluding ids and through models you get exact same payload used to
# construct whole tree
assert department_check.dict(exclude=to_exclude) == to_save
assert department_check.model_dump(exclude=to_exclude) == to_save
```

View File

@ -16,14 +16,14 @@ engine = sqlalchemy.create_engine("sqlite:///test.db")
metadata.create_all(engine)
```
You can also create single tables, sqlalchemy tables are exposed in `ormar.Meta` class.
You can also create single tables, sqlalchemy tables are exposed in `ormar.ormar_config` object.
```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)
Artist.ormar_config.table.create(engine)
```
!!!warning