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

@ -16,17 +16,18 @@ Here you can find a very simple sample application code.
It's divided into subsections for clarity.
!!!note
If you want to read more on how you can use ormar models in fastapi requests and
responses check the [responses](response.md) and [requests](requests.md) documentation.
If you want to read more on how you can use ormar models in fastapi requests and
responses check the [responses](response.md) and [requests](requests.md) documentation.
## Quick Start
!!!note
Note that you can find the full quick start script in the [github](https://github.com/collerek/ormar) repo under examples.
Note that you can find the full quick start script in the [github](https://github.com/collerek/ormar) repo under examples.
### Imports and initialization
First take care of the imports and initialization
Define startup and shutdown procedures using FastAPI lifespan and use is in the
application.
```python
from typing import List, Optional
@ -36,29 +37,26 @@ from fastapi import FastAPI
import ormar
app = FastAPI()
metadata = sqlalchemy.MetaData()
database = databases.Database("sqlite:///test.db")
app.state.database = database
```
### Database connection
Next define startup and shutdown events (or use middleware)
- note that this is `databases` specific setting not the ormar one
```python
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
from contextlib import asynccontextmanager
from fastapi import FastAPI
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
if not config.database.is_connected:
await config.database.connect()
yield
if config.database.is_connected:
await config.database.disconnect()
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database("sqlite:///test.db"),
)
app = FastAPI(lifespan=lifespan(base_ormar_config))
```
!!!info
@ -71,21 +69,21 @@ Define ormar models with appropriate fields.
Those models will be used instead of pydantic ones.
```python
base_ormar_config = OrmarConfig(
metadata = metadata
database = database
)
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)
@ -122,7 +120,7 @@ async def create_category(category: Category):
@app.put("/items/{item_id}")
async def get_item(item_id: int, item: Item):
item_db = await Item.objects.get(pk=item_id)
return await item_db.update(**item.dict())
return await item_db.update(**item.model_dump())
@app.delete("/items/{item_id}")
@ -197,14 +195,14 @@ def test_all_endpoints():
assert items[0] == item
item.name = "New name"
response = client.put(f"/items/{item.pk}", json=item.dict())
assert response.json() == item.dict()
response = client.put(f"/items/{item.pk}", json=item.model_dump())
assert response.json() == item.model_dump()
response = client.get("/items/")
items = [Item(**item) for item in response.json()]
assert items[0].name == "New name"
response = client.delete(f"/items/{item.pk}", json=item.dict())
response = client.delete(f"/items/{item.pk}", json=item.model_dump())
assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__"
response = client.get("/items/")
items = response.json()

View File

@ -23,11 +23,13 @@ Field is not required if (any/many/all) of following:
Example:
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class User(ormar.Model):
class Meta:
tablename: str = "users"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
@ -42,8 +44,8 @@ In above example fields `id` (is an `autoincrement` `Integer`), `first_name` ( h
If the field is nullable you don't have to include it in payload during creation as well as in response, so given example above you can:
!!!Warning
Note that although you do not have to pass the optional field, you still **can** do it.
And if someone will pass a value it will be used later unless you take measures to prevent it.
Note that although you do not have to pass the optional field, you still **can** do it.
And if someone will pass a value it will be used later unless you take measures to prevent it.
```python
# note that app is an FastApi app
@ -66,18 +68,18 @@ RequestUser = User.get_pydantic(exclude={"password": ..., "category": {"priority
@app.post("/users3/", response_model=User) # here you can also use both ormar/pydantic
async def create_user3(user: RequestUser): # use the generated model here
# note how now user is pydantic and not ormar Model so you need to convert
return await User(**user.dict()).save()
return await User(**user.model_dump()).save()
```
!!!Note
To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation.
To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation.
!!!Warning
The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `dict()`, `select_all()` etc.)
The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `model_dump()`, `select_all()` etc.)
That means that nested models won't have reference to parent model (by default ormar relation is bidirectional).
That means that nested models won't have reference to parent model (by default ormar relation is bidirectional).
Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree.
Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree.
#### Mypy and type checking
@ -94,7 +96,7 @@ RequestUser = User.get_pydantic(exclude={"password": ..., "category": {"priority
@app.post("/users3/", response_model=User)
async def create_user3(user: RequestUser): # type: ignore
# note how now user is not ormar Model so you need to convert
return await User(**user.dict()).save()
return await User(**user.model_dump()).save()
```
The second one is a little bit more hacky and utilizes a way in which fastapi extract function parameters.
@ -105,7 +107,7 @@ You can overwrite the `__annotations__` entry for given param.
RequestUser = User.get_pydantic(exclude={"password": ..., "category": {"priority"}})
# do not use the app decorator
async def create_user3(user: User): # use ormar model here
return await User(**user.dict()).save()
return await User(**user.model_dump()).save()
# overwrite the function annotations entry for user param with generated model
create_user3.__annotations__["user"] = RequestUser
# manually call app functions (app.get, app.post etc.) and pass your function reference
@ -126,8 +128,7 @@ Sample:
import pydantic
class UserCreate(pydantic.BaseModel):
class Config:
orm_mode = True
model_config = pydantic.ConfigDict(from_attributes=True)
email: str
first_name: str
@ -139,5 +140,5 @@ class UserCreate(pydantic.BaseModel):
async def create_user3(user: UserCreate): # use pydantic model here
# note how now request param is a pydantic model and not the ormar one
# so you need to parse/convert it to ormar before you can use database
return await User(**user.dict()).save()
return await User(**user.model_dump()).save()
```

View File

@ -22,11 +22,13 @@ Field is not required if (any/many/all) of following:
Example:
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class User(ormar.Model):
class Meta:
tablename: str = "users"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
@ -50,9 +52,9 @@ async def create_user(user: User): # here we use ormar.Model in request paramet
That means that if you do not pass i.e. `first_name` in request it will validate correctly (as field is optional), save in the database and return the saved record without this field (which will also pass validation).
!!!Note
Note that although you do not pass the **field value**, the **field itself** is still present in the `response_model` that means it **will be present in response data** and set to `None`.
Note that although you do not pass the **field value**, the **field itself** is still present in the `response_model` that means it **will be present in response data** and set to `None`.
If you want to fully exclude the field from the result read on.
If you want to fully exclude the field from the result read on.
### FastApi `response_model_exclude`
@ -61,7 +63,7 @@ Fastapi has `response_model_exclude` that accepts a set (or a list) of field nam
That has it's limitation as `ormar` and `pydantic` accepts also dictionaries in which you can set exclude/include columns also on nested models (more on this below)
!!!Warning
Note that you cannot exclude required fields when using `response_model` as it will fail during validation.
Note that you cannot exclude required fields when using `response_model` as it will fail during validation.
```python
@app.post("/users/", response_model=User, response_model_exclude={"password"})
@ -96,9 +98,9 @@ with client as client:
```
!!!Note
Note how in above example `password` field is fully gone from the response data.
Note how in above example `password` field is fully gone from the response data.
Note that you can use this method only for non-required fields.
Note that you can use this method only for non-required fields.
#### Nested models excludes
@ -111,13 +113,13 @@ One is a dictionary with nested fields that represents the model tree structure,
Assume for a second that our user's category is a separate model:
```python
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class Category(ormar.Model):
class Meta(BaseMeta):
tablename: str = "categories"
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=255)
@ -125,8 +127,7 @@ class Category(ormar.Model):
class User(ormar.Model):
class Meta(BaseMeta):
tablename: str = "users"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
@ -147,39 +148,39 @@ Note that you can go in deeper models with double underscore, and if you want to
In example `response_model_exclude={"category__priority", "category__other_field", category__nested_model__nested_model_field}` etc.
!!!Note
To read more about possible excludes 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 and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation
!!!Note
Note that apart from `response_model_exclude` parameter `fastapi` supports also other parameters inherited from `pydantic`.
All of them works also with ormar, but can have some nuances so best to read [dict](../models/methods.md#dict) part of the documentation.
Note that apart from `response_model_exclude` parameter `fastapi` supports also other parameters inherited from `pydantic`.
All of them works also with ormar, but can have some nuances so best to read [dict](../models/methods.md#dict) part of the documentation.
### Exclude in `Model.dict()`
### Exclude in `Model.model_dump()`
Alternatively you can just return a dict from `ormar.Model` and use .
Like this you can also set exclude/include as dict and exclude fields on nested models too.
!!!Warning
Not using a `response_model` will cause api documentation having no response example and schema since in theory response can have any format.
Not using a `response_model` will cause api documentation having no response example and schema since in theory response can have any format.
```python
@app.post("/users2/", response_model=User)
async def create_user2(user: User):
user = await user.save()
return user.dict(exclude={'password'})
# could be also something like return user.dict(exclude={'category': {'priority'}}) to exclude category priority
return user.model_dump(exclude={'password'})
# could be also something like return user.model_dump(exclude={'category': {'priority'}}) to exclude category priority
```
!!!Note
Note that above example will nullify the password field even if you pass it in request, but the **field will be still there** as it's part of the response schema, the value will be set to `None`.
Note that above example will nullify the password field even if you pass it in request, but the **field will be still there** as it's part of the response schema, the value will be set to `None`.
If you want to fully exclude the field with this approach simply don't use `response_model` and exclude in Model's dict()
If you want to fully exclude the field with this approach simply don't use `response_model` and exclude in Model's model_dump()
Alternatively you can just return a dict from ormar model.
Like this you can also set exclude/include as dict and exclude fields on nested models.
!!!Note
In theory you loose validation of response here but since you operate on `ormar.Models` the response data have already been validated after db query (as ormar model is pydantic model).
In theory you loose validation of response here but since you operate on `ormar.Models` the response data have already been validated after db query (as ormar model is pydantic model).
So if you skip `response_model` altogether you can do something like this:
@ -187,13 +188,13 @@ So if you skip `response_model` altogether you can do something like this:
@app.post("/users4/") # note no response_model
async def create_user4(user: User):
user = await user.save()
return user.dict(exclude={'last_name'})
return user.model_dump(exclude={'last_name'})
```
!!!Note
Note that when you skip the response_model you can now **exclude also required fields** as the response is no longer validated after being returned.
Note that when you skip the response_model you can now **exclude also required fields** as the response is no longer validated after being returned.
The cost of this solution is that you loose also api documentation as response schema in unknown from fastapi perspective.
The cost of this solution is that you loose also api documentation as response schema in unknown from fastapi perspective.
### Generate `pydantic` model from `ormar.Model`
@ -210,14 +211,14 @@ async def create_user3(user: User):
```
!!!Note
To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation.
To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation.
!!!Warning
The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `dict()`, `select_all()` etc.)
The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `model_dump()`, `select_all()` etc.)
That means that nested models won't have reference to parent model (by default ormar relation is bidirectional).
That means that nested models won't have reference to parent model (by default ormar relation is bidirectional).
Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree.
Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree.
### Separate `pydantic` model
@ -229,8 +230,7 @@ Sample:
import pydantic
class UserBase(pydantic.BaseModel):
class Config:
orm_mode = True
model_config = pydantic.ConfigDict(from_attributes=True)
email: str
first_name: str

View File

@ -29,7 +29,6 @@ Automatically changed to True if user provide one of the following:
* `default` value or function is provided
* `server_default` value or function is provided
* `autoincrement` is set on `Integer` `primary_key` field
* **[DEPRECATED]**`pydantic_only=True` is set
Specifies if field is optional or required, used both with sql and pydantic.
@ -109,7 +108,7 @@ Used in sql only.
Sample usage:
```Python hl_lines="21-23"
```Python hl_lines="20-22"
--8<-- "../docs_src/fields/docs004.py"
```
@ -167,20 +166,6 @@ Sets the unique constraint on a table's column.
Used in sql only.
## pydantic_only (**DEPRECATED**)
**This parameter is deprecated and will be removed in one of next releases!**
**To check how to declare pydantic only fields that are not saved into database see [pydantic fields section](pydantic-fields.md)**
`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.
## overwrite_pydantic_type
By default, ormar uses predefined pydantic field types that it applies on model creation (hence the type hints are optional).
@ -189,22 +174,25 @@ If you want to, you can apply your own type, that will be **completely** replaci
So it's on you as a user to provide a type that is valid in the context of given ormar field type.
!!!warning
Note that by default you should use build in arguments that are passed to underlying pydantic field.
You can check what arguments are supported in field types section or in [pydantic](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) docs.
Note that by default you should use build in arguments that are passed to underlying pydantic field.
You can check what arguments are supported in field types section or in [pydantic](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) docs.
!!!danger
Setting a wrong type of pydantic field can break your model, so overwrite it only when you know what you are doing.
As it's easy to break functionality of ormar the `overwrite_pydantic_type` argument is not available on relation fields!
Setting a wrong type of pydantic field can break your model, so overwrite it only when you know what you are doing.
As it's easy to break functionality of ormar the `overwrite_pydantic_type` argument is not available on relation fields!
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
# sample overwrites
class OverwriteTest(ormar.Model):
class Meta:
tablename = "overwrites"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="overwrites")
id: int = ormar.Integer(primary_key=True)
my_int: str = ormar.Integer(overwrite_pydantic_type=PositiveInt)
@ -212,18 +200,6 @@ class OverwriteTest(ormar.Model):
overwrite_pydantic_type=Optional[Json[Dict[str, int]]])
```
## 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/index.md
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types

View File

@ -17,10 +17,14 @@ well as both-way encryption/decryption (`FERNET` backend).
To encrypt a field you need to pass at minimum `encrypt_secret` and `encrypt_backend` parameters.
```python hl_lines="7-8"
```python hl_lines="10-12"
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class Filter(ormar.Model):
class Meta(BaseMeta):
tablename = "filters"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100,
@ -59,8 +63,7 @@ Note that since this backend never decrypt the stored value it's only applicable
```python
class Hash(ormar.Model):
class Meta(BaseMeta):
tablename = "hashes"
ormar_config = base_ormar_config.copy(tablename="hashes")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=128,
@ -106,8 +109,7 @@ as the returned value is parsed to corresponding python type.
```python
class Filter(ormar.Model):
class Meta(BaseMeta):
tablename = "filters"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100,
@ -152,8 +154,7 @@ argument by `encrypt_custom_backend`.
```python
class Filter(ormar.Model):
class Meta(BaseMeta):
tablename = "filters"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100,
@ -161,4 +162,4 @@ class Filter(ormar.Model):
encrypt_backend=ormar.EncryptBackends.CUSTOM,
encrypt_custom_backend=DummyBackend
)
```
```

View File

@ -1,10 +1,10 @@
# Fields
There are 12 basic model field types and a special `ForeignKey` and `Many2Many` fields to establish relationships between models.
There are 12 basic model field types and a special `ForeignKey` and `ManyToMany` fields to establish relationships between models.
!!!tip
For explanation of `ForeignKey` and `Many2Many` fields check [relations][relations].
For explanation of `ForeignKey` and `ManyToMany` fields check [relations][relations].
Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model.
@ -160,11 +160,16 @@ That way you can i.e. set the value by API, even if value is not `utf-8` compati
```python
import base64
... # other imports skipped for brevity
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class LargeBinaryStr(ormar.Model):
class Meta:
tablename = "my_str_blobs"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="my_str_blobs")
id: int = ormar.Integer(primary_key=True)
test_binary: str = ormar.LargeBinary(
@ -215,46 +220,6 @@ So which one to use depends on the backend you use and on the column/ data type
* Sqlalchemy column: `sqlalchemy.Enum`
* Type (used for pydantic): `Type[Enum]`
#### Choices
You can change any field into `Enum` like field by passing a `choices` list that is accepted by all Field types.
It will add both: validation in `pydantic` model and will display available options in schema,
therefore it will be available in docs of `fastapi`.
If you still want to use `Enum` in your application you can do this by passing a `Enum` into choices
and later pass value of given option to a given field (note that Enum is not JsonSerializable).
```python
# note that imports and endpoints declaration
# is skipped here for brevity
from enum import Enum
class TestEnum(Enum):
val1 = 'Val1'
val2 = 'Val2'
class TestModel(ormar.Model):
class Meta:
tablename = "org"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
# pass list(Enum) to choices
enum_string: str = ormar.String(max_length=100, choices=list(TestEnum))
# sample payload coming to fastapi
response = client.post(
"/test_models/",
json={
"id": 1,
# you need to refer to the value of the `Enum` option
# if called like this, alternatively just use value
# string "Val1" in this case
"enum_string": TestEnum.val1.value
},
)
```
[relations]: ../relations/index.md
[queries]: ../queries.md

View File

@ -22,17 +22,14 @@ If you set a field as `Optional`, it defaults to `None` if not provided and that
exactly what's going to happen during loading from database.
```python
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL),
)
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class ModelTest(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=200)
@ -57,17 +54,14 @@ By setting a default value, this value will be set on initialization and databas
Note that setting a default to `None` is the same as setting the field to `Optional`.
```python
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL),
)
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class ModelTest(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=200)
@ -97,13 +91,12 @@ on initialization and each database load.
from pydantic import Field, PaymentCardNumber
# ...
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL),
)
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
CARD_NUMBERS = [
"123456789007",
@ -119,8 +112,7 @@ def get_number():
class ModelTest2(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=200)
@ -149,13 +141,12 @@ You can provide a value for the field in your `__init__()` method before calling
from pydantic import BaseModel
# ...
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL),
)
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class PydanticTest(BaseModel):
aa: str
@ -163,8 +154,7 @@ class PydanticTest(BaseModel):
class ModelTest3(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
# provide your custom init function
def __init__(self, **kwargs):
@ -192,4 +182,4 @@ assert test_check.pydantic_test.aa == "random"
```
!!!warning
If you do not provide a value in one of the above ways `ValidationError` will be raised on load from database.
If you do not provide a value in one of the above ways `ValidationError` will be raised on load from database.

