fix inherited pk and add field accessor access to relations
This commit is contained in:
130
README.md
130
README.md
@ -79,7 +79,27 @@ Ormar is built with:
|
||||
As I write open-source code to solve everyday problems in my work or to promote and build strong python
|
||||
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
|
||||
|
||||
<iframe src="https://github.com/sponsors/collerek/button" title="Sponsor collerek" height="35" width="116" style="border: 0;"></iframe>
|
||||
<a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;">
|
||||
<div style="
|
||||
background-color: #21262d;
|
||||
border-color: #30363d;
|
||||
box-shadow: 0 0 transparent, 0 0 transparent;
|
||||
color: #c9d1d9 !important;
|
||||
border: 1px solid;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
line-height: 0px;
|
||||
height: 40px;
|
||||
">
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" height="16" width="16" style="fill: #db61a2">
|
||||
<path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path>
|
||||
</svg>
|
||||
<span style="color: #c9d1d9 !important;">Sponsor</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
### Migrating from `sqlalchemy` and existing databases
|
||||
|
||||
@ -176,6 +196,7 @@ class BaseMeta(ormar.ModelMeta):
|
||||
# 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"
|
||||
@ -210,15 +231,9 @@ async def create():
|
||||
# Create some records to work with through QuerySet.create method.
|
||||
# Note that queryset is exposed on each Model's class as objects
|
||||
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Hobbit",
|
||||
year=1937)
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Lord of the Rings",
|
||||
year=1955)
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Silmarillion",
|
||||
year=1977)
|
||||
await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
|
||||
await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
|
||||
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
|
||||
|
||||
# alternative creation of object divided into 2 steps
|
||||
sapkowski = Author(name="Andrzej Sapkowski")
|
||||
@ -317,9 +332,7 @@ async def delete():
|
||||
# note that despite the fact that record no longer exists in database
|
||||
# the object above is still accessible and you can use it (and i.e. save()) again.
|
||||
tolkien = silmarillion.author
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Silmarillion",
|
||||
year=1977)
|
||||
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
|
||||
|
||||
|
||||
async def joins():
|
||||
@ -371,11 +384,17 @@ async def filter_and_sort():
|
||||
# to sort decreasing use hyphen before the field name
|
||||
# same as with filter you can use double underscores to access related fields
|
||||
# Django style
|
||||
books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
|
||||
"-year").all()
|
||||
books = (
|
||||
await Book.objects.filter(author__name__icontains="tolkien")
|
||||
.order_by("-year")
|
||||
.all()
|
||||
)
|
||||
# python style
|
||||
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
|
||||
Book.year.desc()).all()
|
||||
books = (
|
||||
await Book.objects.filter(Book.author.name.icontains("tolkien"))
|
||||
.order_by(Book.year.desc())
|
||||
.all()
|
||||
)
|
||||
assert len(books) == 3
|
||||
assert books[0].title == "The Silmarillion"
|
||||
assert books[2].title == "The Hobbit"
|
||||
@ -448,25 +467,68 @@ async def aggregations():
|
||||
# count:
|
||||
assert 2 == await Author.objects.count()
|
||||
|
||||
# exists:
|
||||
# exists
|
||||
assert await Book.objects.filter(title="The Hobbit").exists()
|
||||
|
||||
# max:
|
||||
# maximum
|
||||
assert 1990 == await Book.objects.max(columns=["year"])
|
||||
|
||||
# min:
|
||||
# minimum
|
||||
assert 1937 == await Book.objects.min(columns=["year"])
|
||||
|
||||
# avg:
|
||||
# average
|
||||
assert 1964.75 == await Book.objects.avg(columns=["year"])
|
||||
|
||||
# sum:
|
||||
# sum
|
||||
assert 7859 == await Book.objects.sum(columns=["year"])
|
||||
|
||||
# to read more about aggregated functions
|
||||
# visit: https://collerek.github.io/ormar/queries/aggregations/
|
||||
|
||||
|
||||
|
||||
async def raw_data():
|
||||
# extract raw data in a form of dicts or tuples
|
||||
# note that this skips the validation(!) as models are
|
||||
# not created from parsed data
|
||||
|
||||
# get list of objects as dicts
|
||||
assert await Book.objects.values() == [
|
||||
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
|
||||
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
|
||||
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
|
||||
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
|
||||
]
|
||||
|
||||
# get list of objects as tuples
|
||||
assert await Book.objects.values_list() == [
|
||||
(1, 1, "The Hobbit", 1937),
|
||||
(2, 1, "The Lord of the Rings", 1955),
|
||||
(4, 2, "The Witcher", 1990),
|
||||
(5, 1, "The Silmarillion", 1977),
|
||||
]
|
||||
|
||||
# filter data - note how you always get a list
|
||||
assert await Book.objects.filter(title="The Hobbit").values() == [
|
||||
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
|
||||
]
|
||||
|
||||
# select only wanted fields
|
||||
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
|
||||
{"id": 1, "title": "The Hobbit"}
|
||||
]
|
||||
|
||||
# if you select only one column you could flatten it with values_list
|
||||
assert await Book.objects.values_list("title", flatten=True) == [
|
||||
"The Hobbit",
|
||||
"The Lord of the Rings",
|
||||
"The Witcher",
|
||||
"The Silmarillion",
|
||||
]
|
||||
|
||||
# to read more about extracting raw values
|
||||
# visit: https://collerek.github.io/ormar/queries/aggregations/
|
||||
|
||||
|
||||
async def with_connect(function):
|
||||
# note that for any other backend than sqlite you actually need to
|
||||
# connect to the database to perform db operations
|
||||
@ -477,15 +539,25 @@ async def with_connect(function):
|
||||
# in your endpoints but have a global connection pool
|
||||
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
|
||||
|
||||
|
||||
# gather and execute all functions
|
||||
# note - normally import should be at the beginning of the file
|
||||
import asyncio
|
||||
|
||||
# note that normally you use gather() function to run several functions
|
||||
# concurrently but we actually modify the data and we rely on the order of functions
|
||||
for func in [create, read, update, delete, joins,
|
||||
filter_and_sort, subset_of_columns,
|
||||
pagination, aggregations]:
|
||||
for func in [
|
||||
create,
|
||||
read,
|
||||
update,
|
||||
delete,
|
||||
joins,
|
||||
filter_and_sort,
|
||||
subset_of_columns,
|
||||
pagination,
|
||||
aggregations,
|
||||
raw_data,
|
||||
]:
|
||||
print(f"Executing: {func.__name__}")
|
||||
asyncio.run(with_connect(func))
|
||||
|
||||
@ -523,6 +595,8 @@ metadata.drop_all(engine)
|
||||
* `fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||
* `order_by(columns:Union[List, str]) -> QuerySet`
|
||||
* `values(fields: Union[List, str, Set, Dict])`
|
||||
* `values_list(fields: Union[List, str, Set, Dict])`
|
||||
|
||||
|
||||
#### Relation types
|
||||
@ -584,6 +658,10 @@ Signals allow to trigger your function for a given event on a given Model.
|
||||
* `post_update`
|
||||
* `pre_delete`
|
||||
* `post_delete`
|
||||
* `pre_relation_add`
|
||||
* `post_relation_add`
|
||||
* `pre_relation_remove`
|
||||
* `post_relation_remove`
|
||||
|
||||
|
||||
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
||||
|
||||
148
docs/index.md
148
docs/index.md
@ -79,7 +79,27 @@ Ormar is built with:
|
||||
As I write open-source code to solve everyday problems in my work or to promote and build strong python
|
||||
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
|
||||
|
||||
<iframe src="https://github.com/sponsors/collerek/button" title="Sponsor collerek" height="35" width="116" style="border: 0;"></iframe>
|
||||
<a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;">
|
||||
<div style="
|
||||
background-color: #21262d;
|
||||
border-color: #30363d;
|
||||
box-shadow: 0 0 transparent, 0 0 transparent;
|
||||
color: #c9d1d9 !important;
|
||||
border: 1px solid;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
line-height: 0px;
|
||||
height: 40px;
|
||||
">
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" height="16" width="16" style="fill: #db61a2">
|
||||
<path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path>
|
||||
</svg>
|
||||
<span style="color: #c9d1d9 !important;">Sponsor</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
### Migrating from `sqlalchemy` and existing databases
|
||||
|
||||
@ -176,6 +196,7 @@ class BaseMeta(ormar.ModelMeta):
|
||||
# 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"
|
||||
@ -210,15 +231,9 @@ async def create():
|
||||
# Create some records to work with through QuerySet.create method.
|
||||
# Note that queryset is exposed on each Model's class as objects
|
||||
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Hobbit",
|
||||
year=1937)
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Lord of the Rings",
|
||||
year=1955)
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Silmarillion",
|
||||
year=1977)
|
||||
await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
|
||||
await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
|
||||
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
|
||||
|
||||
# alternative creation of object divided into 2 steps
|
||||
sapkowski = Author(name="Andrzej Sapkowski")
|
||||
@ -317,27 +332,43 @@ async def delete():
|
||||
# note that despite the fact that record no longer exists in database
|
||||
# the object above is still accessible and you can use it (and i.e. save()) again.
|
||||
tolkien = silmarillion.author
|
||||
await Book.objects.create(author=tolkien,
|
||||
title="The Silmarillion",
|
||||
year=1977)
|
||||
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
|
||||
|
||||
|
||||
async def joins():
|
||||
# Tho join two models use select_related
|
||||
|
||||
# Django style
|
||||
book = await Book.objects.select_related("author").get(title="The Hobbit")
|
||||
# Python style
|
||||
book = await Book.objects.select_related(Book.author).get(
|
||||
Book.title == "The Hobbit"
|
||||
)
|
||||
|
||||
# now the author is already prefetched
|
||||
assert book.author.name == "J.R.R. Tolkien"
|
||||
|
||||
# By default you also get a second side of the relation
|
||||
# constructed as lowercase source model name +'s' (books in this case)
|
||||
# you can also provide custom name with parameter related_name
|
||||
|
||||
# Django style
|
||||
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
|
||||
# Python style
|
||||
author = await Author.objects.select_related(Author.books).all(
|
||||
Author.name == "J.R.R. Tolkien"
|
||||
)
|
||||
assert len(author[0].books) == 3
|
||||
|
||||
# for reverse and many to many relations you can also prefetch_related
|
||||
# that executes a separate query for each of related models
|
||||
|
||||
# Django style
|
||||
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
|
||||
# Python style
|
||||
author = await Author.objects.prefetch_related(Author.books).get(
|
||||
Author.name == "J.R.R. Tolkien"
|
||||
)
|
||||
assert len(author.books) == 3
|
||||
|
||||
# to read more about relations
|
||||
@ -371,11 +402,17 @@ async def filter_and_sort():
|
||||
# to sort decreasing use hyphen before the field name
|
||||
# same as with filter you can use double underscores to access related fields
|
||||
# Django style
|
||||
books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
|
||||
"-year").all()
|
||||
books = (
|
||||
await Book.objects.filter(author__name__icontains="tolkien")
|
||||
.order_by("-year")
|
||||
.all()
|
||||
)
|
||||
# python style
|
||||
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
|
||||
Book.year.desc()).all()
|
||||
books = (
|
||||
await Book.objects.filter(Book.author.name.icontains("tolkien"))
|
||||
.order_by(Book.year.desc())
|
||||
.all()
|
||||
)
|
||||
assert len(books) == 3
|
||||
assert books[0].title == "The Silmarillion"
|
||||
assert books[2].title == "The Hobbit"
|
||||
@ -448,25 +485,68 @@ async def aggregations():
|
||||
# count:
|
||||
assert 2 == await Author.objects.count()
|
||||
|
||||
# exists:
|
||||
# exists
|
||||
assert await Book.objects.filter(title="The Hobbit").exists()
|
||||
|
||||
# max:
|
||||
# maximum
|
||||
assert 1990 == await Book.objects.max(columns=["year"])
|
||||
|
||||
# min:
|
||||
# minimum
|
||||
assert 1937 == await Book.objects.min(columns=["year"])
|
||||
|
||||
# avg:
|
||||
# average
|
||||
assert 1964.75 == await Book.objects.avg(columns=["year"])
|
||||
|
||||
# sum:
|
||||
# sum
|
||||
assert 7859 == await Book.objects.sum(columns=["year"])
|
||||
|
||||
# to read more about aggregated functions
|
||||
# visit: https://collerek.github.io/ormar/queries/aggregations/
|
||||
|
||||
|
||||
|
||||
async def raw_data():
|
||||
# extract raw data in a form of dicts or tuples
|
||||
# note that this skips the validation(!) as models are
|
||||
# not created from parsed data
|
||||
|
||||
# get list of objects as dicts
|
||||
assert await Book.objects.values() == [
|
||||
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
|
||||
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
|
||||
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
|
||||
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
|
||||
]
|
||||
|
||||
# get list of objects as tuples
|
||||
assert await Book.objects.values_list() == [
|
||||
(1, 1, "The Hobbit", 1937),
|
||||
(2, 1, "The Lord of the Rings", 1955),
|
||||
(4, 2, "The Witcher", 1990),
|
||||
(5, 1, "The Silmarillion", 1977),
|
||||
]
|
||||
|
||||
# filter data - note how you always get a list
|
||||
assert await Book.objects.filter(title="The Hobbit").values() == [
|
||||
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
|
||||
]
|
||||
|
||||
# select only wanted fields
|
||||
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
|
||||
{"id": 1, "title": "The Hobbit"}
|
||||
]
|
||||
|
||||
# if you select only one column you could flatten it with values_list
|
||||
assert await Book.objects.values_list("title", flatten=True) == [
|
||||
"The Hobbit",
|
||||
"The Lord of the Rings",
|
||||
"The Witcher",
|
||||
"The Silmarillion",
|
||||
]
|
||||
|
||||
# to read more about extracting raw values
|
||||
# visit: https://collerek.github.io/ormar/queries/aggregations/
|
||||
|
||||
|
||||
async def with_connect(function):
|
||||
# note that for any other backend than sqlite you actually need to
|
||||
# connect to the database to perform db operations
|
||||
@ -477,15 +557,25 @@ async def with_connect(function):
|
||||
# in your endpoints but have a global connection pool
|
||||
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
|
||||
|
||||
|
||||
# gather and execute all functions
|
||||
# note - normally import should be at the beginning of the file
|
||||
import asyncio
|
||||
|
||||
# note that normally you use gather() function to run several functions
|
||||
# concurrently but we actually modify the data and we rely on the order of functions
|
||||
for func in [create, read, update, delete, joins,
|
||||
filter_and_sort, subset_of_columns,
|
||||
pagination, aggregations]:
|
||||
for func in [
|
||||
create,
|
||||
read,
|
||||
update,
|
||||
delete,
|
||||
joins,
|
||||
filter_and_sort,
|
||||
subset_of_columns,
|
||||
pagination,
|
||||
aggregations,
|
||||
raw_data,
|
||||
]:
|
||||
print(f"Executing: {func.__name__}")
|
||||
asyncio.run(with_connect(func))
|
||||
|
||||
@ -523,6 +613,8 @@ metadata.drop_all(engine)
|
||||
* `fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||
* `order_by(columns:Union[List, str]) -> QuerySet`
|
||||
* `values(fields: Union[List, str, Set, Dict])`
|
||||
* `values_list(fields: Union[List, str, Set, Dict])`
|
||||
|
||||
|
||||
#### Relation types
|
||||
@ -584,6 +676,10 @@ Signals allow to trigger your function for a given event on a given Model.
|
||||
* `post_update`
|
||||
* `pre_delete`
|
||||
* `post_delete`
|
||||
* `pre_relation_add`
|
||||
* `post_relation_add`
|
||||
* `pre_relation_remove`
|
||||
* `post_relation_remove`
|
||||
|
||||
|
||||
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
|
||||
|
||||
@ -69,11 +69,16 @@ class Track(ormar.Model):
|
||||
```
|
||||
|
||||
```python
|
||||
# Django style
|
||||
album = await Album.objects.select_related("tracks").all()
|
||||
|
||||
# Python style
|
||||
album = await Album.objects.select_related(Album.tracks).all()
|
||||
|
||||
# will return album will all columns tracks
|
||||
```
|
||||
|
||||
You can provide a string or a list of strings
|
||||
You can provide a string or a list of strings (or a field/ list of fields)
|
||||
|
||||
```python
|
||||
class SchoolClass(ormar.Model):
|
||||
@ -122,8 +127,14 @@ class Teacher(ormar.Model):
|
||||
```
|
||||
|
||||
```python
|
||||
# Django style
|
||||
classes = await SchoolClass.objects.select_related(
|
||||
["teachers__category", "students"]).all()
|
||||
|
||||
# Python style
|
||||
classes = await SchoolClass.objects.select_related(
|
||||
[SchoolClass.teachers.category, SchoolClass.students]).all()
|
||||
|
||||
# will return classes with teachers and teachers categories
|
||||
# as well as classes students
|
||||
```
|
||||
@ -276,7 +287,13 @@ class Track(ormar.Model):
|
||||
```
|
||||
|
||||
```python
|
||||
# Django style
|
||||
album = await Album.objects.prefetch_related("tracks").all()
|
||||
|
||||
# Python style
|
||||
album = await Album.objects.prefetch_related(Album.tracks).all()
|
||||
|
||||
|
||||
# will return album will all columns tracks
|
||||
```
|
||||
|
||||
@ -329,8 +346,13 @@ class Teacher(ormar.Model):
|
||||
```
|
||||
|
||||
```python
|
||||
# Django style
|
||||
classes = await SchoolClass.objects.prefetch_related(
|
||||
["teachers__category", "students"]).all()
|
||||
|
||||
# Python style
|
||||
classes = await SchoolClass.objects.prefetch_related(
|
||||
[SchoolClass.teachers.category, SchoolClass.students]).all()
|
||||
# will return classes with teachers and teachers categories
|
||||
# as well as classes students
|
||||
```
|
||||
|
||||
@ -1,3 +1,23 @@
|
||||
# 0.10.13
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Allow passing field accessors in `select_related` and `prefetch_related` aka. python style `select_related` [#225](https://github.com/collerek/ormar/issues/225).
|
||||
* Previously:
|
||||
```python
|
||||
await Post.objects.select_related(["author", "categories"]).get()
|
||||
await Author.objects.prefetch_related("posts__categories").get()
|
||||
```
|
||||
* Now also:
|
||||
```python
|
||||
await Post.objects.select_related([Post.author, Post.categories]).get()
|
||||
await Author.objects.prefetch_related(Author.posts.categories).get()
|
||||
```
|
||||
|
||||
## 🐛 Fixes
|
||||
|
||||
* Fix overwriting default value for inherited primary key [#253](https://github.com/collerek/ormar/issues/253)
|
||||
|
||||
# 0.10.12
|
||||
|
||||
## 🐛 Fixes
|
||||
|
||||
@ -169,20 +169,37 @@ async def delete():
|
||||
|
||||
async def joins():
|
||||
# Tho join two models use select_related
|
||||
|
||||
# Django style
|
||||
book = await Book.objects.select_related("author").get(title="The Hobbit")
|
||||
# Python style
|
||||
book = await Book.objects.select_related(Book.author).get(
|
||||
Book.title == "The Hobbit"
|
||||
)
|
||||
|
||||
# now the author is already prefetched
|
||||
assert book.author.name == "J.R.R. Tolkien"
|
||||
|
||||
# By default you also get a second side of the relation
|
||||
# constructed as lowercase source model name +'s' (books in this case)
|
||||
# you can also provide custom name with parameter related_name
|
||||
# Django style
|
||||
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
|
||||
# Python style
|
||||
author = await Author.objects.select_related(Author.books).all(
|
||||
Author.name == "J.R.R. Tolkien"
|
||||
)
|
||||
assert len(author[0].books) == 3
|
||||
|
||||
# for reverse and many to many relations you can also prefetch_related
|
||||
# that executes a separate query for each of related models
|
||||
|
||||
# Django style
|
||||
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
|
||||
# Python style
|
||||
author = await Author.objects.prefetch_related(Author.books).get(
|
||||
Author.name == "J.R.R. Tolkien"
|
||||
)
|
||||
assert len(author.books) == 3
|
||||
|
||||
# to read more about relations
|
||||
@ -302,13 +319,13 @@ async def aggregations():
|
||||
# exists
|
||||
assert await Book.objects.filter(title="The Hobbit").exists()
|
||||
|
||||
# max
|
||||
# maximum
|
||||
assert 1990 == await Book.objects.max(columns=["year"])
|
||||
|
||||
# min
|
||||
# minimum
|
||||
assert 1937 == await Book.objects.min(columns=["year"])
|
||||
|
||||
# avg
|
||||
# average
|
||||
assert 1964.75 == await Book.objects.avg(columns=["year"])
|
||||
|
||||
# sum
|
||||
@ -318,6 +335,49 @@ async def aggregations():
|
||||
# visit: https://collerek.github.io/ormar/queries/aggregations/
|
||||
|
||||
|
||||
async def raw_data():
|
||||
# extract raw data in a form of dicts or tuples
|
||||
# note that this skips the validation(!) as models are
|
||||
# not created from parsed data
|
||||
|
||||
# get list of objects as dicts
|
||||
assert await Book.objects.values() == [
|
||||
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
|
||||
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
|
||||
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
|
||||
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
|
||||
]
|
||||
|
||||
# get list of objects as tuples
|
||||
assert await Book.objects.values_list() == [
|
||||
(1, 1, "The Hobbit", 1937),
|
||||
(2, 1, "The Lord of the Rings", 1955),
|
||||
(4, 2, "The Witcher", 1990),
|
||||
(5, 1, "The Silmarillion", 1977),
|
||||
]
|
||||
|
||||
# filter data - note how you always get a list
|
||||
assert await Book.objects.filter(title="The Hobbit").values() == [
|
||||
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
|
||||
]
|
||||
|
||||
# select only wanted fields
|
||||
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
|
||||
{"id": 1, "title": "The Hobbit"}
|
||||
]
|
||||
|
||||
# if you select only one column you could flatten it with values_list
|
||||
assert await Book.objects.values_list("title", flatten=True) == [
|
||||
"The Hobbit",
|
||||
"The Lord of the Rings",
|
||||
"The Witcher",
|
||||
"The Silmarillion",
|
||||
]
|
||||
|
||||
# to read more about extracting raw values
|
||||
# visit: https://collerek.github.io/ormar/queries/aggregations/
|
||||
|
||||
|
||||
async def with_connect(function):
|
||||
# note that for any other backend than sqlite you actually need to
|
||||
# connect to the database to perform db operations
|
||||
@ -345,6 +405,7 @@ for func in [
|
||||
subset_of_columns,
|
||||
pagination,
|
||||
aggregations,
|
||||
raw_data,
|
||||
]:
|
||||
print(f"Executing: {func.__name__}")
|
||||
asyncio.run(with_connect(func))
|
||||
|
||||
@ -76,7 +76,7 @@ class UndefinedType: # pragma no cover
|
||||
|
||||
Undefined = UndefinedType()
|
||||
|
||||
__version__ = "0.10.12"
|
||||
__version__ = "0.10.13"
|
||||
__all__ = [
|
||||
"Integer",
|
||||
"BigInteger",
|
||||
|
||||
@ -584,7 +584,11 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
|
||||
register_relation_in_alias_manager(field=field)
|
||||
add_field_descriptor(name=name, field=field, new_model=new_model)
|
||||
|
||||
if new_model.Meta.pkname not in attrs["__annotations__"]:
|
||||
if (
|
||||
new_model.Meta.pkname
|
||||
and new_model.Meta.pkname not in attrs["__annotations__"]
|
||||
and new_model.Meta.pkname not in new_model.__fields__
|
||||
):
|
||||
field_name = new_model.Meta.pkname
|
||||
attrs["__annotations__"][field_name] = Optional[int] # type: ignore
|
||||
attrs[field_name] = None
|
||||
|
||||
@ -21,7 +21,7 @@ from sqlalchemy import bindparam
|
||||
import ormar # noqa I100
|
||||
from ormar import MultipleMatches, NoMatch
|
||||
from ormar.exceptions import ModelPersistenceError, QueryDefinitionError
|
||||
from ormar.queryset import FilterQuery, SelectAction
|
||||
from ormar.queryset import FieldAccessor, FilterQuery, SelectAction
|
||||
from ormar.queryset.actions.order_action import OrderAction
|
||||
from ormar.queryset.clause import FilterGroup, QueryClause
|
||||
from ormar.queryset.prefetch_query import PrefetchQuery
|
||||
@ -353,7 +353,7 @@ class QuerySet(Generic[T]):
|
||||
"""
|
||||
return self.filter(_exclude=True, *args, **kwargs)
|
||||
|
||||
def select_related(self, related: Union[List, str]) -> "QuerySet[T]":
|
||||
def select_related(self, related: Union[List, str, FieldAccessor]) -> "QuerySet[T]":
|
||||
"""
|
||||
Allows to prefetch related models during the same query.
|
||||
|
||||
@ -372,6 +372,10 @@ class QuerySet(Generic[T]):
|
||||
"""
|
||||
if not isinstance(related, list):
|
||||
related = [related]
|
||||
related = [
|
||||
rel._access_chain if isinstance(rel, FieldAccessor) else rel
|
||||
for rel in related
|
||||
]
|
||||
|
||||
related = sorted(list(set(list(self._select_related) + related)))
|
||||
return self.rebuild_self(select_related=related,)
|
||||
@ -402,7 +406,9 @@ class QuerySet(Generic[T]):
|
||||
relations = self.model._iterate_related_models()
|
||||
return self.rebuild_self(select_related=relations,)
|
||||
|
||||
def prefetch_related(self, related: Union[List, str]) -> "QuerySet[T]":
|
||||
def prefetch_related(
|
||||
self, related: Union[List, str, FieldAccessor]
|
||||
) -> "QuerySet[T]":
|
||||
"""
|
||||
Allows to prefetch related models during query - but opposite to
|
||||
`select_related` each subsequent model is fetched in a separate database query.
|
||||
@ -422,6 +428,10 @@ class QuerySet(Generic[T]):
|
||||
"""
|
||||
if not isinstance(related, list):
|
||||
related = [related]
|
||||
related = [
|
||||
rel._access_chain if isinstance(rel, FieldAccessor) else rel
|
||||
for rel in related
|
||||
]
|
||||
|
||||
related = list(set(list(self._prefetch_related) + related))
|
||||
return self.rebuild_self(prefetch_related=related,)
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import databases
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
|
||||
import ormar
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
metadata = sqlalchemy.MetaData()
|
||||
database = databases.Database(DATABASE_URL)
|
||||
|
||||
|
||||
class BaseMeta(ormar.ModelMeta):
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
|
||||
class BaseModel(ormar.Model):
|
||||
class Meta(ormar.ModelMeta):
|
||||
abstract = True
|
||||
|
||||
id: uuid.UUID = ormar.UUID(
|
||||
primary_key=True, default=uuid.uuid4, uuid_format="string"
|
||||
)
|
||||
created_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow())
|
||||
updated_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow())
|
||||
|
||||
|
||||
class Member(BaseModel):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "members"
|
||||
|
||||
first_name: str = ormar.String(max_length=50)
|
||||
last_name: str = ormar.String(max_length=50)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def create_test_database():
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
metadata.drop_all(engine)
|
||||
metadata.create_all(engine)
|
||||
yield
|
||||
metadata.drop_all(engine)
|
||||
|
||||
|
||||
def test_model_structure():
|
||||
assert "id" in BaseModel.__fields__
|
||||
assert "id" in BaseModel.Meta.model_fields
|
||||
assert BaseModel.Meta.model_fields["id"].has_default()
|
||||
assert BaseModel.__fields__["id"].default_factory is not None
|
||||
|
||||
assert "id" in Member.__fields__
|
||||
assert "id" in Member.Meta.model_fields
|
||||
assert Member.Meta.model_fields["id"].has_default()
|
||||
assert Member.__fields__["id"].default_factory is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fields_inherited_with_default():
|
||||
async with database:
|
||||
await Member(first_name="foo", last_name="bar").save()
|
||||
await Member.objects.create(first_name="foo", last_name="bar")
|
||||
100
tests/test_relations/test_python_style_relations.py
Normal file
100
tests/test_relations/test_python_style_relations.py
Normal file
@ -0,0 +1,100 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import databases
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
|
||||
import ormar
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||
metadata = sqlalchemy.MetaData()
|
||||
|
||||
|
||||
class Author(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "authors"
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
first_name: str = ormar.String(max_length=80)
|
||||
last_name: str = ormar.String(max_length=80)
|
||||
|
||||
|
||||
class Category(ormar.Model):
|
||||
class Meta:
|
||||
tablename = "categories"
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
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
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
title: str = ormar.String(max_length=200)
|
||||
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
|
||||
author: Optional[Author] = ormar.ForeignKey(Author, related_name="author_posts")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def create_test_database():
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
metadata.create_all(engine)
|
||||
yield
|
||||
metadata.drop_all(engine)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def cleanup():
|
||||
yield
|
||||
async with database:
|
||||
PostCategory = Post.Meta.model_fields["categories"].through
|
||||
await PostCategory.objects.delete(each=True)
|
||||
await Post.objects.delete(each=True)
|
||||
await Category.objects.delete(each=True)
|
||||
await Author.objects.delete(each=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_selecting_related(cleanup):
|
||||
async with database:
|
||||
guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
|
||||
post = await Post.objects.create(title="Hello, M2M", author=guido)
|
||||
news = await Category.objects.create(name="News")
|
||||
recent = await Category.objects.create(name="Recent")
|
||||
await post.categories.add(news)
|
||||
await post.categories.add(recent)
|
||||
assert len(await post.categories.all()) == 2
|
||||
# Loads categories and posts (2 queries) and perform the join in Python.
|
||||
categories = await Category.objects.select_related(Category.posts).all()
|
||||
assert len(categories) == 2
|
||||
assert categories[0].name == "News"
|
||||
|
||||
news_posts = await news.posts.select_related(Post.author).all()
|
||||
assert news_posts[0].author == guido
|
||||
assert (await post.categories.limit(1).all())[0] == news
|
||||
assert (await post.categories.offset(1).limit(1).all())[0] == recent
|
||||
assert await post.categories.first() == news
|
||||
assert await post.categories.exists()
|
||||
|
||||
author = await Author.objects.prefetch_related(
|
||||
Author.author_posts.categories
|
||||
).get()
|
||||
assert len(author.author_posts) == 1
|
||||
assert author.author_posts[0].title == "Hello, M2M"
|
||||
assert author.author_posts[0].categories[0].name == "News"
|
||||
assert author.author_posts[0].categories[1].name == "Recent"
|
||||
|
||||
post = await Post.objects.select_related([Post.author, Post.categories]).get()
|
||||
assert len(post.categories) == 2
|
||||
assert post.categories[0].name == "News"
|
||||
assert post.categories[1].name == "Recent"
|
||||
assert post.author.first_name == "Guido"
|
||||
Reference in New Issue
Block a user