fix __all__ error in exclude, update docs
This commit is contained in:
@ -198,10 +198,88 @@ or it can be a dictionary that can also contain nested items.
|
||||
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,
|
||||
To avoid circular updates with `follow=True` set, `save_related` keeps a set of already visited Models on each branch of relation tree,
|
||||
and won't perform nested `save_related` on Models that were already visited.
|
||||
|
||||
So if you have a diamond or circular relations types you need to perform the updates in a manual way.
|
||||
So if you have circular relations types you need to perform the updates in a manual way.
|
||||
|
||||
Note that with `save_all=True` and `follow=True` you can use `save_related()` to save whole relation tree at once.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
class Department(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
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
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
course_name: str = ormar.String(max_length=100)
|
||||
completed: bool = ormar.Boolean()
|
||||
department: Optional[Department] = ormar.ForeignKey(Department)
|
||||
|
||||
|
||||
class Student(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
courses = ormar.ManyToMany(Course)
|
||||
|
||||
to_save = {
|
||||
"department_name": "Ormar",
|
||||
"courses": [
|
||||
{"course_name": "basic1",
|
||||
"completed": True,
|
||||
"students": [
|
||||
{"name": "Jack"},
|
||||
{"name": "Abi"}
|
||||
]},
|
||||
{"course_name": "basic2",
|
||||
"completed": True,
|
||||
"students": [
|
||||
{"name": "Kate"},
|
||||
{"name": "Miranda"}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
# initializa whole tree
|
||||
department = Department(**to_save)
|
||||
|
||||
# save all at once (one after another)
|
||||
await department.save_related(follow=True, save_all=True)
|
||||
|
||||
department_check = await Department.objects.select_all(follow=True).get()
|
||||
|
||||
to_exclude = {
|
||||
"id": ...,
|
||||
"courses": {
|
||||
"id": ...,
|
||||
"students": {"id", "studentcourse"}
|
||||
}
|
||||
}
|
||||
# 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
|
||||
|
||||
```
|
||||
|
||||
|
||||
!!!warning
|
||||
`save_related()` iterates all relations and all models and upserts() them one by one,
|
||||
so it will save all models but might not be optimal in regard of number of database queries.
|
||||
|
||||
[fields]: ../fields.md
|
||||
[relations]: ../relations/index.md
|
||||
|
||||
@ -27,6 +27,66 @@ By default it's child (source) `Model` name + s, like courses in snippet below:
|
||||
|
||||
Reverse relation exposes API to manage related objects also from parent side.
|
||||
|
||||
### Skipping reverse relation
|
||||
|
||||
If you are sure you don't want the reverse relation you can use `skip_reverse=True`
|
||||
flag of the `ForeignKey`.
|
||||
|
||||
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:
|
||||
* 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()`
|
||||
* 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
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
first_name: str = ormar.String(max_length=80)
|
||||
last_name: str = ormar.String(max_length=80)
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
pass
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
author: Optional[Author] = ormar.ForeignKey(Author, skip_reverse=True)
|
||||
|
||||
# create sample data
|
||||
author = Author(first_name="Test", last_name="Author")
|
||||
post = Post(title="Test Post", author=author)
|
||||
|
||||
assert post.author == author # ok
|
||||
assert author.posts # Attribute error!
|
||||
|
||||
# but still can use in order_by
|
||||
authors = (
|
||||
await Author.objects.select_related("posts").order_by("posts__title").all()
|
||||
)
|
||||
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"}
|
||||
|
||||
# still can filter through fields of related model
|
||||
authors = await Author.objects.filter(posts__title="Test Post").all()
|
||||
assert authors[0].first_name == "Test"
|
||||
assert len(authors) == 1
|
||||
```
|
||||
|
||||
|
||||
### add
|
||||
|
||||
Adding child model from parent side causes adding related model to currently loaded parent relation,
|
||||
|
||||
@ -20,6 +20,122 @@ post = await Post.objects.create(title="Hello, M2M", author=guido)
|
||||
news = await Category.objects.create(name="News")
|
||||
```
|
||||
|
||||
## Reverse relation
|
||||
|
||||
`ForeignKey` fields are automatically registering reverse side of the relation.
|
||||
|
||||
By default it's child (source) `Model` name + s, like courses in snippet below:
|
||||
|
||||
```python
|
||||
class Category(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "categories"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=40)
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
pass
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
|
||||
|
||||
# create some sample data
|
||||
post = await Post.objects.create(title="Hello, M2M")
|
||||
news = await Category.objects.create(name="News")
|
||||
await post.categories.add(news)
|
||||
|
||||
# now you can query and access from both sides:
|
||||
post_check = Post.objects.select_related("categories").get()
|
||||
assert post_check.categories[0] == news
|
||||
|
||||
# query through auto registered reverse side
|
||||
category_check = Category.objects.select_related("posts").get()
|
||||
assert category_check.posts[0] == post
|
||||
```
|
||||
|
||||
Reverse relation exposes API to manage related objects also from parent side.
|
||||
|
||||
### related_name
|
||||
|
||||
By default, the related_name is generated in the same way as for the `ForeignKey` relation (class.name.lower()+'s'),
|
||||
but in the same way you can overwrite this name by providing `related_name` parameter like below:
|
||||
|
||||
```Python
|
||||
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
|
||||
Category, through=PostCategory, related_name="new_categories"
|
||||
)
|
||||
```
|
||||
|
||||
!!!warning
|
||||
When you provide multiple relations to the same model `ormar` can no longer auto generate
|
||||
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
|
||||
for all but one (one can be default and generated) or all related fields.
|
||||
|
||||
|
||||
### Skipping reverse relation
|
||||
|
||||
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:
|
||||
* `filter` by related models fields from reverse model
|
||||
* `order_by` by related models fields from reverse model
|
||||
|
||||
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()`
|
||||
* 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"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=40)
|
||||
|
||||
|
||||
class Post(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
pass
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories: Optional[List[Category]] = ormar.ManyToMany(Category, skip_reverse=True)
|
||||
|
||||
# create some sample data
|
||||
post = await Post.objects.create(title="Hello, M2M")
|
||||
news = await Category.objects.create(name="News")
|
||||
await post.categories.add(news)
|
||||
|
||||
assert post.categories[0] == news # ok
|
||||
assert news.posts # Attribute error!
|
||||
|
||||
# but still can use in order_by
|
||||
categories = (
|
||||
await Category.objects.select_related("posts").order_by("posts__title").all()
|
||||
)
|
||||
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"}
|
||||
|
||||
# still can filter through fields of related model
|
||||
categories = await Category.objects.filter(posts__title="Hello, M2M").all()
|
||||
assert categories[0].name == "News"
|
||||
assert len(categories) == 1
|
||||
```
|
||||
|
||||
|
||||
## Through Model
|
||||
|
||||
Optionally if you want to add additional fields you can explicitly create and pass
|
||||
@ -220,22 +336,6 @@ Reverse relation exposes QuerysetProxy API that allows you to query related mode
|
||||
|
||||
To read which methods of QuerySet are available read below [querysetproxy][querysetproxy]
|
||||
|
||||
## related_name
|
||||
|
||||
By default, the related_name is generated in the same way as for the `ForeignKey` relation (class.name.lower()+'s'),
|
||||
but in the same way you can overwrite this name by providing `related_name` parameter like below:
|
||||
|
||||
```Python
|
||||
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
|
||||
Category, through=PostCategory, related_name="new_categories"
|
||||
)
|
||||
```
|
||||
|
||||
!!!warning
|
||||
When you provide multiple relations to the same model `ormar` can no longer auto generate
|
||||
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
|
||||
for all but one (one can be default and generated) or all related fields.
|
||||
|
||||
|
||||
[queries]: ./queries.md
|
||||
[querysetproxy]: ./queryset-proxy.md
|
||||
|
||||
@ -30,10 +30,12 @@
|
||||
* Fix weakref `ReferenceError` error [#118](https://github.com/collerek/ormar/issues/118)
|
||||
* Fix error raised by Through fields when pydantic `Config.extra="forbid"` is set
|
||||
* Fix bug with `pydantic.PrivateAttr` not being initialized at `__init__` [#149](https://github.com/collerek/ormar/issues/149)
|
||||
* Fix bug with pydantic-type `exclude` in `dict()` with `__all__` key not working
|
||||
|
||||
## 💬 Other
|
||||
* Introduce link to `sqlalchemy-to-ormar` auto-translator for models
|
||||
* Provide links to fastapi ecosystem libraries that support `ormar`
|
||||
* Add transactions to docs (supported with `databases`)
|
||||
|
||||
|
||||
# 0.10.2
|
||||
|
||||
88
docs/transactions.md
Normal file
88
docs/transactions.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Transactions
|
||||
|
||||
Database transactions are supported thanks to `encode/databases` which is used to issue async queries.
|
||||
|
||||
## Basic usage
|
||||
|
||||
To use transactions use `database.transaction` as async context manager:
|
||||
|
||||
```python
|
||||
async with database.transaction():
|
||||
# everyting called here will be one transaction
|
||||
await Model1().save()
|
||||
await Model2().save()
|
||||
...
|
||||
```
|
||||
|
||||
!!!note
|
||||
Note that it has to be the same `database` that the one used in Model's `Meta` class.
|
||||
|
||||
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
|
||||
be reached from both class and instance.
|
||||
|
||||
```python
|
||||
import databases
|
||||
import sqlalchemy
|
||||
import ormar
|
||||
|
||||
metadata = sqlalchemy.MetaData()
|
||||
database = databases.Database("sqlite:///")
|
||||
|
||||
class Author(ormar.Model):
|
||||
class Meta:
|
||||
database=database
|
||||
metadata=metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=255)
|
||||
|
||||
# database is accessible from class
|
||||
database = Author.Meta.database
|
||||
|
||||
# as well as from instance
|
||||
author = Author(name="Stephen King")
|
||||
database = author.Meta.database
|
||||
|
||||
```
|
||||
|
||||
You can also use `.transaction()` as a function decorator on any async function:
|
||||
|
||||
```python
|
||||
@database.transaction()
|
||||
async def create_users(request):
|
||||
...
|
||||
```
|
||||
|
||||
Transaction blocks are managed as task-local state. Nested transactions
|
||||
are fully supported, and are implemented using database savepoints.
|
||||
|
||||
## Manual commits/ rollbacks
|
||||
|
||||
For a lower-level transaction API you can trigger it manually
|
||||
|
||||
```python
|
||||
transaction = await database.transaction()
|
||||
try:
|
||||
await transaction.start()
|
||||
...
|
||||
except:
|
||||
await transaction.rollback()
|
||||
else:
|
||||
await transaction.commit()
|
||||
```
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Transactions can also be useful during testing when you can apply force rollback
|
||||
and you do not have to clean the data after each test.
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def sample_test():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
# your test code here
|
||||
...
|
||||
```
|
||||
Reference in New Issue
Block a user