This commit is contained in:
collerek
2021-03-15 18:45:46 +01:00
parent 03e6ac6c02
commit 67904980ce
8 changed files with 449 additions and 5 deletions

View File

@ -368,6 +368,51 @@ You can set this parameter by providing `Meta` class `constraints` argument.
--8<-- "../docs_src/models/docs006.py"
```
## 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.
Sample default ordering:
```python
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"
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
# now default sort by name descending
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
orders_by = ["-name"]
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
## Model Initialization
There are two ways to create and persist the `Model` instance in the database.

View File

@ -1,15 +1,23 @@
# Aggregation functions
Currently 2 aggregation functions are supported.
Currently 6 aggregation functions are supported.
* `count() -> int`
* `exists() -> bool`
* `sum(columns) -> Any`
* `avg(columns) -> Any`
* `min(columns) -> Any`
* `max(columns) -> Any`
* `QuerysetProxy`
* `QuerysetProxy.count()` method
* `QuerysetProxy.exists()` method
* `QuerysetProxy.sum(columns)` method
* `QuerysetProxy.avg(columns)` method
* `QuerysetProxy.min(column)` method
* `QuerysetProxy.max(columns)` method
## count
@ -68,6 +76,209 @@ class Book(ormar.Model):
has_sample = await Book.objects.filter(title='Sample').exists()
```
## sum
`sum(columns) -> Any`
Returns sum value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
You cannot `sum` non numeric columns.
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
Given models like follows
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert await Book.objects.sum("year") == 5773
result = await Book.objects.sum(["year", "ranking"])
assert result == dict(year=5773, ranking=9)
try:
# cannot sum string column
await Book.objects.sum("title")
except ormar.QueryDefinitionError:
pass
assert await Author.objects.select_related("books").sum("books__year") == 5773
result = await Author.objects.select_related("books").sum(
["books__year", "books__ranking"]
)
assert result == dict(books__year=5773, books__ranking=9)
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.sum("books__year")
== 3843
)
```
## avg
`avg(columns) -> Any`
Returns avg value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
You cannot `avg` non numeric columns.
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert round(float(await Book.objects.avg("year")), 2) == 1924.33
result = await Book.objects.avg(["year", "ranking"])
assert round(float(result.get("year")), 2) == 1924.33
assert result.get("ranking") == 3.0
try:
# cannot avg string column
await Book.objects.avg("title")
except ormar.QueryDefinitionError:
pass
result = await Author.objects.select_related("books").avg("books__year")
assert round(float(result), 2) == 1924.33
result = await Author.objects.select_related("books").avg(
["books__year", "books__ranking"]
)
assert round(float(result.get("books__year")), 2) == 1924.33
assert result.get("books__ranking") == 3.0
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.avg("books__year")
== 1921.5
)
```
## min
`min(columns) -> Any`
Returns min value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert await Book.objects.min("year") == 1920
result = await Book.objects.min(["year", "ranking"])
assert result == dict(year=1920, ranking=1)
assert await Book.objects.min("title") == "Book 1"
assert await Author.objects.select_related("books").min("books__year") == 1920
result = await Author.objects.select_related("books").min(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1920, books__ranking=1)
assert (
await Author.objects.select_related("books")
.filter(books__year__gt=1925)
.min("books__year")
== 1930
)
```
## max
`max(columns) -> Any`
Returns max value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
Returns min value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert await Book.objects.max("year") == 1930
result = await Book.objects.max(["year", "ranking"])
assert result == dict(year=1930, ranking=5)
assert await Book.objects.max("title") == "Book 3"
assert await Author.objects.select_related("books").max("books__year") == 1930
result = await Author.objects.select_related("books").max(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1930, books__ranking=5)
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.max("books__year")
== 1923
)
```
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
@ -89,6 +300,26 @@ objects from other side of the relation.
Works exactly the same as [exists](./#exists) function above but allows you to select columns from related
objects from other side of the relation.
### sum
Works exactly the same as [sum](./#sum) function above but allows you to sum columns from related
objects from other side of the relation.
### avg
Works exactly the same as [avg](./#avg) function above but allows you to average columns from related
objects from other side of the relation.
### min
Works exactly the same as [min](./#min) function above but allows you to select minimum of columns from related
objects from other side of the relation.
### max
Works exactly the same as [max](./#max) function above but allows you to select maximum of columns from related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section

View File

@ -289,7 +289,7 @@ books = (
```
If you want or need to you can nest deeper conditions as deep as you want, in example to
acheive a query like this:
achieve a query like this:
sql:
```
@ -564,6 +564,38 @@ assert owner.toys[1].name == "Toy 1"
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### Default sorting in ormar
Since order of rows in a database is not guaranteed, `ormar` **always** issues an `order by` sql clause to each (part of) query even if you do not provide order yourself.
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.
!!!tip
To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation
By default the relations follow the same ordering, but you can modify the order in which related models are loaded during query by providing `orders_by` and `related_orders_by`
parameters to relations.
!!!tip
To read more about models sort order visit [relations](../relations/index.md#relationship-default-sort-order) section of documentation
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)
**Order from only one source is applied to each `Model` (so that you can always overwrite it in a single query).**
That means that if you provide explicit `order_by` for a model in a query, the `Relation` and `Model` sort orders are skipped.
If you provide a `Relation` one, the `Model` sort is skipped.
Finally, if you provide one for `Model` the default one by `primary_key` is skipped.
### QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`

View File

@ -128,6 +128,58 @@ class Post(ormar.Model):
It allows you to use `await post.categories.all()` but also `await category.posts.all()` to fetch data related only to specific post, category etc.
## 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.
!!!tip
To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation
But you can modify the order in which related models are loaded during query by providing `orders_by` and `related_orders_by`
parameters to relations.
In relations you can sort only by directly related model columns or for `ManyToMany`
columns also `Through` model columns `{through_field_name}__{column_name}`
Sample configuration might look like this:
```python hl_lines="24"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Author(ormar.Model):
class Meta(BaseMeta):
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"
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
Author, orders_by=["name"], related_orders_by=["-year"]
)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)
```
Now calls:
`await Author.objects.select_related("books").get()` - the books will be sorted by the book year descending
`await Book.objects.select_related("author").all()` - the authors will be sorted by author name ascending
## Self-reference and postponed references
In order to create auto-relation or create two models that reference each other in at least two

View File

@ -192,6 +192,47 @@ Send for `Model.update()` method.
`sender` is a `ormar.Model` class and `instance` is the model that was deleted.
### pre_relation_add
`pre_relation_add(sender: Type["Model"], instance: "Model", child: "Model",
relation_name: str, passed_args: Dict)`
Send for `Model.relation_name.add()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added, for add signals also `passed_kwargs` - dict of kwargs passed to `add()`
### post_relation_add
`post_relation_add(sender: Type["Model"], instance: "Model", child: "Model",
relation_name: str, passed_args: Dict)`
Send for `Model.relation_name.add()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added, for add signals also `passed_kwargs` - dict of kwargs passed to `add()`
### pre_relation_remove
`pre_relation_remove(sender: Type["Model"], instance: "Model", child: "Model",
relation_name: str)`
Send for `Model.relation_name.remove()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added.
### post_relation_remove
`post_relation_remove(sender: Type["Model"], instance: "Model", child: "Model",
relation_name: str, passed_args: Dict)`
Send for `Model.relation_name.remove()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added.
## Defining your own signals
Note that you can create your own signals although you will have to send them manually in your code or subclass `ormar.Model`

View File

View File

@ -0,0 +1,36 @@
from typing import Optional
import databases
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
order_by = ["-name"]
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
order_by = ["year", "-ranking"]
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)

View File

@ -71,6 +71,13 @@ class Query:
self.sorted_orders[clause] = clause.get_text_clause()
if not current_table_sorted:
self._apply_default_model_sorting()
def _apply_default_model_sorting(self) -> None:
"""
Applies orders_by from model Meta class (if provided), if it was not provided
it was filled by metaclass so it's always there and falls back to pk column
"""
for order_by in self.model_cls.Meta.orders_by:
clause = ormar.OrderAction(order_str=order_by, model_cls=self.model_cls)
self.sorted_orders[clause] = clause.get_text_clause()