fix inherited pk and add field accessor access to relations

This commit is contained in:
collerek
2021-06-25 13:32:31 +02:00
parent cc76e9b862
commit 107404c3e8
10 changed files with 516 additions and 61 deletions

128
README.md
View File

@ -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 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. 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 ### Migrating from `sqlalchemy` and existing databases
@ -176,6 +196,7 @@ class BaseMeta(ormar.ModelMeta):
# id = ormar.Integer(primary_key=True) # <= notice no field types # id = ormar.Integer(primary_key=True) # <= notice no field types
# name = ormar.String(max_length=100) # name = ormar.String(max_length=100)
class Author(ormar.Model): class Author(ormar.Model):
class Meta(BaseMeta): class Meta(BaseMeta):
tablename = "authors" tablename = "authors"
@ -210,15 +231,9 @@ async def create():
# Create some records to work with through QuerySet.create method. # Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects # Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien") tolkien = await Author.objects.create(name="J.R.R. Tolkien")
await Book.objects.create(author=tolkien, await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
title="The Hobbit", await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
year=1937) await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
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 # alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski") sapkowski = Author(name="Andrzej Sapkowski")
@ -317,9 +332,7 @@ async def delete():
# note that despite the fact that record no longer exists in database # 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. # the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author tolkien = silmarillion.author
await Book.objects.create(author=tolkien, await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
title="The Silmarillion",
year=1977)
async def joins(): async def joins():
@ -371,11 +384,17 @@ async def filter_and_sort():
# to sort decreasing use hyphen before the field name # to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields # same as with filter you can use double underscores to access related fields
# Django style # Django style
books = await Book.objects.filter(author__name__icontains="tolkien").order_by( books = (
"-year").all() await Book.objects.filter(author__name__icontains="tolkien")
.order_by("-year")
.all()
)
# python style # python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by( books = (
Book.year.desc()).all() await Book.objects.filter(Book.author.name.icontains("tolkien"))
.order_by(Book.year.desc())
.all()
)
assert len(books) == 3 assert len(books) == 3
assert books[0].title == "The Silmarillion" assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit" assert books[2].title == "The Hobbit"
@ -448,25 +467,68 @@ async def aggregations():
# count: # count:
assert 2 == await Author.objects.count() assert 2 == await Author.objects.count()
# exists: # exists
assert await Book.objects.filter(title="The Hobbit").exists() assert await Book.objects.filter(title="The Hobbit").exists()
# max: # maximum
assert 1990 == await Book.objects.max(columns=["year"]) assert 1990 == await Book.objects.max(columns=["year"])
# min: # minimum
assert 1937 == await Book.objects.min(columns=["year"]) assert 1937 == await Book.objects.min(columns=["year"])
# avg: # average
assert 1964.75 == await Book.objects.avg(columns=["year"]) assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum: # sum
assert 7859 == await Book.objects.sum(columns=["year"]) assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions # to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/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): async def with_connect(function):
# note that for any other backend than sqlite you actually need to # note that for any other backend than sqlite you actually need to
# connect to the database to perform db operations # 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 # in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection # check https://collerek.github.io/ormar/fastapi/ and section with db connection
# gather and execute all functions # gather and execute all functions
# note - normally import should be at the beginning of the file # note - normally import should be at the beginning of the file
import asyncio import asyncio
# note that normally you use gather() function to run several functions # 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 # concurrently but we actually modify the data and we rely on the order of functions
for func in [create, read, update, delete, joins, for func in [
filter_and_sort, subset_of_columns, create,
pagination, aggregations]: read,
update,
delete,
joins,
filter_and_sort,
subset_of_columns,
pagination,
aggregations,
raw_data,
]:
print(f"Executing: {func.__name__}") print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func)) asyncio.run(with_connect(func))
@ -523,6 +595,8 @@ metadata.drop_all(engine)
* `fields(columns: Union[List, str, set, dict]) -> QuerySet` * `fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` * `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[List, str]) -> 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 #### Relation types
@ -584,6 +658,10 @@ Signals allow to trigger your function for a given event on a given Model.
* `post_update` * `post_update`
* `pre_delete` * `pre_delete`
* `post_delete` * `post_delete`
* `pre_relation_add`
* `post_relation_add`
* `pre_relation_remove`
* `post_relation_remove`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/

View File

@ -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 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. 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 ### Migrating from `sqlalchemy` and existing databases
@ -176,6 +196,7 @@ class BaseMeta(ormar.ModelMeta):
# id = ormar.Integer(primary_key=True) # <= notice no field types # id = ormar.Integer(primary_key=True) # <= notice no field types
# name = ormar.String(max_length=100) # name = ormar.String(max_length=100)
class Author(ormar.Model): class Author(ormar.Model):
class Meta(BaseMeta): class Meta(BaseMeta):
tablename = "authors" tablename = "authors"
@ -210,15 +231,9 @@ async def create():
# Create some records to work with through QuerySet.create method. # Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects # Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien") tolkien = await Author.objects.create(name="J.R.R. Tolkien")
await Book.objects.create(author=tolkien, await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
title="The Hobbit", await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
year=1937) await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
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 # alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski") sapkowski = Author(name="Andrzej Sapkowski")
@ -317,27 +332,43 @@ async def delete():
# note that despite the fact that record no longer exists in database # 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. # the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author tolkien = silmarillion.author
await Book.objects.create(author=tolkien, await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
title="The Silmarillion",
year=1977)
async def joins(): async def joins():
# Tho join two models use select_related # Tho join two models use select_related
# Django style
book = await Book.objects.select_related("author").get(title="The Hobbit") 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 # now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien" assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation # By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case) # constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name # 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") 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 assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related # for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models # 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") 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 assert len(author.books) == 3
# to read more about relations # to read more about relations
@ -371,11 +402,17 @@ async def filter_and_sort():
# to sort decreasing use hyphen before the field name # to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields # same as with filter you can use double underscores to access related fields
# Django style # Django style
books = await Book.objects.filter(author__name__icontains="tolkien").order_by( books = (
"-year").all() await Book.objects.filter(author__name__icontains="tolkien")
.order_by("-year")
.all()
)
# python style # python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by( books = (
Book.year.desc()).all() await Book.objects.filter(Book.author.name.icontains("tolkien"))
.order_by(Book.year.desc())
.all()
)
assert len(books) == 3 assert len(books) == 3
assert books[0].title == "The Silmarillion" assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit" assert books[2].title == "The Hobbit"
@ -448,25 +485,68 @@ async def aggregations():
# count: # count:
assert 2 == await Author.objects.count() assert 2 == await Author.objects.count()
# exists: # exists
assert await Book.objects.filter(title="The Hobbit").exists() assert await Book.objects.filter(title="The Hobbit").exists()
# max: # maximum
assert 1990 == await Book.objects.max(columns=["year"]) assert 1990 == await Book.objects.max(columns=["year"])
# min: # minimum
assert 1937 == await Book.objects.min(columns=["year"]) assert 1937 == await Book.objects.min(columns=["year"])
# avg: # average
assert 1964.75 == await Book.objects.avg(columns=["year"]) assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum: # sum
assert 7859 == await Book.objects.sum(columns=["year"]) assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions # to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/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): async def with_connect(function):
# note that for any other backend than sqlite you actually need to # note that for any other backend than sqlite you actually need to
# connect to the database to perform db operations # 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 # in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection # check https://collerek.github.io/ormar/fastapi/ and section with db connection
# gather and execute all functions # gather and execute all functions
# note - normally import should be at the beginning of the file # note - normally import should be at the beginning of the file
import asyncio import asyncio
# note that normally you use gather() function to run several functions # 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 # concurrently but we actually modify the data and we rely on the order of functions
for func in [create, read, update, delete, joins, for func in [
filter_and_sort, subset_of_columns, create,
pagination, aggregations]: read,
update,
delete,
joins,
filter_and_sort,
subset_of_columns,
pagination,
aggregations,
raw_data,
]:
print(f"Executing: {func.__name__}") print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func)) asyncio.run(with_connect(func))
@ -523,6 +613,8 @@ metadata.drop_all(engine)
* `fields(columns: Union[List, str, set, dict]) -> QuerySet` * `fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` * `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[List, str]) -> 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 #### Relation types
@ -584,6 +676,10 @@ Signals allow to trigger your function for a given event on a given Model.
* `post_update` * `post_update`
* `pre_delete` * `pre_delete`
* `post_delete` * `post_delete`
* `pre_relation_add`
* `post_relation_add`
* `pre_relation_remove`
* `post_relation_remove`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/

View File

@ -69,11 +69,16 @@ class Track(ormar.Model):
``` ```
```python ```python
# Django style
album = await Album.objects.select_related("tracks").all() 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 # 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 ```python
class SchoolClass(ormar.Model): class SchoolClass(ormar.Model):
@ -122,8 +127,14 @@ class Teacher(ormar.Model):
``` ```
```python ```python
# Django style
classes = await SchoolClass.objects.select_related( classes = await SchoolClass.objects.select_related(
["teachers__category", "students"]).all() ["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 # will return classes with teachers and teachers categories
# as well as classes students # as well as classes students
``` ```
@ -276,7 +287,13 @@ class Track(ormar.Model):
``` ```
```python ```python
# Django style
album = await Album.objects.prefetch_related("tracks").all() 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 # will return album will all columns tracks
``` ```
@ -329,8 +346,13 @@ class Teacher(ormar.Model):
``` ```
```python ```python
# Django style
classes = await SchoolClass.objects.prefetch_related( classes = await SchoolClass.objects.prefetch_related(
["teachers__category", "students"]).all() ["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 # will return classes with teachers and teachers categories
# as well as classes students # as well as classes students
``` ```

View File

@ -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 # 0.10.12
## 🐛 Fixes ## 🐛 Fixes

View File

@ -169,20 +169,37 @@ async def delete():
async def joins(): async def joins():
# Tho join two models use select_related # Tho join two models use select_related
# Django style
book = await Book.objects.select_related("author").get(title="The Hobbit") 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 # now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien" assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation # By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case) # constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name # 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") 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 assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related # for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models # 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") 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 assert len(author.books) == 3
# to read more about relations # to read more about relations
@ -302,13 +319,13 @@ async def aggregations():
# exists # exists
assert await Book.objects.filter(title="The Hobbit").exists() assert await Book.objects.filter(title="The Hobbit").exists()
# max # maximum
assert 1990 == await Book.objects.max(columns=["year"]) assert 1990 == await Book.objects.max(columns=["year"])
# min # minimum
assert 1937 == await Book.objects.min(columns=["year"]) assert 1937 == await Book.objects.min(columns=["year"])
# avg # average
assert 1964.75 == await Book.objects.avg(columns=["year"]) assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum # sum
@ -318,6 +335,49 @@ async def aggregations():
# visit: https://collerek.github.io/ormar/queries/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): async def with_connect(function):
# note that for any other backend than sqlite you actually need to # note that for any other backend than sqlite you actually need to
# connect to the database to perform db operations # connect to the database to perform db operations
@ -345,6 +405,7 @@ for func in [
subset_of_columns, subset_of_columns,
pagination, pagination,
aggregations, aggregations,
raw_data,
]: ]:
print(f"Executing: {func.__name__}") print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func)) asyncio.run(with_connect(func))

View File

@ -76,7 +76,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType() Undefined = UndefinedType()
__version__ = "0.10.12" __version__ = "0.10.13"
__all__ = [ __all__ = [
"Integer", "Integer",
"BigInteger", "BigInteger",

View File

@ -584,7 +584,11 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
register_relation_in_alias_manager(field=field) register_relation_in_alias_manager(field=field)
add_field_descriptor(name=name, field=field, new_model=new_model) 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 field_name = new_model.Meta.pkname
attrs["__annotations__"][field_name] = Optional[int] # type: ignore attrs["__annotations__"][field_name] = Optional[int] # type: ignore
attrs[field_name] = None attrs[field_name] = None

View File

@ -21,7 +21,7 @@ from sqlalchemy import bindparam
import ormar # noqa I100 import ormar # noqa I100
from ormar import MultipleMatches, NoMatch from ormar import MultipleMatches, NoMatch
from ormar.exceptions import ModelPersistenceError, QueryDefinitionError 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.actions.order_action import OrderAction
from ormar.queryset.clause import FilterGroup, QueryClause from ormar.queryset.clause import FilterGroup, QueryClause
from ormar.queryset.prefetch_query import PrefetchQuery from ormar.queryset.prefetch_query import PrefetchQuery
@ -353,7 +353,7 @@ class QuerySet(Generic[T]):
""" """
return self.filter(_exclude=True, *args, **kwargs) 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. Allows to prefetch related models during the same query.
@ -372,6 +372,10 @@ class QuerySet(Generic[T]):
""" """
if not isinstance(related, list): if not isinstance(related, list):
related = [related] 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))) related = sorted(list(set(list(self._select_related) + related)))
return self.rebuild_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() relations = self.model._iterate_related_models()
return self.rebuild_self(select_related=relations,) 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 Allows to prefetch related models during query - but opposite to
`select_related` each subsequent model is fetched in a separate database query. `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): if not isinstance(related, list):
related = [related] related = [related]
related = [
rel._access_chain if isinstance(rel, FieldAccessor) else rel
for rel in related
]
related = list(set(list(self._prefetch_related) + related)) related = list(set(list(self._prefetch_related) + related))
return self.rebuild_self(prefetch_related=related,) return self.rebuild_self(prefetch_related=related,)

View File

@ -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")

View 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"