diff --git a/docs/fields/common-parameters.md b/docs/fields/common-parameters.md index eb47818..6cc127d 100644 --- a/docs/fields/common-parameters.md +++ b/docs/fields/common-parameters.md @@ -121,6 +121,6 @@ Prevents insertion of value not present in the choices list. Used in pydantic only. [relations]: ../relations/index.md -[queries]: ../queries.md +[queries]: ../queries/index.md [pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types [server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions \ No newline at end of file diff --git a/docs/models/index.md b/docs/models/index.md index 0bee4ea..35ab50c 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -423,7 +423,7 @@ You can check if model is saved with `ModelInstance.saved` property [fields]: ../fields/field-types.md [relations]: ../relations/index.md -[queries]: ../queries.md +[queries]: ../queries/index.md [pydantic]: https://pydantic-docs.helpmanual.io/ [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html diff --git a/docs/models/methods.md b/docs/models/methods.md index a16369e..084ba25 100644 --- a/docs/models/methods.md +++ b/docs/models/methods.md @@ -118,7 +118,7 @@ But you can specify the `follow=True` parameter to traverse through nested model [fields]: ../fields.md [relations]: ../relations/index.md -[queries]: ../queries.md +[queries]: ../queries/index.md [pydantic]: https://pydantic-docs.helpmanual.io/ [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html diff --git a/docs/queries/aggregations.md b/docs/queries/aggregations.md index 9732314..25f5512 100644 --- a/docs/queries/aggregations.md +++ b/docs/queries/aggregations.md @@ -1,9 +1,16 @@ # Aggregation functions -`ormar` currently supports 2 aggregation functions: +Currently 2 aggregation functions are supported. -* `count() -> int` -* `exists() -> bool` + +* `count() -> int` +* `exists() -> bool` + + +* `QuerysetProxy` + * `QuerysetProxy.count()` method + * `QuerysetProxy.exists()` method + ## count @@ -11,6 +18,23 @@ Returns number of rows matching the given criteria (i.e. applied with `filter` and `exclude`) +```python +class Book(ormar.Model): + class Meta: + tablename = "books" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=200) + author: str = ormar.String(max_length=100) + genre: str = ormar.String( + max_length=100, + default="Fiction", + choices=["Fiction", "Adventure", "Historic", "Fantasy"], + ) +``` + ```python # returns count of rows in db for Books model no_of_books = await Book.objects.count() @@ -22,7 +46,49 @@ no_of_books = await Book.objects.count() Returns a bool value to confirm if there are rows matching the given criteria (applied with `filter` and `exclude`) +```python +class Book(ormar.Model): + class Meta: + tablename = "books" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=200) + author: str = ormar.String(max_length=100) + genre: str = ormar.String( + max_length=100, + default="Fiction", + choices=["Fiction", "Adventure", "Historic", "Fantasy"], + ) +``` + ```python # returns a boolean value if given row exists has_sample = await Book.objects.filter(title='Sample').exists() ``` + +## QuerysetProxy methods + +When access directly the related `ManyToMany` field as well as `ReverseForeignKey` +returns the list of related models. + +But at the same time it exposes a subset of QuerySet API, so you can filter, create, +select related etc related models directly from parent model. + +### count + +Works exactly the same as [count](./#count) function above but allows you to select columns from related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + +### exists + +Works exactly the same as [exists](./#exists) function above but allows you to select columns from related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + diff --git a/docs/queries/filter-and-sort.md b/docs/queries/filter-and-sort.md index 14b48db..2e451e3 100644 --- a/docs/queries/filter-and-sort.md +++ b/docs/queries/filter-and-sort.md @@ -22,13 +22,39 @@ And following methods to sort the data (sql order by clause). * `QuerysetProxy` * `QuerysetProxy.order_by(columns:Union[List, str])` method -## filter +## Filtering + +### filter `filter(**kwargs) -> QuerySet` Allows you to filter by any `Model` attribute/field as well as to fetch instances, with a filter across an FK relationship. +```python +class Album(ormar.Model): + class Meta: + tablename = "albums" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + is_best_seller: bool = ormar.Boolean(default=False) + +class Track(ormar.Model): + class Meta: + tablename = "tracks" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + name: str = ormar.String(max_length=100) + position: int = ormar.Integer() + play_count: int = ormar.Integer(nullable=True) +``` + ```python track = Track.objects.filter(name="The Bird").get() # will return a track with name equal to 'The Bird' @@ -67,7 +93,7 @@ You can use special filter suffix to change the filter operands: filters, it's added for you. If you include `%` in your search value it will be escaped and treated as literal percentage sign inside the text. -## exclude +### exclude `exclude(**kwargs) -> QuerySet` @@ -82,12 +108,79 @@ conditions. `exclude(name='John', age>=35)` will become `where not (name='John' and age>=35)` +```python +class Album(ormar.Model): + class Meta: + tablename = "albums" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + is_best_seller: bool = ormar.Boolean(default=False) + +class Track(ormar.Model): + class Meta: + tablename = "tracks" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + name: str = ormar.String(max_length=100) + position: int = ormar.Integer() + play_count: int = ormar.Integer(nullable=True) +``` + ```python notes = await Track.objects.exclude(position_gt=3).all() # returns all tracks with position < 3 ``` -## QuerysetProxy methods +## get + +`get(**kwargs) -> Model` + +Get's the first row from the db meeting the criteria set by kwargs. + +When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwargs).get()` + +!!!tip + To read more about `filter` go to [filter](./#filter). + + To read more about `get` go to [read/get](../read/#get) + +## get_or_create + +`get_or_create(**kwargs) -> Model` + +Combination of create and get methods. + +When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwargs).get_or_create()` + +!!!tip + To read more about `filter` go to [filter](./#filter). + + To read more about `get_or_create` go to [read/get_or_create](../read/#get_or_create) + +!!!warning + When given item does not exist you need to pass kwargs for all required fields of the + model, including but not limited to primary_key column (unless it's autoincrement). + +## all + +`all(**kwargs) -> List[Optional["Model"]]` + +Returns all rows from a database for given model for set filter options. + +When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwargs).all()` + +!!!tip + To read more about `filter` go to [filter](./#filter). + + To read more about `all` go to [read/all](../read/#all) + +### QuerysetProxy methods When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models. @@ -95,33 +188,51 @@ returns the list of related models. But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model. -### get +#### filter -Works exactly the same as [get](./#get) function above but allows you to fetch related +Works exactly the same as [filter](./#filter) function above but allows you to filter related objects from other side of the relation. !!!tip To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section -### get_or_create + +#### exclude + +Works exactly the same as [exclude](./#exclude) function above but allows you to filter related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + + +#### get + +Works exactly the same as [get](./#get) function above but allows you to filter related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + +#### get_or_create Works exactly the same as [get_or_create](./#get_or_create) function above but allows -you to query or create related objects from other side of the relation. +you to filter related objects from other side of the relation. !!!tip To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section -### all +#### all -Works exactly the same as [all](./#all) function above but allows you to query related +Works exactly the same as [all](./#all) function above but allows you to filter related objects from other side of the relation. !!!tip To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section +## Sorting - -## order_by +### order_by `order_by(columns: Union[List, str]) -> QuerySet` @@ -206,4 +317,21 @@ assert owner.toys[1].name == "Toy 1" Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` +### QuerysetProxy methods + +When access directly the related `ManyToMany` field as well as `ReverseForeignKey` +returns the list of related models. + +But at the same time it exposes subset of QuerySet API, so you can filter, create, +select related etc related models directly from parent model. + +#### order_by + +Works exactly the same as [order_by](./#order_by) function above but allows you to sort related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + + [querysetproxy]: ../relations/queryset-proxy.md \ No newline at end of file diff --git a/docs/queries/pagination-and-rows-number.md b/docs/queries/pagination-and-rows-number.md index 888eaf4..f961dce 100644 --- a/docs/queries/pagination-and-rows-number.md +++ b/docs/queries/pagination-and-rows-number.md @@ -1,11 +1,18 @@ #Pagination and rows number -* `paginate(page: int) -> QuerySet` -* `limit(limit_count: int) -> QuerySet` -* `offset(offset: int) -> QuerySet` -* `get(**kwargs): -> Model` -* `first(): -> Model` +Following methods allow you to paginate and limit number of rows in queries. +* `paginate(page: int) -> QuerySet` +* `limit(limit_count: int) -> QuerySet` +* `offset(offset: int) -> QuerySet` +* `get() -> Model` +* `first() -> Model` + + +* `QuerysetProxy` + * `QuerysetProxy.paginate(page: int)` method + * `QuerysetProxy.limit(limit_count: int)` method + * `QuerysetProxy.offset(offset: int)` method ## paginate @@ -13,6 +20,19 @@ Combines the `offset` and `limit` methods based on page number and size +```python +class Track(ormar.Model): + class Meta: + tablename = "track" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + name: str = ormar.String(max_length=100) + position: int = ormar.Integer() +``` + ```python tracks = await Track.objects.paginate(3).all() # will return 20 tracks starting at row 41 @@ -30,6 +50,19 @@ You can limit the results to desired number of parent models. To limit the actual number of database query rows instead of number of main models use the `limit_raw_sql` parameter flag, and set it to `True`. +```python +class Track(ormar.Model): + class Meta: + tablename = "track" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + name: str = ormar.String(max_length=100) + position: int = ormar.Integer() +``` + ```python tracks = await Track.objects.limit(1).all() # will return just one Track @@ -51,6 +84,19 @@ You can also offset the results by desired number of main models. To offset the actual number of database query rows instead of number of main models use the `limit_raw_sql` parameter flag, and set it to `True`. +```python +class Track(ormar.Model): + class Meta: + tablename = "track" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + name: str = ormar.String(max_length=100) + position: int = ormar.Integer() +``` + ```python tracks = await Track.objects.offset(1).limit(1).all() # will return just one Track, but this time the second one @@ -67,28 +113,57 @@ tracks = await Track.objects.offset(1).limit(1).all() ## get -`get(**kwargs): -> Model` +`get(**kwargs) -> Model` Get's the first row from the db meeting the criteria set by kwargs. -If no criteria set it will return the last row in db sorted by pk. +If no criteria is set it will return the last row in db sorted by pk. +(The criteria cannot be set also with filter/exclude). -Passing a criteria is actually calling filter(**kwargs) method described below. +!!!tip + To read more about `get` visit [read/get](./read/#get) -```python -track = await Track.objects.get(name='The Bird') -# note that above is equivalent to await Track.objects.filter(name='The Bird').get() -track2 = track = await Track.objects.get() -track == track2 # True since it's the only row in db in our example -``` - -!!!warning - If no row meets the criteria `NoMatch` exception is raised. - - If there are multiple rows meeting the criteria the `MultipleMatches` exception is raised. ## first -`first(): -> Model` +`first() -> Model` Gets the first row from the db ordered by primary key column ascending. + +!!!tip + To read more about `first` visit [read/first](./read/#first) + + +## QuerysetProxy methods + +When access directly the related `ManyToMany` field as well as `ReverseForeignKey` +returns the list of related models. + +But at the same time it exposes subset of QuerySet API, so you can filter, create, +select related etc related models directly from parent model. + +### paginate + +Works exactly the same as [paginate](./#paginate) function above but allows you to paginate related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + +### limit + +Works exactly the same as [limit](./#limit) function above but allows you to paginate related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + +### offset + +Works exactly the same as [offset](./#offset) function above but allows you to paginate related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + +[querysetproxy]: ../relations/queryset-proxy.md \ No newline at end of file diff --git a/docs/queries/select-columns.md b/docs/queries/select-columns.md index abb56f5..5128f25 100644 --- a/docs/queries/select-columns.md +++ b/docs/queries/select-columns.md @@ -1,7 +1,14 @@ # Selecting subset of columns -* `fields(columns: Union[List, str, set, dict]) -> QuerySet` -* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` +To select only chosen columns of your model you can use following functions. + +* `fields(columns: Union[List, str, set, dict]) -> QuerySet` +* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` + + +* `QuerysetProxy` + * `QuerysetProxy.fields(columns: Union[List, str, set, dict])` method + * `QuerysetProxy.exclude_fields(columns: Union[List, str, set, dict])` method ## fields @@ -9,14 +16,60 @@ With `fields()` you can select subset of model columns to limit the data load. -!!!note Note that `fields()` and `exclude_fields()` works both for main models (on -normal queries like `get`, `all` etc.) -as well as `select_related` and `prefetch_related` models (with nested notation). +!!!note + Note that `fields()` and `exclude_fields()` works both for main models (on + normal queries like `get`, `all` etc.) + as well as `select_related` and `prefetch_related` models (with nested notation). Given a sample data like following: ```python ---8 < -- "../docs_src/queries/docs006.py" +import databases +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +database = databases.Database(DATABASE_URL, force_rollback=True) +metadata = sqlalchemy.MetaData() + + +class Company(ormar.Model): + class Meta: + tablename = "companies" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + founded: int = ormar.Integer(nullable=True) + + +class Car(ormar.Model): + class Meta: + tablename = "cars" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + manufacturer = ormar.ForeignKey(Company) + name: str = ormar.String(max_length=100) + year: int = ormar.Integer(nullable=True) + gearbox_type: str = ormar.String(max_length=20, nullable=True) + gears: int = ormar.Integer(nullable=True) + aircon_type: str = ormar.String(max_length=20, nullable=True) + + +# build some sample data +toyota = await Company.objects.create(name="Toyota", founded=1937) +await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5, + aircon_type='Manual') +await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5, + aircon_type='Manual') +await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6, + aircon_type='Auto') + + ``` You can select specified fields by passing a `str, List[str], Set[str] or dict` with @@ -52,8 +105,9 @@ assert all_cars[0].manufacturer.name == 'Toyota' assert all_cars[0].manufacturer.founded == 1937 ``` -!!!warning Mandatory fields cannot be excluded as it will raise `ValidationError`, to -exclude a field it has to be nullable. +!!!warning + Mandatory fields cannot be excluded as it will raise `ValidationError`, to + exclude a field it has to be nullable. You cannot exclude mandatory model columns - `manufacturer__name` in this example. @@ -63,8 +117,9 @@ await Car.objects.select_related('manufacturer').fields( # will raise pydantic ValidationError as company.name is required ``` -!!!tip Pk column cannot be excluded - it's always auto added even if not explicitly -included. +!!!tip + Pk column cannot be excluded - it's always auto added even if not explicitly + included. You can also pass fields to include as dictionary or set. @@ -79,11 +134,45 @@ To include whole nested model specify model related field name and ellipsis. Below you can see examples that are equivalent: ```python ---8 < -- "../docs_src/queries/docs009.py" +# 1. like in example above +await Car.objects.select_related('manufacturer').fields(['id', 'name', 'manufacturer__name']).all() + +# 2. to mark a field as required use ellipsis +await Car.objects.select_related('manufacturer').fields({'id': ..., + 'name': ..., + 'manufacturer': { + 'name': ...} + }).all() + +# 3. to include whole nested model use ellipsis +await Car.objects.select_related('manufacturer').fields({'id': ..., + 'name': ..., + 'manufacturer': ... + }).all() + +# 4. to specify fields at last nesting level you can also use set - equivalent to 2. above +await Car.objects.select_related('manufacturer').fields({'id': ..., + 'name': ..., + 'manufacturer': {'name'} + }).all() + +# 5. of course set can have multiple fields +await Car.objects.select_related('manufacturer').fields({'id': ..., + 'name': ..., + 'manufacturer': {'name', 'founded'} + }).all() + +# 6. you can include all nested fields but it will be equivalent of 3. above which is shorter +await Car.objects.select_related('manufacturer').fields({'id': ..., + 'name': ..., + 'manufacturer': {'id', 'name', 'founded'} + }).all() + ``` -!!!note All methods that do not return the rows explicitly returns a QueySet instance so -you can chain them together +!!!note + All methods that do not return the rows explicitly returns a QueySet instance so + you can chain them together So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained. @@ -102,25 +191,125 @@ are available. Especially check above how you can pass also nested dictionaries and sets as a mask to exclude fields from whole hierarchy. -!!!note Note that `fields()` and `exclude_fields()` works both for main models (on -normal queries like `get`, `all` etc.) -as well as `select_related` and `prefetch_related` models (with nested notation). +!!!note + Note that `fields()` and `exclude_fields()` works both for main models (on + normal queries like `get`, `all` etc.) + as well as `select_related` and `prefetch_related` models (with nested notation). Below you can find few simple examples: ```python hl_lines="47 48 60 61 67" ---8<-- "../docs_src/queries/docs008.py" +import databases +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +database = databases.Database(DATABASE_URL, force_rollback=True) +metadata = sqlalchemy.MetaData() + + +class Company(ormar.Model): + class Meta: + tablename = "companies" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + founded: int = ormar.Integer(nullable=True) + + +class Car(ormar.Model): + class Meta: + tablename = "cars" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + manufacturer = ormar.ForeignKey(Company) + name: str = ormar.String(max_length=100) + year: int = ormar.Integer(nullable=True) + gearbox_type: str = ormar.String(max_length=20, nullable=True) + gears: int = ormar.Integer(nullable=True) + aircon_type: str = ormar.String(max_length=20, nullable=True) + + +# build some sample data +toyota = await Company.objects.create(name="Toyota", founded=1937) +await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5, + aircon_type='Manual') +await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5, + aircon_type='Manual') +await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6, + aircon_type='Auto') + +# select manufacturer but only name - to include related models use notation {model_name}__{column} +all_cars = await Car.objects.select_related('manufacturer').exclude_fields( + ['year', 'gearbox_type', 'gears', 'aircon_type', 'company__founded']).all() +for car in all_cars: + # excluded columns will yield None + assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type']) + # included column on related models will be available, pk column is always included + # even if you do not include it in fields list + assert car.manufacturer.name == 'Toyota' + # also in the nested related models - you cannot exclude pk - it's always auto added + assert car.manufacturer.founded is None + +# fields() can be called several times, building up the columns to select +# models selected in select_related but with no columns in fields list implies all fields +all_cars = await Car.objects.select_related('manufacturer').exclude_fields('year').exclude_fields( + ['gear', 'gearbox_type']).all() +# all fiels from company model are selected +assert all_cars[0].manufacturer.name == 'Toyota' +assert all_cars[0].manufacturer.founded == 1937 + +# cannot exclude mandatory model columns - company__name in this example - note usage of dict/set this time +await Car.objects.select_related('manufacturer').exclude_fields([{'company': {'name'}}]).all() +# will raise pydantic ValidationError as company.name is required + ``` -!!!warning Mandatory fields cannot be excluded as it will raise `ValidationError`, to -exclude a field it has to be nullable. +!!!warning + Mandatory fields cannot be excluded as it will raise `ValidationError`, to + exclude a field it has to be nullable. -!!!tip Pk column cannot be excluded - it's always auto added even if explicitly -excluded. +!!!tip + Pk column cannot be excluded - it's always auto added even if explicitly + excluded. -!!!note All methods that do not return the rows explicitly returns a QueySet instance so -you can chain them together +!!!note + All methods that do not return the rows explicitly returns a QueySet instance so + you can chain them together So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained. Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` + + +## QuerysetProxy methods + +When access directly the related `ManyToMany` field as well as `ReverseForeignKey` +returns the list of related models. + +But at the same time it exposes subset of QuerySet API, so you can filter, create, +select related etc related models directly from parent model. + +### fields + +Works exactly the same as [fields](./#fields) function above but allows you to select columns from related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + +### exclude_fields + +Works exactly the same as [exclude_fields](./#exclude_fields) function above but allows you to select columns from related +objects from other side of the relation. + +!!!tip + To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section + + +[querysetproxy]: ../relations/queryset-proxy.md \ No newline at end of file diff --git a/docs/relations/queryset-proxy.md b/docs/relations/queryset-proxy.md index 315dc68..9e033b6 100644 --- a/docs/relations/queryset-proxy.md +++ b/docs/relations/queryset-proxy.md @@ -30,7 +30,9 @@ But at the same time it exposes subset of QuerySet API, so you can filter, creat Note that value returned by `create` or created in `get_or_create` and `update_or_create` if model does not exist will be added to relation list (not clearing it). -## get +## Read data from database + +### get `get(**kwargs): -> Model` @@ -52,7 +54,16 @@ assert post.categories[0] == news !!!tip Read more in queries documentation [get][get] -## all +### get_or_create + +`get_or_create(**kwargs) -> Model` + +Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs. + +!!!tip + Read more in queries documentation [get_or_create][get_or_create] + +### all `all(**kwargs) -> List[Optional["Model"]]` @@ -73,7 +84,9 @@ assert news_posts[0].author == guido !!!tip Read more in queries documentation [all][all] -## create +## Insert/ update data into database + +### create `create(**kwargs): -> Model` @@ -91,113 +104,162 @@ assert len(await post.categories.all()) == 2 !!!tip Read more in queries documentation [create][create] - -## get_or_create +### get_or_create `get_or_create(**kwargs) -> Model` +Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs. + !!!tip Read more in queries documentation [get_or_create][get_or_create] -## update_or_create +### update_or_create `update_or_create(**kwargs) -> Model` +Updates the model, or in case there is no match in database creates a new one. + !!!tip Read more in queries documentation [update_or_create][update_or_create] -## filter +## Filtering and sorting + +### filter `filter(**kwargs) -> QuerySet` +Allows you to filter by any Model attribute/field as well as to fetch instances, with a filter across an FK relationship. + !!!tip Read more in queries documentation [filter][filter] -## exclude +### exclude `exclude(**kwargs) -> QuerySet` +Works exactly the same as filter and all modifiers (suffixes) are the same, but returns a not condition. + !!!tip Read more in queries documentation [exclude][exclude] -## select_related - -`select_related(related: Union[List, str]) -> QuerySet` - -!!!tip - Read more in queries documentation [select_related][select_related] - -## prefetch_related - -`prefetch_related(related: Union[List, str]) -> QuerySet` - -!!!tip - Read more in queries documentation [prefetch_related][prefetch_related] - -## limit - -`limit(limit_count: int) -> QuerySet` - -!!!tip - Read more in queries documentation [limit][limit] - -## offset - -`offset(offset: int) -> QuerySet` - -!!!tip - Read more in queries documentation [offset][offset] - -## count - -`count() -> int` - -!!!tip - Read more in queries documentation [count][count] - -## exists - -`exists() -> bool` - -!!!tip - Read more in queries documentation [exists][exists] - -## fields - -`fields(columns: Union[List, str, set, dict]) -> QuerySet` - -!!!tip - Read more in queries documentation [fields][fields] - -## exclude_fields - -`exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` - -!!!tip - Read more in queries documentation [exclude_fields][exclude_fields] - -## order_by +### order_by `order_by(columns:Union[List, str]) -> QuerySet` +With order_by() you can order the results from database based on your choice of fields. + !!!tip Read more in queries documentation [order_by][order_by] +## Joins and subqueries -[queries]: ../queries.md -[get]: ../queries.md#get -[all]: ../queries.md#all -[create]: ../queries.md#create -[get_or_create]: ../queries.md#get_or_create -[update_or_create]: ../queries.md#update_or_create -[filter]: ../queries.md#filter -[exclude]: ../queries.md#exclude -[select_related]: ../queries.md#select_related -[prefetch_related]: ../queries.md#prefetch_related -[limit]: ../queries.md#limit -[offset]: ../queries.md#offset -[count]: ../queries.md#count -[exists]: ../queries.md#exists -[fields]: ../queries.md#fields -[exclude_fields]: ../queries.md#exclude_fields -[order_by]: ../queries.md#order_by \ No newline at end of file +### select_related + +`select_related(related: Union[List, str]) -> QuerySet` + +Allows to prefetch related models during the same query. + +With select_related always only one query is run against the database, meaning that one (sometimes complicated) join is generated and later nested models are processed in python. + +!!!tip + Read more in queries documentation [select_related][select_related] + +### prefetch_related + +`prefetch_related(related: Union[List, str]) -> QuerySet` + +Allows to prefetch related models during query - but opposite to select_related each subsequent model is fetched in a separate database query. + +With prefetch_related always one query per Model is run against the database, meaning that you will have multiple queries executed one after another. + +!!!tip + Read more in queries documentation [prefetch_related][prefetch_related] + +## Pagination and rows number + +### paginate + +`paginate(page: int, page_size: int = 20) -> QuerySet` + +Combines the offset and limit methods based on page number and size. + +!!!tip + Read more in queries documentation [paginate][paginate] + +### limit + +`limit(limit_count: int) -> QuerySet` + +You can limit the results to desired number of parent models. + +!!!tip + Read more in queries documentation [limit][limit] + +### offset + +`offset(offset: int) -> QuerySet` + +You can offset the results by desired number of main models. + +!!!tip + Read more in queries documentation [offset][offset] + +## Selecting subset of columns + +### fields + +`fields(columns: Union[List, str, set, dict]) -> QuerySet` + +With fields() you can select subset of model columns to limit the data load. + +!!!tip + Read more in queries documentation [fields][fields] + +### exclude_fields + +`exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` + +With exclude_fields() you can select subset of model columns that will be excluded to limit the data load. + +!!!tip + Read more in queries documentation [exclude_fields][exclude_fields] + +## Aggregated functions + +### count + +`count() -> int` + +Returns number of rows matching the given criteria (i.e. applied with filter and exclude) + +!!!tip + Read more in queries documentation [count][count] + +### exists + +`exists() -> bool` + +Returns a bool value to confirm if there are rows matching the given criteria (applied with filter and exclude) + +!!!tip + Read more in queries documentation [exists][exists] + + +[queries]: ../queries/index.md +[get]: ../queries/read.md#get +[all]: ../queries/read.md#all +[create]: ../queries/create.md#create +[get_or_create]: ../queries/read.md#get_or_create +[update_or_create]: ../queries/update.md#update_or_create +[filter]: ../queries/filter-and-sort.md#filter +[exclude]: ../queries/filter-and-sort.md#exclude +[select_related]: ../queries/joins-and-subqueries.md#select_related +[prefetch_related]: ../queries/joins-and-subqueries.md#prefetch_related +[limit]: ../queries/pagination-and-rows-number.md#limit +[offset]: ../queries/pagination-and-rows-number.md#offset +[paginate]: ../queries/pagination-and-rows-number.md#paginate +[count]: ../queries/aggregations.md#count +[exists]: ../queries/aggregations.md#exists +[fields]: ../queries/select-columns.md#fields +[exclude_fields]: ../queries/select-columns.md#exclude_fields +[order_by]: ../queries/filter-and-sort.md#order_by \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 8a5aa6a..c6379f1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,9 +84,9 @@ nav: - Exceptions: api/exceptions.md repo_name: collerek/ormar repo_url: https://github.com/collerek/ormar -#google_analytics: -# - UA-72514911-3 -# - auto +google_analytics: + - UA-72514911-3 + - auto theme: name: material highlightjs: true diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index 3161a21..f76c8e9 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -1,3 +1,4 @@ +import sys import uuid from dataclasses import dataclass from typing import Any, List, Optional, TYPE_CHECKING, Tuple, Type, Union @@ -14,6 +15,11 @@ if TYPE_CHECKING: # pragma no cover from ormar.models import Model, NewBaseModel from ormar.fields import ManyToManyField + if sys.version_info < (3, 7): + ToType = Type["Model"] + else: + ToType = Union[Type["Model"], "ForwardRef"] + def create_dummy_instance(fk: Type["Model"], pk: Any = None) -> "Model": """ @@ -124,7 +130,7 @@ class ForeignKeyConstraint: def ForeignKey( # noqa CFQ002 - to: Union[Type["Model"], "ForwardRef"], + to: "ToType", *, name: str = None, unique: bool = False, diff --git a/ormar/fields/many_to_many.py b/ormar/fields/many_to_many.py index a55c04e..2228121 100644 --- a/ormar/fields/many_to_many.py +++ b/ormar/fields/many_to_many.py @@ -1,3 +1,4 @@ +import sys from typing import Any, List, Optional, TYPE_CHECKING, Tuple, Type, Union from pydantic.typing import ForwardRef, evaluate_forwardref @@ -8,6 +9,11 @@ from ormar.fields.foreign_key import ForeignKeyField if TYPE_CHECKING: # pragma no cover from ormar.models import Model + if sys.version_info < (3, 7): + ToType = Type["Model"] + else: + ToType = Union[Type["Model"], "ForwardRef"] + REF_PREFIX = "#/components/schemas/" @@ -36,8 +42,8 @@ def populate_m2m_params_based_on_to_model( def ManyToMany( - to: Union[Type["Model"], ForwardRef], - through: Union[Type["Model"], ForwardRef], + to: "ToType", + through: "ToType", *, name: str = None, unique: bool = False, diff --git a/tests/test_forward_cross_refs.py b/tests/test_forward_cross_refs.py index 9b813aa..79dbf72 100644 --- a/tests/test_forward_cross_refs.py +++ b/tests/test_forward_cross_refs.py @@ -60,55 +60,57 @@ def create_test_database(): @pytest.mark.asyncio async def test_double_relations(): - t1 = await Teacher.objects.create(name="Mr. Jones") - t2 = await Teacher.objects.create(name="Ms. Smith") - t3 = await Teacher.objects.create(name="Mr. Quibble") + async with db: + async with db.transaction(force_rollback=True): + t1 = await Teacher.objects.create(name="Mr. Jones") + t2 = await Teacher.objects.create(name="Ms. Smith") + t3 = await Teacher.objects.create(name="Mr. Quibble") - s1 = await Student.objects.create(name="Joe", primary_teacher=t1) - s2 = await Student.objects.create(name="Sam", primary_teacher=t1) - s3 = await Student.objects.create(name="Kate", primary_teacher=t2) - s4 = await Student.objects.create(name="Zoe", primary_teacher=t2) - s5 = await Student.objects.create(name="John", primary_teacher=t3) - s6 = await Student.objects.create(name="Anna", primary_teacher=t3) + s1 = await Student.objects.create(name="Joe", primary_teacher=t1) + s2 = await Student.objects.create(name="Sam", primary_teacher=t1) + s3 = await Student.objects.create(name="Kate", primary_teacher=t2) + s4 = await Student.objects.create(name="Zoe", primary_teacher=t2) + s5 = await Student.objects.create(name="John", primary_teacher=t3) + s6 = await Student.objects.create(name="Anna", primary_teacher=t3) - for t in [t1, t2, t3]: - for s in [s1, s2, s3, s4, s5, s6]: - await t.students.add(s) + for t in [t1, t2, t3]: + for s in [s1, s2, s3, s4, s5, s6]: + await t.students.add(s) - jones = ( - await Teacher.objects.select_related(["students", "own_students"]) - .order_by(["students__name", "own_students__name"]) - .get(name="Mr. Jones") - ) - assert len(jones.students) == 6 - assert jones.students[0].name == "Anna" - assert jones.students[5].name == "Zoe" - assert len(jones.own_students) == 2 - assert jones.own_students[0].name == "Joe" - assert jones.own_students[1].name == "Sam" + jones = ( + await Teacher.objects.select_related(["students", "own_students"]) + .order_by(["students__name", "own_students__name"]) + .get(name="Mr. Jones") + ) + assert len(jones.students) == 6 + assert jones.students[0].name == "Anna" + assert jones.students[5].name == "Zoe" + assert len(jones.own_students) == 2 + assert jones.own_students[0].name == "Joe" + assert jones.own_students[1].name == "Sam" - smith = ( - await Teacher.objects.select_related(["students", "own_students"]) - .filter(students__name__contains="a") - .order_by(["students__name", "own_students__name"]) - .get(name="Ms. Smith") - ) - assert len(smith.students) == 3 - assert smith.students[0].name == "Anna" - assert smith.students[2].name == "Sam" - assert len(smith.own_students) == 2 - assert smith.own_students[0].name == "Kate" - assert smith.own_students[1].name == "Zoe" + smith = ( + await Teacher.objects.select_related(["students", "own_students"]) + .filter(students__name__contains="a") + .order_by(["students__name", "own_students__name"]) + .get(name="Ms. Smith") + ) + assert len(smith.students) == 3 + assert smith.students[0].name == "Anna" + assert smith.students[2].name == "Sam" + assert len(smith.own_students) == 2 + assert smith.own_students[0].name == "Kate" + assert smith.own_students[1].name == "Zoe" - quibble = ( - await Teacher.objects.select_related(["students", "own_students"]) - .filter(students__name__startswith="J") - .order_by(["-students__name", "own_students__name"]) - .get(name="Mr. Quibble") - ) - assert len(quibble.students) == 2 - assert quibble.students[1].name == "Joe" - assert quibble.students[0].name == "John" - assert len(quibble.own_students) == 2 - assert quibble.own_students[1].name == "John" - assert quibble.own_students[0].name == "Anna" + quibble = ( + await Teacher.objects.select_related(["students", "own_students"]) + .filter(students__name__startswith="J") + .order_by(["-students__name", "own_students__name"]) + .get(name="Mr. Quibble") + ) + assert len(quibble.students) == 2 + assert quibble.students[1].name == "Joe" + assert quibble.students[0].name == "John" + assert len(quibble.own_students) == 2 + assert quibble.own_students[1].name == "John" + assert quibble.own_students[0].name == "Anna"