View File

@ -21,7 +21,7 @@
### Overview
The `ormar` package is an async mini ORM for Python, with support for **Postgres,
The `ormar` package is an async ORM for Python, with support for **Postgres,
MySQL**, and **SQLite**.
The main benefits of using `ormar` are:
@ -53,13 +53,7 @@ Yet remember that those are - well - tests and not all solutions are suitable to
### Part of the `fastapi` ecosystem
As part of the fastapi ecosystem `ormar` is supported in libraries that somehow work with databases.
As of now `ormar` is supported by:
* [`fastapi-users`](https://github.com/frankie567/fastapi-users)
* [`fastapi-crudrouter`](https://github.com/awtkns/fastapi-crudrouter)
* [`fastapi-pagination`](https://github.com/uriyyo/fastapi-pagination)
As part of the fastapi ecosystem `ormar` is supported in selected libraries that somehow work with databases.
Ormar remains sql dialect agnostic - so only columns working in all supported backends are implemented.
@ -76,7 +70,6 @@ Ormar is built with:
* [`sqlalchemy core`][sqlalchemy-core] for query building.
* [`databases`][databases] for cross-database async support.
* [`pydantic`][pydantic] for data validation.
* `typing_extensions` for python 3.6 - 3.7
### License
@ -128,17 +121,20 @@ For tests and basic applications the `sqlalchemy` is more than enough:
# 1. Imports
import sqlalchemy
import databases
import ormar
# 2. Initialization
DATABASE_URL = "sqlite:///db.sqlite"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL),
engine=sqlalchemy.create_engine(DATABASE_URL),
)
# Define models here
# 3. Database creation and tables creation
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
base_ormar_config.metadata.create_all(engine)
```
For a sample configuration of alembic and more information regarding migrations and
@ -175,45 +171,39 @@ Note that you can find the same script in examples folder on github.
from typing import Optional
import databases
import pydantic
import ormar
import sqlalchemy
DATABASE_URL = "sqlite:///db.sqlite"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
# note that this step is optional -> all ormar cares is a internal
# class with name Meta and proper parameters, but this way you do not
# have to repeat the same parameters if you use only one database
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=databases.Database(DATABASE_URL),
engine = sqlalchemy.create_engine(DATABASE_URL),
)
# note that this step is optional -> all ormar cares is a field with name
# ormar_config # and proper parameters, but this way you do not have to repeat
# the same parameters if you use only one database
#
# Note that all type hints are optional
# below is a perfectly valid model declaration
# class Author(ormar.Model):
# class Meta(BaseMeta):
# tablename = "authors"
# ormar_config = base_ormar_config.copy(tablename="authors")
#
# id = ormar.Integer(primary_key=True) # <= notice no field types
# name = ormar.String(max_length=100)
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)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
@ -224,10 +214,9 @@ class Book(ormar.Model):
# create the database
# note that in production you should use migrations
# note that this is not required if you connect to existing database
engine = sqlalchemy.create_engine(DATABASE_URL)
# just to be sure we clear the db before
metadata.drop_all(engine)
metadata.create_all(engine)
base_ormar_config.metadata.drop_all(engine)
base_ormar_config.metadata.create_all(engine)
# all functions below are divided into functionality categories
@ -662,9 +651,7 @@ The following keyword arguments are supported on all field types.
* `server_default: Any`
* `index: bool`
* `unique: bool`
* `choices: typing.Sequence`
* `name: str`
* `pydantic_only: bool`
All fields are required unless one of the following is set:
@ -674,7 +661,6 @@ All fields are required unless one of the following is set:
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
Autoincrement is set by default on int primary keys.
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.
### Available signals

View File

@ -13,22 +13,25 @@ Ormar uses `databases` for connectivity issues, `pydantic` for validation and `s
All three should install along the installation of ormar if not present at your system before.
* databases
* pydantic>=1.5
* pydantic
* sqlalchemy
The required versions are pinned in the pyproject.toml file.
## Optional dependencies
*ormar* has three optional dependencies based on database backend you use:
### Postgresql
### Database backend
#### Postgresql
```py
pip install ormar[postgresql]
```
Will install also `asyncpg` and `psycopg2`.
### Mysql
#### Mysql
```py
pip install ormar[mysql]
@ -36,7 +39,7 @@ pip install ormar[mysql]
Will install also `aiomysql` and `pymysql`.
### Sqlite
#### Sqlite
```py
pip install ormar[sqlite]

330
docs/migration.md Normal file
View File

@ -0,0 +1,330 @@
# Migration to 0.20.0 based on pydantic 2.X.X
Version 0.20.0 provides support for pydantic v2.X.X that provides significant speed boost (validation and serialization is written in rust) and cleaner api for developers,
at the same time it drops support for pydantic v.1.X.X. There are changes in `ormar` interface corresponding to changes made in `pydantic`.
## Breaking changes
Migration to version >= 0.20.0 requires several changes in order to work properly.
## `ormar` Model configuration
Instead of defining a `Meta` class now each of the ormar models require an ormar_config parameter that is an instance of the `OrmarConfig` class.
Note that the attribute must be named `ormar_config` and be an instance of the config class.
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
# ormar < 0.20
class Album(ormar.Model):
class Meta:
database = database
metadata = metadata
tablename = "albums"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
favorite: bool = ormar.Boolean(default=False)
# ormar >= 0.20
class AlbumV20(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="albums_v20"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
favorite: bool = ormar.Boolean(default=False)
```
### `OrmarConfig` api/ parameters
The `ormar_config` expose the same set of settings as `Meta` class used to provide.
That means that you can use any of the following parameters initializing the config:
```python
metadata: Optional[sqlalchemy.MetaData]
database: Optional[databases.Database]
engine: Optional[sqlalchemy.engine.Engine]
tablename: Optional[str]
order_by: Optional[List[str]]
abstract: bool
exclude_parent_fields: Optional[List[str]]
queryset_class: Type[QuerySet]
extra: Extra
constraints: Optional[List[ColumnCollectionConstraint]]
```
### `BaseMeta` equivalent - best practice
Note that to reduce the duplication of code and ease of development it's still recommended to create a base config and provide each of the models with a copy.
OrmarConfig provides a convenient `copy` method for that purpose.
The `copy` method accepts the same parameters as `OrmarConfig` init, so you can overwrite if needed, but by default it will return already existing attributes, except for: `tablename`, `order_by` and `constraints` which by default are cleared.
```python hl_lines="5-8 11 20"
import databases
import ormar
import sqlalchemy
base_ormar_config = ormar.OrmarConfig(
database=databases.Database("sqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData()
)
class AlbumV20(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="albums_v20"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class TrackV20(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="tracks_v20"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
## `choices` Field parameter is no longer supported.
Before version 0.20 you could provide `choices` parameter to any existing ormar Field to limit the accepted values.
This functionality was dropped, and you should use `ormar.Enum` field that was designed for this purpose.
If you want to keep the database field type (i.e. an Integer field) you can always write a custom validator.
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
# ormar < 0.20
class Artist(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
country: str = ormar.String(default=False, max_length=50, choices=["UK", "US", "Vietnam", "Colombia"])
# ormar >= 0.20
from enum import Enum
class Country(str, Enum):
UK = "UK"
US = "US"
VIETNAM = "Vietnam"
COLOMBIA = "Colombia"
class ArtistV20(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="artists_v20"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
country: Country = ormar.Enum(enum_class=Country)
```
## `pydantic_only` Field parameter is no longer supported
`pydantic_only` fields were already deprecated and are removed in v 0.20. Ormar allows defining pydantic fields as in ordinary pydantic model.
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
# ormar < 0.20
class Dish(ormar.Model):
class Meta:
database = database
metadata = metadata
tablename = "dishes"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
cook: str = ormar.String(max_length=40, pydantic_only=True, default="sam")
# ormar >= 0.20
class DishV20(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="dishes_v20"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
cook: str = "sam" # this is normal pydantic field
```
## `property_field` decorator is no longer supported
`property_field` decorator was used to provide a way to pass calculated fields that were included in dictionary/ serialized json representation of the model.
Version 2.X of pydantic introduced such a possibility, so you should now switch to the one native to the pydantic.
```python
import databases
import ormar
import sqlalchemy
import pydantic
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
# ormar < 0.20
class Employee(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=100)
last_name: str = ormar.String(max_length=100)
@ormar.property_field()
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
# ormar >= 0.20
class EmployeeV20(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=100)
last_name: str = ormar.String(max_length=100)
@pydantic.computed_field()
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
```
## Deprecated methods
All methods listed below are deprecated and will be removed in version 0.30 of `ormar`.
### `dict()` becomes the `model_dump()`
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Album(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="albums"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
favorite: bool = ormar.Boolean(default=False)
album = Album(name="Dark Side of the Moon")
# ormar < 0.20
album_dict = album.dict()
# ormar >= 0.20
new_album_dict = album.model_dump()
```
Note that parameters remain the same i.e. `include`, `exclude` etc.
### `json()` becomes the `model_dump_json()`
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Album(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="albums"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
favorite: bool = ormar.Boolean(default=False)
album = Album(name="Dark Side of the Moon")
# ormar < 0.20
album_json= album.json()
# ormar >= 0.20
new_album_dict = album.model_dump_json()
```
Note that parameters remain the same i.e. `include`, `exclude` etc.
### `construct()` becomes the `model_construct()`
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Album(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="albums"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
favorite: bool = ormar.Boolean(default=False)
params = {
"name": "Dark Side of the Moon",
"favorite": True,
}
# ormar < 0.20
album = Album.construct(**params)
# ormar >= 0.20
album = Album.model_construct(**params)
```
To read more about construct please refer to `pydantic` documentation.

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

View File

@ -1,14 +1,6 @@
To provide better errors check you should use mypy with pydantic [plugin][plugin]
Note that legacy model declaration type will raise static type analyzers errors.
So you **cannot use the old notation** like this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs011.py"
```
Instead switch to notation introduced in version 0.4.0.
Please use notation introduced in version 0.4.0.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs012.py"

View File

@ -32,10 +32,11 @@ the count will be the total number of rows returned
```python
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="book"
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
@ -60,10 +61,11 @@ Returns a bool value to confirm if there are rows matching the given criteria (a
```python
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="book"
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)

View File

@ -30,10 +30,12 @@ The allowed kwargs are `Model` fields names and proper value types.
```python
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="album"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -52,7 +54,7 @@ await malibu.save()
```
!!!tip
Check other `Model` methods in [models][models]
Check other `Model` methods in [models][models]
## get_or_create
@ -68,10 +70,11 @@ i.e. `get_or_create(_defaults: {"title": "I win"}, title="never used")` will alw
```python
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="album"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -106,7 +109,7 @@ assert album == album2
Updates the model, or in case there is no match in database creates a new one.
```Python hl_lines="26-32"
```Python hl_lines="40-48"
--8<-- "../docs_src/queries/docs003.py"
```
@ -122,7 +125,7 @@ Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
```python hl_lines="21-27"
```python hl_lines="26-32"
--8<-- "../docs_src/queries/docs004.py"
```

View File

@ -26,7 +26,7 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai
Return number of rows deleted.
```python hl_lines="26-30"
```python hl_lines="40-44"
--8<-- "../docs_src/queries/docs005.py"
```
@ -59,20 +59,14 @@ If you specify the keep_reversed flag to `False` `ormar` will also delete the re
```python
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -104,20 +98,14 @@ If you specify the keep_reversed flag to `False` `ormar` will also delete the re
```python
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -148,4 +136,4 @@ await album.tracks.clear()
await album.tracks.clear(keep_reversed=False)
```
[querysetproxy]: ../relations/queryset-proxy.md
[querysetproxy]: ../relations/queryset-proxy.md

View File

@ -35,20 +35,14 @@ a filter across an FK relationship.
```python
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -197,20 +191,14 @@ conditions.
```python
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -245,26 +233,21 @@ Since it sounds more complicated than it is, let's look at some examples.
Given a sample models like this:
```python
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
base_ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
@ -721,7 +704,7 @@ Ordering in sql will be applied in order of names you provide in order_by.
Given sample Models like following:
```python
--8 < -- "../../docs_src/queries/docs007.py"
--8<-- "../docs_src/queries/docs007.py"
```
To order by main model field just provide a field name
@ -808,7 +791,7 @@ Since order of rows in a database is not guaranteed, `ormar` **always** issues a
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 `ormar_config`.
!!!tip
To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation
@ -823,8 +806,8 @@ Order in which order_by clauses are applied is as follows:
* Explicitly passed `order_by()` calls in query
* Relation passed `orders_by` and `related_orders_by` if exists
* Model `Meta` class `orders_by`
* Model `primary_key` column ascending (fallback, used if none of above provided)
* Model's `ormar_config` object `orders_by`
* Model's `primary_key` column ascending (fallback, used if none of above provided)
**Order from only one source is applied to each `Model` (so that you can always overwrite it in a single query).**

View File

@ -46,20 +46,14 @@ To chain related `Models` relation use double underscores between names.
```python
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -82,10 +76,7 @@ You can provide a string or a list of strings (or a field/ list of fields)
```python
class SchoolClass(ormar.Model):
class Meta:
tablename = "schoolclasses"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="schoolclasses")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -93,20 +84,14 @@ class SchoolClass(ormar.Model):
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 Student(ormar.Model):
class Meta:
tablename = "students"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -115,10 +100,7 @@ class Student(ormar.Model):
class Teacher(ormar.Model):
class Meta:
tablename = "teachers"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -182,18 +164,14 @@ If `follow=True` is set it adds also related models of related models.
With sample date like follow:
```python
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
database = database
metadata = metadata
base_ormar_config = OrmarConfig(
database=databases.Database(DATABASE_URL, force_rollback=True),
metadata=sqlalchemy.MetaData(),
)
class Address(ormar.Model):
class Meta(BaseMeta):
tablename = "addresses"
ormar_config = base_ormar_config.copy(tablename="addresses")
id: int = ormar.Integer(primary_key=True)
street: str = ormar.String(max_length=100, nullable=False)
@ -203,8 +181,7 @@ class Address(ormar.Model):
class Branch(ormar.Model):
class Meta(BaseMeta):
tablename = "branches"
ormar_config = base_ormar_config.copy(tablename="branches")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False)
@ -212,8 +189,7 @@ class Branch(ormar.Model):
class Company(ormar.Model):
class Meta(BaseMeta):
tablename = "companies"
ormar_config = base_ormar_config.copy(tablename="companies")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="company_name")
@ -264,20 +240,14 @@ To chain related `Models` relation use double underscores between names.
```python
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -301,10 +271,7 @@ You can provide a string, or a list of strings
```python
class SchoolClass(ormar.Model):
class Meta:
tablename = "schoolclasses"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="schoolclasses")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -312,20 +279,14 @@ class SchoolClass(ormar.Model):
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 Student(ormar.Model):
class Meta:
tablename = "students"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -334,10 +295,7 @@ class Student(ormar.Model):
class Teacher(ormar.Model):
class Meta:
tablename = "teachers"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -432,12 +390,12 @@ might be faster despite it needs to perform three separate queries instead of on
#### Memory
`ormar` is a mini ORM meaning that it does not keep a registry of already loaded models.
`ormar` is does not keep a registry of already loaded models.
That means that in `select_related` example above you will always have 10 000 Models A,
30 000 Models B
(even if the unique number of rows in db is 3 - processing of `select_related` spawns **
new** child models for each parent model). And 60 000 Models C.
(even if the unique number of rows in db is 3 - processing of `select_related` spawns
**new** child models for each parent model). And 60 000 Models C.
If the same Model B is shared by rows 1, 10, 100 etc. and you update one of those, the
rest of rows that share the same child will **not** be updated on the spot. If you
@ -471,7 +429,7 @@ that `select_related` will use more memory as each child is instantiated as a ne
```python
# will return False (note that id is a python `builtin` function not ormar one).
id(row1.child1) == (ro100.child1)
id(row1.child1) == id(ro100.child1)
# from above - will also return False
id(model1) == id(model2)

View File

@ -22,10 +22,11 @@ Combines the `offset` and `limit` methods based on page number and size
```python
class Track(ormar.Model):
class Meta:
tablename = "track"
metadata = metadata
database = database
ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -52,10 +53,11 @@ use the `limit_raw_sql` parameter flag, and set it to `True`.
```python
class Track(ormar.Model):
class Meta:
tablename = "track"
metadata = metadata
database = database
ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -86,10 +88,11 @@ use the `limit_raw_sql` parameter flag, and set it to `True`.
```python
class Track(ormar.Model):
class Meta:
tablename = "track"
metadata = metadata
database = database
ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)

View File

@ -40,8 +40,7 @@ Example:
# declared models
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=40)
@ -49,8 +48,7 @@ class Category(ormar.Model):
class Post(ormar.Model):
class Meta(BaseMeta):
tablename = "posts"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
@ -83,16 +81,14 @@ Note how nested models columns will be prefixed with full relation path coming f
# declare models
class User(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)
class Role(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)
@ -216,8 +212,7 @@ Example:
# declared models
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=40)
@ -225,8 +220,7 @@ class Category(ormar.Model):
class Post(ormar.Model):
class Meta(BaseMeta):
tablename = "posts"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
@ -257,8 +251,7 @@ Let's complicate the relation and modify the previously mentioned Category model
```python
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=40)

View File

@ -31,10 +31,11 @@ Passing a criteria is actually calling filter(*args, **kwargs) method described
```python
class Track(ormar.Model):
class Meta:
tablename = "track"
metadata = metadata
database = database
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -74,10 +75,7 @@ a new one with given kwargs and _defaults.
```python
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -114,10 +112,7 @@ Gets the first row from the db ordered by primary key column ascending.
```python
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -143,20 +138,14 @@ If there are no rows meeting the criteria an empty list is returned.
```python
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Track(ormar.Model):
class Meta:
tablename = "track"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="track")
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -186,10 +175,7 @@ If there are no rows meeting the criteria an empty async generator is returned.
```python
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)

View File

@ -24,52 +24,7 @@ With `fields()` you can select subset of model columns to limit the data load.
Given a sample data like following:
```python
import databases
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Company(ormar.Model):
class Meta:
tablename = "companies"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
founded: int = ormar.Integer(nullable=True)
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
manufacturer = ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
gearbox_type: str = ormar.String(max_length=20, nullable=True)
gears: int = ormar.Integer(nullable=True)
aircon_type: str = ormar.String(max_length=20, nullable=True)
# build some sample data
toyota = await Company.objects.create(name="Toyota", founded=1937)
await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6,
aircon_type='Auto')
--8<-- "../docs_src/select_columns/docs001.py"
```
You can select specified fields by passing a `str, List[str], Set[str] or dict` with
@ -78,8 +33,13 @@ nested definition.
To include related models use
notation `{related_name}__{column}[__{optional_next} etc.]`.
```python hl_lines="1"
all_cars = await Car.objects.select_related('manufacturer').fields(['id', 'name', 'manufacturer__name']).all()
```python hl_lines="1-6"
all_cars = await (
Car.objects
.select_related('manufacturer')
.fields(['id', 'name', 'manufacturer__name'])
.all()
)
for car in all_cars:
# excluded columns will yield None
assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type'])
@ -97,9 +57,14 @@ for those models in fields
- implies a list of all fields for those nested models.
```python hl_lines="1"
all_cars = await Car.objects.select_related('manufacturer').fields('id').fields(
['name']).all()
```python hl_lines="1-7"
all_cars = await (
Car.objects
.select_related('manufacturer')
.fields('id')
.fields(['name'])
.all()
)
# all fields from company model are selected
assert all_cars[0].manufacturer.name == 'Toyota'
assert all_cars[0].manufacturer.founded == 1937
@ -115,8 +80,12 @@ assert all_cars[0].manufacturer.founded == 1937
You cannot exclude mandatory model columns - `manufacturer__name` in this example.
```python
await Car.objects.select_related('manufacturer').fields(
['id', 'name', 'manufacturer__founded']).all()
await (
Car.objects
.select_related('manufacturer')
.fields(['id', 'name', 'manufacturer__founded'])
.all()
)
# will raise pydantic ValidationError as company.name is required
```
@ -138,38 +107,71 @@ Below you can see examples that are equivalent:
```python
# 1. like in example above
await Car.objects.select_related('manufacturer').fields(['id', 'name', 'manufacturer__name']).all()
await (
Car.objects
.select_related('manufacturer')
.fields(['id', 'name', 'manufacturer__name'])
.all()
)
# 2. to mark a field as required use ellipsis
await Car.objects.select_related('manufacturer').fields({'id': ...,
'name': ...,
'manufacturer': {
'name': ...}
}).all()
await (
Car.objects
.select_related('manufacturer')
.fields({'id': ...,
'name': ...,
'manufacturer': {
'name': ...
}
})
.all()
)
# 3. to include whole nested model use ellipsis
await Car.objects.select_related('manufacturer').fields({'id': ...,
'name': ...,
'manufacturer': ...
}).all()
await (
Car.objects
.select_related('manufacturer')
.fields({'id': ...,
'name': ...,
'manufacturer': ...
})
.all()
)
# 4. to specify fields at last nesting level you can also use set - equivalent to 2. above
await Car.objects.select_related('manufacturer').fields({'id': ...,
'name': ...,
'manufacturer': {'name'}
}).all()
# 4. to specify fields at last nesting level
# you can also use set - equivalent to 2. above
await (
Car.objects
.select_related('manufacturer')
.fields({'id': ...,
'name': ...,
'manufacturer': {'name'}
})
.all()
)
# 5. of course set can have multiple fields
await Car.objects.select_related('manufacturer').fields({'id': ...,
'name': ...,
'manufacturer': {'name', 'founded'}
}).all()
await (
Car.objects
.select_related('manufacturer')
.fields({'id': ...,
'name': ...,
'manufacturer': {'name', 'founded'}
})
.all()
)
# 6. you can include all nested fields but it will be equivalent of 3. above which is shorter
await Car.objects.select_related('manufacturer').fields({'id': ...,
'name': ...,
'manufacturer': {'id', 'name', 'founded'}
}).all()
# 6. you can include all nested fields,
# but it will be equivalent of 3. above which is shorter
await (
Car.objects
.select_related('manufacturer')
.fields({'id': ...,
'name': ...,
'manufacturer': {'id', 'name', 'founded'}
})
.all()
)
```
@ -201,74 +203,65 @@ exclude fields from whole hierarchy.
Below you can find few simple examples:
```python hl_lines="47 48 60 61 67"
import databases
import sqlalchemy
```python
--8<-- "../docs_src/select_columns/docs001.py"
```
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Company(ormar.Model):
class Meta:
tablename = "companies"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
founded: int = ormar.Integer(nullable=True)
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
manufacturer = ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
gearbox_type: str = ormar.String(max_length=20, nullable=True)
gears: int = ormar.Integer(nullable=True)
aircon_type: str = ormar.String(max_length=20, nullable=True)
# build some sample data
toyota = await Company.objects.create(name="Toyota", founded=1937)
await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6,
aircon_type='Auto')
# select manufacturer but only name - to include related models use notation {model_name}__{column}
all_cars = await Car.objects.select_related('manufacturer').exclude_fields(
['year', 'gearbox_type', 'gears', 'aircon_type', 'company__founded']).all()
```python
# select manufacturer but only name,
# to include related models use notation {model_name}__{column}
all_cars = await (
Car.objects
.select_related('manufacturer')
.exclude_fields([
'year',
'gearbox_type',
'gears',
'aircon_type',
'company__founded'
])
.all()
)
for car in all_cars:
# excluded columns will yield None
assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type'])
# included column on related models will be available, pk column is always included
assert all(getattr(car, x) is None
for x in [
'year',
'gearbox_type',
'gears',
'aircon_type'
])
# included column on related models will be available,
# pk column is always included
# even if you do not include it in fields list
assert car.manufacturer.name == 'Toyota'
# also in the nested related models - you cannot exclude pk - it's always auto added
# also in the nested related models,
# you cannot exclude pk - it's always auto added
assert car.manufacturer.founded is None
# fields() can be called several times, building up the columns to select
# models selected in select_related but with no columns in fields list implies all fields
all_cars = await Car.objects.select_related('manufacturer').exclude_fields('year').exclude_fields(
['gear', 'gearbox_type']).all()
# fields() can be called several times,
# building up the columns to select
# models included in select_related
# but with no columns in fields list implies all fields
all_cars = await (
Car.objects
.select_related('manufacturer')
.exclude_fields('year')
.exclude_fields(['gear', 'gearbox_type'])
.all()
)
# all fields from company model are selected
assert all_cars[0].manufacturer.name == 'Toyota'
assert all_cars[0].manufacturer.founded == 1937
# cannot exclude mandatory model columns - company__name in this example - note usage of dict/set this time
await Car.objects.select_related('manufacturer').exclude_fields([{'company': {'name'}}]).all()
# cannot exclude mandatory model columns,
# company__name in this example - note usage of dict/set this time
await (
Car.objects
.select_related('manufacturer')
.exclude_fields([{'company': {'name'}}])
.all()
)
# will raise pydantic ValidationError as company.name is required
```

View File

@ -29,7 +29,7 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai
Return number of rows updated.
```Python hl_lines="26-28"
```Python hl_lines="42-44"
--8<-- "../docs_src/queries/docs002.py"
```
@ -44,7 +44,7 @@ Return number of rows updated.
Updates the model, or in case there is no match in database creates a new one.
```Python hl_lines="26-32"
```Python hl_lines="40-48"
--8<-- "../docs_src/queries/docs003.py"
```
@ -123,4 +123,4 @@ from other side of the relation.
[querysetproxy]: ../relations/queryset-proxy.md
[models-upsert]: ../models/methods.md#upsert
[models-save-related]: ../models/methods.md#save_related
[models-save-related]: ../models/methods.md#save_related

View File

@ -14,7 +14,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`.
To define a relation add `ForeignKey` field that points to related `Model`.
```Python hl_lines="29"
```Python hl_lines="30"
--8<-- "../docs_src/fields/docs003.py"
```
@ -24,7 +24,7 @@ To define a relation add `ForeignKey` field that points to related `Model`.
By default it's child (source) `Model` name + s, like courses in snippet below:
```Python hl_lines="29 35"
```Python hl_lines="29 36"
--8<-- "../docs_src/fields/docs001.py"
```
@ -45,15 +45,14 @@ But you cannot:
* Access the related field from reverse model with `related_name`
* Even if you `select_related` from reverse side of the model the returned models won't be populated in reversed instance (the join is not prevented so you still can `filter` and `order_by` over the relation)
* The relation won't be populated in `dict()` and `json()`
* The relation won't be populated in `model_dump()` and `model_dump_json()`
* You cannot pass the nested related objects when populating from dictionary or json (also through `fastapi`). It will be either ignored or error will be raised depending on `extra` setting in pydantic `Config`.
Example:
```python
class Author(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
@ -61,8 +60,7 @@ class Author(ormar.Model):
class Post(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
@ -82,8 +80,8 @@ authors = (
assert authors[0].first_name == "Test"
# note that posts are not populated for author even if explicitly
# included in select_related - note no posts in dict()
assert author.dict(exclude={"id"}) == {"first_name": "Test", "last_name": "Author"}
# included in select_related - note no posts in model_dump()
assert author.model_dump(exclude={"id"}) == {"first_name": "Test", "last_name": "Author"}
# still can filter through fields of related model
authors = await Author.objects.filter(posts__title="Test Post").all()
@ -112,7 +110,7 @@ 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
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()`.
@ -151,7 +149,7 @@ await department.courses.remove(course, keep_reversed=False)
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).
Like with 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
@ -173,9 +171,9 @@ To read which methods of QuerySet are available read below [querysetproxy][query
## related_name
But you can overwrite this name by providing `related_name` parameter like below:
You can overwrite related model field name by providing `related_name` parameter like below:
```Python hl_lines="29 35"
```Python hl_lines="27-29 35"
--8<-- "../docs_src/fields/docs002.py"
```
@ -230,7 +228,7 @@ You have several ways to set-up a relationship connection.
The most obvious one is to pass a related `Model` instance to the constructor.
```Python hl_lines="34-35"
```Python hl_lines="35-36"
--8<-- "../docs_src/relations/docs001.py"
```
@ -238,7 +236,7 @@ The most obvious one is to pass a related `Model` instance to the constructor.
You can setup the relation also with just the pk column value of the related model.
```Python hl_lines="37-38"
```Python hl_lines="38-39"
--8<-- "../docs_src/relations/docs001.py"
```
@ -246,9 +244,9 @@ You can setup the relation also with just the pk column value of the related mod
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.
You can build the dictionary yourself or get it from existing model with `model_dump()` method.
```Python hl_lines="40-41"
```Python hl_lines="41-42"
--8<-- "../docs_src/relations/docs001.py"
```
@ -256,7 +254,7 @@ You can build the dictionary yourself or get it from existing model with `dict()
Finally you can explicitly set it to None (default behavior if no value passed).
```Python hl_lines="43-44"
```Python hl_lines="44-45"
--8<-- "../docs_src/relations/docs001.py"
```
@ -283,4 +281,4 @@ Finally you can explicitly set it to None (default behavior if no value passed).
[fields]: ./queries.md#fields
[exclude_fields]: ./queries.md#exclude_fields
[order_by]: ./queries.md#order_by
[server_default]: ../fields/common-parameters.md#server-default
[server_default]: ../fields/common-parameters.md#server-default

View File

@ -13,7 +13,7 @@ To read more about methods, possibilities, definition etc. please read the subse
To define many-to-one relation use `ForeignKey` field.
```Python hl_lines="17"
```Python hl_lines="26"
--8<-- "../docs_src/relations/docs003.py"
```
@ -24,13 +24,11 @@ To define many-to-one relation use `ForeignKey` field.
The definition of one-to-many relation also uses `ForeignKey`, and it's registered for you automatically.
So in relation ato example above.
So in relation to example above.
```Python hl_lines="17"
```Python hl_lines="7-8"
class Department(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)
@ -52,21 +50,22 @@ class Department(ormar.Model):
To define many-to-many relation use `ManyToMany` field.
```python hl_lines="18"
```python hl_lines="19"
class Category(ormar.Model):
class Meta:
tablename = "categories"
database = database
metadata = metadata
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="categories",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=40)
class Post(ormar.Model):
class Meta:
tablename = "posts"
database = database
metadata = metadata
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
@ -92,18 +91,24 @@ side of the current query for m2m models.
So if you query from model `A` to model `B`, only model `B` has through field exposed.
Which kind of make sense, since it's a one through model/field for each of related models.
```python hl_lines="10-15"
```python hl_lines="12-21"
class Category(ormar.Model):
class Meta(BaseMeta):
tablename = "categories"
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="categories",
)
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=40)
# you can specify additional fields on through model
class PostCategory(ormar.Model):
class Meta(BaseMeta):
tablename = "posts_x_categories"
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="posts_x_categories",
)
id: int = ormar.Integer(primary_key=True)
sort_order: int = ormar.Integer(nullable=True)
@ -111,8 +116,10 @@ class PostCategory(ormar.Model):
class Post(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
@ -130,7 +137,7 @@ class Post(ormar.Model):
## Relationship default sort order
By default relations follow model default sort order so `primary_key` column ascending, or any sort order se in `Meta` class.
By default relations follow model default sort order so `primary_key` column ascending, or any sort order se in `ormar_config` object.
!!!tip
To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation
@ -143,27 +150,26 @@ columns also `Through` model columns `{through_field_name}__{column_name}`
Sample configuration might look like this:
```python hl_lines="24"
```python hl_lines="23"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
@ -186,14 +192,12 @@ In order to create auto-relation or create two models that reference each other
different relations (remember the reverse side is auto-registered for you), you need to use
`ForwardRef` from `typing` module.
```python hl_lines="1 11 14"
```python hl_lines="1 9 12"
PersonRef = ForwardRef("Person")
class Person(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -210,4 +214,4 @@ Person.update_forward_refs()
[foreign-keys]: ./foreign-key.md
[many-to-many]: ./many-to-many.md
[queryset-proxy]: ./queryset-proxy.md
[postponed-annotations]: ./postponed-annotations.md
[postponed-annotations]: ./postponed-annotations.md

View File

@ -9,7 +9,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`.
## Defining Models
```Python hl_lines="40"
```Python hl_lines="34"
--8<-- "../docs_src/relations/docs002.py"
```
@ -24,20 +24,18 @@ news = await Category.objects.create(name="News")
`ForeignKey` fields are automatically registering reverse side of the relation.
By default it's child (source) `Model` name + s, like courses in snippet below:
By default it's child (source) `Model` name + s, like `posts` in snippet below:
```python
```python hl_lines="25-26"
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=40)
class Post(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
@ -81,31 +79,31 @@ categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
If you are sure you don't want the reverse relation you can use `skip_reverse=True`
flag of the `ManyToMany`.
If you set `skip_reverse` flag internally the field is still registered on the other
side of the relationship so you can:
If you set `skip_reverse` flag internally the field is still registered on the other
side of the relationship so you can:
* `filter` by related models fields from reverse model
* `order_by` by related models fields from reverse model
But you cannot:
But you cannot:
* access the related field from reverse model with `related_name`
* even if you `select_related` from reverse side of the model the returned models won't be populated in reversed instance (the join is not prevented so you still can `filter` and `order_by` over the relation)
* the relation won't be populated in `dict()` and `json()`
* the relation won't be populated in `model_dump()` and `json()`
* you cannot pass the nested related objects when populating from dictionary or json (also through `fastapi`). It will be either ignored or error will be raised depending on `extra` setting in pydantic `Config`.
Example:
```python
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=40)
class Post(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
@ -126,8 +124,8 @@ categories = (
assert categories[0].first_name == "Test"
# note that posts are not populated for author even if explicitly
# included in select_related - note no posts in dict()
assert news.dict(exclude={"id"}) == {"name": "News"}
# included in select_related - note no posts in model_dump()
assert news.model_dump(exclude={"id"}) == {"name": "News"}
# still can filter through fields of related model
categories = await Category.objects.filter(posts__title="Hello, M2M").all()
@ -141,7 +139,7 @@ assert len(categories) == 1
Optionally if you want to add additional fields you can explicitly create and pass
the through model class.
```Python hl_lines="14-20 29"
```Python hl_lines="19-24 32"
--8<-- "../docs_src/relations/docs004.py"
```
@ -170,9 +168,7 @@ So in example like this:
```python
... # course declaration omitted
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)
@ -180,10 +176,7 @@ class Student(ormar.Model):
# will produce default Through model like follows (example simplified)
class StudentCourse(ormar.Model):
class Meta:
database = database
metadata = metadata
tablename = "students_courses"
ormar_config = base_ormar_config.copy(tablename="students_courses")
id: int = ormar.Integer(primary_key=True)
student = ormar.ForeignKey(Student) # default name
@ -199,10 +192,14 @@ Example:
```python
... # course declaration omitted
base_ormar_config = ormar.OrmarConfig(
database=databases.Database("sqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData(),
)
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)
@ -212,10 +209,7 @@ class Student(ormar.Model):
# will produce Through model like follows (example simplified)
class StudentCourse(ormar.Model):
class Meta:
database = database
metadata = metadata
tablename = "students_courses"
ormar_config = base_ormar_config.copy(tablename="student_courses")
id: int = ormar.Integer(primary_key=True)
student_id = ormar.ForeignKey(Student) # set by through_relation_name
@ -238,7 +232,7 @@ so it's useful only when additional fields are provided on `Through` model.
In a sample model setup as following:
```Python hl_lines="14-20 29"
```Python hl_lines="19-24 32"
--8<-- "../docs_src/relations/docs004.py"
```

View File

@ -14,21 +14,8 @@ First, you need to import the required ref from typing.
from typing import ForwardRef
```
But note that before python 3.7 it used to be internal, so for python <= 3.6 you need
```python
from typing import _ForwardRef as ForwardRef
```
or since `pydantic` is required by `ormar` it can handle this switch for you.
In that case you can simply import ForwardRef from pydantic regardless of your python version.
```python
from pydantic.typing import ForwardRef
```
Now we need a sample model and a reference to the same model,
which will be used to creat a self referencing relation.
which will be used to create a self referencing relation.
```python
# create the forwardref to model Person
@ -36,9 +23,7 @@ PersonRef = ForwardRef("Person")
class Person(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -72,9 +57,7 @@ PersonRef = ForwardRef("Person")
class Person(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -93,14 +76,10 @@ and through parameters.
ChildRef = ForwardRef("Child")
class ChildFriend(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
class Child(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -132,9 +111,7 @@ TeacherRef = ForwardRef("Teacher")
class Student(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -144,16 +121,11 @@ class Student(ormar.Model):
class StudentTeacher(ormar.Model):
class Meta(ModelMeta):
tablename = 'students_x_teachers'
metadata = metadata
database = db
ormar_config = base_ormar_config.copy(tablename='students_x_teachers')
class Teacher(ormar.Model):
class Meta(ModelMeta):
metadata = metadata
database = db
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -168,4 +140,4 @@ Student.update_forward_refs()
!!!warning
Remember that `related_name` needs to be unique across related models regardless
of how many relations are defined.
of how many relations are defined.

View File

@ -58,7 +58,7 @@ assert post.categories[0] == news
`get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]`
Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs and _defaults.
Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates a new one with given kwargs and _defaults.
!!!tip
Read more in queries documentation [get_or_create][get_or_create]
@ -127,7 +127,7 @@ provided Through model.
Given sample like this:
```Python hl_lines="14-20 29"
```Python hl_lines="19-24 32"
--8<-- "../docs_src/relations/docs004.py"
```
@ -174,7 +174,7 @@ Updates the related model with provided keyword arguments, return number of upda
Note that for `ManyToMany` relations update can also accept an argument with through field
name and a dictionary of fields.
```Python hl_lines="14-20 29"
```Python hl_lines="19-24 32"
--8<-- "../docs_src/relations/docs004.py"
```

File diff suppressed because it is too large Load Diff

View File

@ -14,15 +14,15 @@ import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=databases.Database("sqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData(),
)
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -34,7 +34,7 @@ You can for example define a trigger that will set `album.is_best_seller` status
Import `pre_update` decorator, for list of currently available decorators/ signals check below.
```Python hl_lines="1"
```Python hl_lines="7"
--8<-- "../docs_src/signals/docs002.py"
```
@ -54,7 +54,7 @@ for which you want to run the signal receiver.
Currently there is no way to set signal for all models at once without explicitly passing them all into registration of receiver.
```Python hl_lines="4-7"
```Python hl_lines="28-31"
--8<-- "../docs_src/signals/docs002.py"
```
@ -65,7 +65,7 @@ Currently there is no way to set signal for all models at once without explicitl
Note that our newly created function has instance and class of the instance so you can easily run database
queries inside your receivers if you want to.
```Python hl_lines="15-22"
```Python hl_lines="41-48"
--8<-- "../docs_src/signals/docs002.py"
```
@ -75,15 +75,15 @@ You can define same receiver for multiple models at once by passing a list of mo
# define a dummy debug function
@pre_update([Album, Track])
async def before_update(sender, instance, **kwargs):
print(f"{sender.get_name()}: {instance.json()}: {kwargs}")
print(f"{sender.get_name()}: {instance.model_dump_json()}: {kwargs}")
```
Of course you can also create multiple functions for the same signal and model. Each of them will run at each signal.
Of course, you can also create multiple functions for the same signal and model. Each of them will run at each signal.
```python
@pre_update(Album)
async def before_update(sender, instance, **kwargs):
print(f"{sender.get_name()}: {instance.json()}: {kwargs}")
print(f"{sender.get_name()}: {instance.model_dump_json()}: {kwargs}")
@pre_update(Album)
async def before_update2(sender, instance, **kwargs):
@ -100,13 +100,13 @@ class AlbumAuditor:
async def before_save(self, sender, instance, **kwargs):
await AuditLog(
event_type=f"{self.event_type}_SAVE", event_log=instance.json()
event_type=f"{self.event_type}_SAVE", event_log=instance.model_dump_json()
).save()
auditor = AlbumAuditor()
pre_save(Album)(auditor.before_save)
# call above has same result like the one below
Album.Meta.signals.pre_save.connect(auditor.before_save)
Album.ormar_config.signals.pre_save.connect(auditor.before_save)
# signals are also exposed on instance
album = Album(name='Miami')
album.signals.pre_save.connect(auditor.before_save)
@ -127,7 +127,7 @@ async def before_update(sender, instance, **kwargs):
instance.is_best_seller = True
# disconnect given function from signal for given Model
Album.Meta.signals.pre_save.disconnect(before_save)
Album.ormar_config.signals.pre_save.disconnect(before_save)
# signals are also exposed on instance
album = Album(name='Miami')
album.signals.pre_save.disconnect(before_save)
@ -142,7 +142,7 @@ album.signals.pre_save.disconnect(before_save)
* bulk operations (`QuerySet.bulk_create` and `QuerySet.bulk_update`) as they are designed for speed.
* queryset table level operations (`QuerySet.update` and `QuerySet.delete`) as they run on the underlying tables
(more lak raw sql update/delete operations) and do not have specific instance.
(more like raw sql update/delete operations) and do not have specific instance.
### pre_save
@ -251,23 +251,23 @@ import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=databases.Database("sqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData(),
)
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
play_count: int = ormar.Integer(default=0)
Album.Meta.signals.your_custom_signal = ormar.Signal()
Album.Meta.signals.your_custom_signal.connect(your_receiver_name)
Album.ormar_config.signals.your_custom_signal = ormar.Signal()
Album.ormar_config.signals.your_custom_signal.connect(your_receiver_name)
```
Actually under the hood signal is a `SignalEmitter` instance that keeps a dictionary of know signals, and allows you
@ -276,13 +276,13 @@ to access them as attributes. When you try to access a signal that does not exis
So example above can be simplified to. The `Signal` will be created for you.
```
Album.Meta.signals.your_custom_signal.connect(your_receiver_name)
Album.ormar_config.signals.your_custom_signal.connect(your_receiver_name)
```
Now to trigger this signal you need to call send method of the Signal.
```python
await Album.Meta.signals.your_custom_signal.send(sender=Album)
await Album.ormar_config.signals.your_custom_signal.send(sender=Album)
```
Note that sender is the only required parameter and it should be ormar Model class.
@ -290,6 +290,6 @@ Note that sender is the only required parameter and it should be ormar Model cla
Additional parameters have to be passed as keyword arguments.
```python
await Album.Meta.signals.your_custom_signal.send(sender=Album, my_param=True)
await Album.ormar_config.signals.your_custom_signal.send(sender=Album, my_param=True)
```

View File

@ -15,10 +15,10 @@ async with database.transaction():
```
!!!note
Note that it has to be the same `database` that the one used in Model's `Meta` class.
Note that it has to be the same `database` that the one used in Model's `ormar_config` object.
To avoid passing `database` instance around in your code you can extract the instance from each `Model`.
Database provided during declaration of `ormar.Model` is available through `Meta.database` and can
Database provided during declaration of `ormar.Model` is available through `ormar_config.database` and can
be reached from both class and instance.
```python
@ -26,24 +26,25 @@ import databases
import sqlalchemy
import ormar
metadata = sqlalchemy.MetaData()
database = databases.Database("sqlite:///")
base_ormar_config = OrmarConfig(
metadata=sqlalchemy.MetaData(),
database = databases.Database("sqlite:///"),
)
class Author(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=255)
# database is accessible from class
database = Author.Meta.database
database = Author.ormar_config.database
# as well as from instance
author = Author(name="Stephen King")
database = author.Meta.database
database = author.ormar_config.database
```
You can also use `.transaction()` as a function decorator on any async function: