update docs part 2

This commit is contained in:
collerek
2020-10-07 17:43:03 +02:00
parent ba0990d05b
commit 717feb2c74
18 changed files with 688 additions and 396 deletions

View File

@ -1,7 +1,11 @@
# Fields # Fields
There are 11 basic model field types and a special `ForeignKey` field to establish relationships between models. There are 12 basic model field types and a special `ForeignKey` and `Many2Many` fields to establish relationships between models.
!!!tip
For explanation of `ForeignKey` and `Many2Many` fields check [relations][relations].
Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model. Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model.
@ -22,11 +26,11 @@ Used in sql only.
`autoincrement`: `bool` = `primary_key and type == int` -> defaults to True if column is a primary key and of type Integer, otherwise False. `autoincrement`: `bool` = `primary_key and type == int` -> defaults to True if column is a primary key and of type Integer, otherwise False.
Can be only used with int fields. Can be only used with int/bigint fields.
If a field has autoincrement it becomes optional. If a field has autoincrement it becomes optional.
Used only in sql. Used both in sql and pydantic (changes pk field to optional for autoincrement).
### nullable ### nullable
@ -37,13 +41,8 @@ Specifies if field is optional or required, used both with sql and pydantic.
!!!note !!!note
By default all `ForeignKeys` are also nullable, meaning the related `Model` is not required. By default all `ForeignKeys` are also nullable, meaning the related `Model` is not required.
If you change the `ForeignKey` column to `nullable`, it not only becomes required, it changes also the way in which data is loaded in queries. If you change the `ForeignKey` column to `nullable=False`, it becomes required.
If you select `Model` without explicitly adding related `Model` assigned by not nullable `ForeignKey`, the `Model` is still gona be appended automatically, see example below.
```Python hl_lines="24 32 33 34 35 37 38 39 40 41"
--8<-- "../docs_src/fields/docs003.py"
```
!!!info !!!info
If you want to know more about how you can preload related models during queries and how the relations work read the [queries][queries] and [relations][relations] sections. If you want to know more about how you can preload related models during queries and how the relations work read the [queries][queries] and [relations][relations] sections.
@ -93,22 +92,56 @@ Sets the unique constraint on a table's column.
Used in sql only. Used in sql only.
### pydantic_only
`pydantic_only`: `bool` = `False`
Prevents creation of a sql column for given field.
Used for data related to given model but not to be stored in the database.
Used in pydantic only.
### choices
`choices`: `Sequence` = `[]`
A set of choices allowed to be used for given field.
Used for data validation on pydantic side.
Prevents insertion of value not present in the choices list.
Used in pydantic only.
## Fields Types ## Fields Types
### String ### String
`String(length)` has a required `length` parameter. `String(max_length,
allow_blank: bool = True,
strip_whitespace: bool = False,
min_length: int = None,
max_length: int = None,
curtail_length: int = None,
regex: str = None,)` has a required `max_length` parameter.
* Sqlalchemy column: `sqlalchemy.String` * Sqlalchemy column: `sqlalchemy.String`
* Type (used for pydantic): `str` * Type (used for pydantic): `str`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Text ### Text
`Text()` has no required parameters. `Text(allow_blank: bool = True, strip_whitespace: bool = False)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Text` * Sqlalchemy column: `sqlalchemy.Text`
* Type (used for pydantic): `str` * Type (used for pydantic): `str`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Boolean ### Boolean
`Boolean()` has no required parameters. `Boolean()` has no required parameters.
@ -118,32 +151,58 @@ Used in sql only.
### Integer ### Integer
`Integer()` has no required parameters. `Integer(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Integer` * Sqlalchemy column: `sqlalchemy.Integer`
* Type (used for pydantic): `int` * Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### BigInteger ### BigInteger
`BigInteger()` has no required parameters. `BigInteger(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.BigInteger` * Sqlalchemy column: `sqlalchemy.BigInteger`
* Type (used for pydantic): `int` * Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Float ### Float
`Float()` has no required parameters. `Float(minimum: float = None,
maximum: float = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Float` * Sqlalchemy column: `sqlalchemy.Float`
* Type (used for pydantic): `float` * Type (used for pydantic): `float`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Decimal ### Decimal
`Decimal(lenght, precision)` has required `length` and `precision` parameters. `Decimal(minimum: float = None,
maximum: float = None,
multiple_of: int = None,
precision: int = None,
scale: int = None,
max_digits: int = None,
decimal_places: int = None)` has no required parameters
You can use either `length` and `precision` parameters or `max_digits` and `decimal_places`.
* Sqlalchemy column: `sqlalchemy.DECIMAL` * Sqlalchemy column: `sqlalchemy.DECIMAL`
* Type (used for pydantic): `decimal.Decimal` * Type (used for pydantic): `decimal.Decimal`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Date ### Date
`Date()` has no required parameters. `Date()` has no required parameters.
@ -172,35 +231,13 @@ Used in sql only.
* Sqlalchemy column: `sqlalchemy.JSON` * Sqlalchemy column: `sqlalchemy.JSON`
* Type (used for pydantic): `pydantic.Json` * Type (used for pydantic): `pydantic.Json`
### ForeignKey ### UUID
`ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class. `UUID()` has no required parameters.
Sqlalchemy column and Type are automatically taken from target `Model`. * Sqlalchemy column: `ormar.UUID` based on `sqlalchemy.CHAR` field
* Type (used for pydantic): `uuid.UUID`
* Sqlalchemy column: class of a target `Model` primary key column
* Type (used for pydantic): type of a target `Model` primary key column
`ForeignKey` fields are automatically registering reverse side of the relation.
By default it's child (source) `Model` name + s, like courses in snippet below:
```Python hl_lines="25 31"
--8<-- "../docs_src/fields/docs001.py"
```
But you can overwrite this name by providing `related_name` parameter like below:
```Python hl_lines="25 30"
--8<-- "../docs_src/fields/docs002.py"
```
!!!tip
Since related models are coming from Relationship Manager the reverse relation on access returns list of `wekref.proxy` to avoid circular references.
!!!info
All relations are stored in lists, but when you access parent `Model` the ormar is unpacking the value for you.
Read more in [relations][relations].
[relations]: ./relations.md [relations]: ./relations.md
[queries]: ./queries.md [queries]: ./queries.md
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types

0
docs/install.md Normal file
View File

View File

@ -82,6 +82,21 @@ You can overwrite this parameter by providing `Meta` class `tablename` argument.
--8<-- "../docs_src/models/docs002.py" --8<-- "../docs_src/models/docs002.py"
``` ```
### Constraints
On a model level you can also set model-wise constraints on sql columns.
Right now only `UniqueColumns` constraint is present.
!!!tip
To read more about columns constraints like `primary_key`, `unique`, `ForeignKey` etc. visit [fields][fields].
You can set this parameter by providing `Meta` class `constraints` argument.
```Python hl_lines="14-17"
--8<-- "../docs_src/models/docs006.py"
```
## Initialization ## Initialization
There are two ways to create and persist the `Model` instance in the database. There are two ways to create and persist the `Model` instance in the database.
@ -97,6 +112,8 @@ If you plan to modify the instance in the later execution of your program you ca
If you want to initiate your `Model` and at the same time save in in the database use a QuerySet's method `create()`. If you want to initiate your `Model` and at the same time save in in the database use a QuerySet's method `create()`.
For creating multiple objects at once a `bulk_create()` QuerySet's method is available.
Each model has a `QuerySet` initialised as `objects` parameter Each model has a `QuerySet` initialised as `objects` parameter
```Python hl_lines="23" ```Python hl_lines="23"
@ -104,7 +121,31 @@ Each model has a `QuerySet` initialised as `objects` parameter
``` ```
!!!info !!!info
To read more about `QuerySets` and available methods visit [queries][queries] To read more about `QuerySets` (including bulk operations) and available methods visit [queries][queries]
## `Model` methods
### load
By default when you query a table without prefetching related models, the ormar will still construct
your related models, but populate them only with the pk value.
```python
track = await Track.objects.get(name='The Bird')
track.album.pk # will return malibu album pk (1)
track.album.name # will return None
# you need to actually load the data first
await track.album.load()
track.album.name # will return 'Malibu'
```
### save
### delete
### update
## Internals ## Internals
@ -114,7 +155,7 @@ Apart from special parameters defined in the `Model` during definition (tablenam
All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models. All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models.
For example to list model fields you can: For example to list pydantic model fields you can:
```Python hl_lines="20" ```Python hl_lines="20"
--8<-- "../docs_src/models/docs003.py" --8<-- "../docs_src/models/docs003.py"
@ -137,7 +178,7 @@ For example to list table columns you can:
``` ```
!!!tip !!!tip
You can access table primary key name by `Course.__pkname__` You can access table primary key name by `Course.Meta.pkname`
!!!info !!!info
For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation. For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation.

View File

@ -4,17 +4,24 @@
Each Model is auto registered with a QuerySet that represents the underlaying query and it's options. Each Model is auto registered with a QuerySet that represents the underlaying query and it's options.
Most of the methods are also available through many to many relation interface.
Given the Models like this Given the Models like this
```Python ```Python
--8<-- "../docs_src/relations/docs001.py" --8<-- "../docs_src/queries/docs001.py"
``` ```
we can demonstrate available methods to fetch and save the data into the database. we can demonstrate available methods to fetch and save the data into the database.
### create(**kwargs)
Creates the model instance, saves it in a database and returns the updates model (with pk populated). ### create
`create(**kwargs): -> Model`
Creates the model instance, saves it in a database and returns the updates model
(with pk populated if not passed and autoincrement is set).
The allowed kwargs are `Model` fields names and proper value types. The allowed kwargs are `Model` fields names and proper value types.
```python ```python
@ -28,22 +35,12 @@ malibu = Album(name="Malibu")
await malibu.save() await malibu.save()
``` ```
### load() !!!tip
Check other `Model` methods in [models][models]
By default when you query a table without prefetching related models, the ormar will still construct ### get
your related models, but populate them only with the pk value.
```python `get(**kwargs): -> Model`
track = await Track.objects.get(name='The Bird')
track.album.pk # will return malibu album pk (1)
track.album.name # will return None
# you need to actually load the data first
await track.album.load()
track.album.name # will return 'Malibu'
```
### get(**kwargs)
Get's the first row from the db meeting the criteria set by kwargs. Get's the first row from the db meeting the criteria set by kwargs.
@ -53,11 +50,193 @@ Passing a criteria is actually calling filter(**kwargs) method described below.
```python ```python
track = await Track.objects.get(name='The Bird') 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() track2 = track = await Track.objects.get()
track == track2 # True since it's the only row in db track == track2 # True since it's the only row in db in our example
``` ```
### all() !!!warning
If no row meets the criteria `NoMatch` exception is raised.
If there are multiple rows meeting the criteria the `MultipleMatches` exception is raised.
### get_or_create
`get_or_create(**kwargs) -> Model`
Combination of create and get methods.
Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates a new one with given kwargs.
```python
album = await Album.objects.get_or_create(name='The Cat')
# object is created as it does not exist
album2 = await Album.objects.get_or_create(name='The Cat')
assert album == album2
# return True as the same db row is returned
```
!!!warning
Despite being a equivalent row from database the `album` and `album2` in example above are 2 different python objects!
Updating one of them will not refresh the second one until you excplicitly load() the fresh data from db.
!!!note
Note that if you want to create a new object you either have to pass pk column value or pk column has to be set as autoincrement
### update
`update(each: bool = False, **kwargs) -> int`
QuerySet level update is used to update multiple records with the same value at once.
You either have to filter the QuerySet first or provide a `each=True` flag to update whole table.
If you do not provide this flag or a filter a `QueryDefinitionError` will be raised.
Return number of rows updated.
```python hl_lines="24-28"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# queryset needs to be filtered before deleting to prevent accidental overwrite
# to update whole database table each=True needs to be provided as a safety switch
await Book.objects.update(each=True, genre='Fiction')
all_books = await Book.objects.filter(genre='Fiction').all()
assert len(all_books) == 3
```
### update_or_create
`update_or_create(**kwargs) -> Model`
### bulk_create
`bulk_create(objects: List["Model"]) -> None`
Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
```python hl_lines="20-26"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class ToDo(ormar.Model):
class Meta:
tablename = "todos"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
text: ormar.String(max_length=500)
completed: ormar.Boolean(default=False)
# create multiple instances at once with bulk_create
await ToDo.objects.bulk_create(
[
ToDo(text="Buy the groceries."),
ToDo(text="Call Mum.", completed=True),
ToDo(text="Send invoices.", completed=True),
]
)
todoes = await ToDo.objects.all()
assert len(todoes) == 3
```
### bulk_update
`bulk_update(objects: List["Model"], columns: List[str] = None) -> None`
Allows to update multiple instance at once.
All `Models` passed need to have primary key column populated.
You can also select which fields to update by passing `columns` list as a list of string names.
```python hl_lines="8"
# continuing the example from bulk_create
# update objects
for todo in todoes:
todo.completed = False
# perform update of all objects at once
# objects need to have pk column set, otherwise exception is raised
await ToDo.objects.bulk_update(todoes)
completed = await ToDo.objects.filter(completed=False).all()
assert len(completed) == 3
```
### delete
`delete(each: bool = False, **kwargs) -> int`
QuerySet level delete is used to delete multiple records at once.
You either have to filter the QuerySet first or provide a `each=True` flag to delete whole table.
If you do not provide this flag or a filter a `QueryDefinitionError` will be raised.
Return number of rows deleted.
```python hl_lines="23-27"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace in Space', author="Tolstoy, Leo", genre='Fantasy')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# delete accepts kwargs that will be used in filter
# acting in same way as queryset.filter(**kwargs).delete()
await Book.objects.delete(genre='Fantasy') # delete all fantasy books
all_books = await Book.objects.all()
assert len(all_books) == 2
```
### all
Returns all rows from a database for given model Returns all rows from a database for given model
@ -66,7 +245,7 @@ tracks = await Track.objects.select_related("album").all()
# will return a list of all Tracks # will return a list of all Tracks
``` ```
### filter(**kwargs) ### filter
Allows you to filter by any `Model` attribute/field Allows you to filter by any `Model` attribute/field
as well as to fetch instances, with a filter across an FK relationship. as well as to fetch instances, with a filter across an FK relationship.
@ -96,7 +275,9 @@ You can use special filter suffix to change the filter operands:
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### select_related(*args) ### exclude
### select_related
Allows to prefetch related models. Allows to prefetch related models.
@ -127,7 +308,7 @@ classes = await SchoolClass.objects.select_related(
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### limit(int) ### limit
You can limit the results to desired number of rows. You can limit the results to desired number of rows.
@ -141,7 +322,7 @@ tracks = await Track.objects.limit(1).all()
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
### offset(int) ### offset
You can also offset the results by desired number of rows. You can also offset the results by desired number of rows.
@ -150,7 +331,18 @@ tracks = await Track.objects.offset(1).limit(1).all()
# will return just one Track, but this time the second one # will return just one Track, but this time the second one
``` ```
### count
### exists
### fields
!!!note !!!note
`filter()`, `select_related()`, `limit()` and `offset()` returns a QueySet instance so you can chain them together. `filter()`, `select_related()`, `limit()` and `offset()` returns a QueySet instance so you can chain them together.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
[models]: ./models.md

View File

@ -2,205 +2,169 @@
## Defining a relationship ## Defining a relationship
### Foreign Key ### ForeignKey
To define a relationship you simply need to create a ForeignKey field on one `Model` and point it to another `Model`. `ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class.
```Python hl_lines="24" Sqlalchemy column and Type are automatically taken from target `Model`.
--8<-- "../docs_src/relations/docs001.py"
* Sqlalchemy column: class of a target `Model` primary key column
* Type (used for pydantic): type of a target `Model`
#### Defining Models
To define a relation add `ForeignKey` field that points to related `Model`.
```Python hl_lines="27"
--8<-- "../docs_src/fields/docs003.py"
``` ```
It automatically creates an sql foreign key constraint on a underlying table as well as nested pydantic model in the definition. #### Reverse Relation
`ForeignKey` fields are automatically registering reverse side of the relation.
```Python hl_lines="29 33" By default it's child (source) `Model` name + s, like courses in snippet below:
--8<-- "../docs_src/relations/docs002.py"
```
Of course it's handled for you so you don't have to delve deep into this but you can. ```Python hl_lines="27 33"
!!!tip
Note how by default the relation is optional, you can require the related `Model` by setting `nullable=False` on the `ForeignKey` field.
### Reverse Relation
At the same time the reverse relationship is registered automatically on parent model (target of `ForeignKey`).
By default it's child (source) `Model` name + 's', like courses in snippet below:
```Python hl_lines="25 31"
--8<-- "../docs_src/fields/docs001.py" --8<-- "../docs_src/fields/docs001.py"
``` ```
#### related_name
But you can overwrite this name by providing `related_name` parameter like below: But you can overwrite this name by providing `related_name` parameter like below:
```Python hl_lines="25 30" ```Python hl_lines="27 33"
--8<-- "../docs_src/fields/docs002.py" --8<-- "../docs_src/fields/docs002.py"
``` ```
!!!tip !!!tip
Since related models are coming from Relationship Manager the reverse relation on access returns list of `wekref.proxy` to avoid circular references. The reverse relation on access returns list of `wekref.proxy` to avoid circular references.
## Relationship Manager
!!!tip ### Relation Setup
This section is more technical so you might want to skip it if you are not interested in implementation details.
### Need for a manager? You have several ways to set-up a relationship connection.
Since orm uses Sqlalchemy core under the hood to prepare the queries, #### `Model` instance
the orm needs a way to uniquely identify each relationship between the tables to construct working queries.
Imagine that you have models as following: The most obvious one is to pass a related `Model` instance to the constructor.
```Python hl_lines="32-33"
--8<-- "../docs_src/relations/docs001.py"
```
#### Primary key value
You can setup the relation also with just the pk column value of the related model.
```Python hl_lines="35-36"
--8<-- "../docs_src/relations/docs001.py"
```
#### Dictionary
Next option is with a dictionary of key-values of the related model.
You can build the dictionary yourself or get it from existing model with `dict()` method.
```Python hl_lines="38-39"
--8<-- "../docs_src/relations/docs001.py"
```
#### None
Finally you can explicitly set it to None (default behavior if no value passed).
```Python hl_lines="41-42"
--8<-- "../docs_src/relations/docs001.py"
```
!!!warning
In all not None cases the primary key value for related model **has to exist in database**.
Otherwise an IntegrityError will be raised by your database driver library.
### Many2Many
`Many2Many(to, through)` has required parameters `to` and `through` that takes target and relation `Model` classes.
Sqlalchemy column and Type are automatically taken from target `Model`.
* Sqlalchemy column: class of a target `Model` primary key column
* Type (used for pydantic): type of a target `Model`
####Defining `Models`:
```Python ```Python
--8<-- "../docs_src/relations/docs003.py" --8<-- "../docs_src/relations/docs002.py"
``` ```
Now imagine that you want to go from school class to student and his category and to teacher and his category. Create sample data:
```Python ```Python
classes = await SchoolClass.objects.select_related( guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
["teachers__category", "students__category"]).all() post = await Post.objects.create(title="Hello, M2M", author=guido)
news = await Category.objects.create(name="News")
``` ```
!!!tip #### Adding related models
To query a chain of models use double underscores between the relation names (`ForeignKeys` or reverse `ForeignKeys`)
!!!note
To select related models use `select_related` method from `Model` `QuerySet`.
Note that you use relation (`ForeignKey`) names and not the table names.
Since you join two times to the same table (categories) it won't work by default -> you would need to use aliases for category tables and columns.
But don't worry - ormar can handle situations like this, as it uses the Relationship Manager which has it's aliases defined for all relationships.
Each class is registered with the same instance of the AliasManager that you can access like this:
```python ```python
SchoolClass.alias_manager # Add a category to a post.
await post.categories.add(news)
# or from the other end:
await news.posts.add(post)
``` ```
It's the same object for all `Models` !!!warning
In all not None cases the primary key value for related model **has to exist in database**.
Otherwise an IntegrityError will be raised by your database driver library.
#### Creating new related `Model` instances
```python ```python
print(Teacher.alias_manager == Student.alias_manager) # Creating columns object from instance:
# will produce: True await post.categories.create(name="Tips")
``` assert len(await post.categories.all()) == 2
# newly created instance already have relation persisted in the database
### Table aliases
You can even preview the alias used for any relation by passing two tables names.
```python
print(Teacher.alias_manager.resolve_relation_join(
'students', 'categories'))
# will produce: KId1c6 (sample value)
print(Teacher.alias_manager.resolve_relation_join(
'categories', 'students'))
# will produce: EFccd5 (sample value)
``` ```
!!!note !!!note
The order that you pass the names matters -> as those are 2 different relationships depending on join order. Note that when accessing QuerySet API methods through Many2Many relation you don't
need to use objects attribute like in normal queries.
As aliases are produced randomly you can be presented with different results. To learn more about available QuerySet methods visit [queries][queries]
### Query automatic construction #### Removing related models
```python
Ormar is using those aliases during queries to both construct a meaningful and valid sql, # Removal of the relationship by one
as well as later use it to extract proper columns for proper nested models. await news.posts.remove(post)
# or all at once
Running a previously mentioned query to select school classes and related teachers and students: await news.posts.clear()
```Python
classes = await SchoolClass.objects.select_related(
["teachers__category", "students__category"]).all()
``` ```
Will result in a query like this (run under the hood): #### All other queryset methods
```sql When access directly the related `Many2Many` field returns the list of related models.
SELECT schoolclasses.id,
schoolclasses.name,
schoolclasses.department,
NZc8e2_students.id as NZc8e2_id,
NZc8e2_students.name as NZc8e2_name,
NZc8e2_students.schoolclass as NZc8e2_schoolclass,
NZc8e2_students.category as NZc8e2_category,
MYfe53_categories.id as MYfe53_id,
MYfe53_categories.name as MYfe53_name,
WA49a3_teachers.id as WA49a3_id,
WA49a3_teachers.name as WA49a3_name,
WA49a3_teachers.schoolclass as WA49a3_schoolclass,
WA49a3_teachers.category as WA49a3_category,
WZa13b_categories.id as WZa13b_id,
WZa13b_categories.name as WZa13b_name
FROM schoolclasses
LEFT OUTER JOIN students NZc8e2_students ON NZc8e2_students.schoolclass = schoolclasses.id
LEFT OUTER JOIN categories MYfe53_categories ON MYfe53_categories.id = NZc8e2_students.category
LEFT OUTER JOIN teachers WA49a3_teachers ON WA49a3_teachers.schoolclass = schoolclasses.id
LEFT OUTER JOIN categories WZa13b_categories ON WZa13b_categories.id = WA49a3_teachers.category
ORDER BY schoolclasses.id, NZc8e2_students.id, MYfe53_categories.id, WA49a3_teachers.id, WZa13b_categories.id
```
!!!note But at the same time it exposes full QuerySet API, so you can filter, create, select related etc.
As mentioned before the aliases are produced dynamically so the actual result might differ.
Note that aliases are assigned to relations and not the tables, therefore the first table is always without an alias.
### Returning related Models
Each object in Relationship Manager is identified by orm_id which you can preview like this
```python ```python
category = Category(name='Math') # Many to many relation exposes a list of columns models
print(category._orm_id) # and an API of the Queryset:
# will produce: c76046d9410c4582a656bf12a44c892c (sample value) assert news == await post.categories.get(name="News")
```
Each call to related `Model` is actually coming through the Manager which stores all # with all Queryset methods - filtering, selecting columns, counting etc.
the relations in a dictionary and returns related `Models` by relation type (name) and by object _orm_id. await news.posts.filter(title__contains="M2M").all()
await Category.objects.filter(posts__author=guido).get()
Since we register both sides of the relation the side registering the relation # columns models of many to many relation can be prefetched
is always registering the other side as concrete model, news_posts = await news.posts.select_related("author").all()
while the reverse relation is a weakref.proxy to avoid circular references. assert news_posts[0].author == guido
Sounds complicated but in reality it means something like this:
```python
test_class = await SchoolClass.objects.create(name='Test')
student = await Student.objects.create(name='John', schoolclass=test_class)
# the relation to schoolsclass from student (i.e. when you call student.schoolclass)
# is a concrete one, meaning directy relating the schoolclass `Model` object
# On the other side calling test_class.students will result in a list of wekref.proxy objects
``` ```
!!!tip !!!tip
To learn more about queries and available methods please review [queries][queries] section. To learn more about available QuerySet methods visit [queries][queries]
All relations are kept in lists, meaning that when you access related object the Relationship Manager is
searching itself for related models and get a list of them.
But since child to parent relation is a many to one type,
the Manager is unpacking the first (and only) related model from a list and you get an actual `Model` instance instead of a list.
Coming from parent to child relation (one to many) you always get a list of results.
Translating this into concrete sample, the same as above:
```python
test_class = await SchoolClass.objects.create(name='Test')
student = await Student.objects.create(name='John', schoolclass=test_class)
student.schoolclass # return a test_class instance extracted from relationship list
test_class.students # return a list of related wekref.proxy refering related students `Models`
```
!!!tip
You can preview all relations currently registered by accessing Relationship Manager on any class/instance `Student._orm_relationship_manager._relations`
[queries]: ./queries.md [queries]: ./queries.md

0
docs/testing.md Normal file
View File

View File

@ -8,21 +8,23 @@ metadata = sqlalchemy.MetaData()
class Department(ormar.Model): class Department(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)
department = ormar.ForeignKey(Department) department: ormar.ForeignKey(Department)
department = Department(name='Science') department = Department(name='Science')

View File

@ -8,21 +8,24 @@ metadata = sqlalchemy.MetaData()
class Department(ormar.Model): class Department(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
completed: ormar.Boolean(default=False)
department: ormar.ForeignKey(Department, related_name="my_courses")
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
completed = ormar.Boolean(default=False)
department = ormar.ForeignKey(Department, related_name="my_courses")
department = Department(name='Science') department = Department(name='Science')
course = Course(name='Math', completed=False, department=department) course = Course(name='Math', completed=False, department=department)
@ -33,4 +36,3 @@ print(department.my_courses[0])
# name='Math', # name='Math',
# completed=False, # completed=False,
# department=Department(id=None, name='Science')) # department=Department(id=None, name='Science'))

View File

@ -1,41 +1,27 @@
import ormar
import databases import databases
import sqlalchemy import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite") database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData() metadata = sqlalchemy.MetaData()
class Album(ormar.Model): class Department(ormar.Model):
__tablename__ = "album" class Meta:
__metadata__ = metadata database = database
__database__ = database metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
class Track(ormar.Model): class Course(ormar.Model):
__tablename__ = "track" class Meta:
__metadata__ = metadata database = database
__database__ = database metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
album = ormar.ForeignKey(Album, nullable=False) name: ormar.String(max_length=100)
title = ormar.String(length=100) completed: ormar.Boolean(default=False)
position = ormar.Integer() department: ormar.ForeignKey(Department)
album = await Album.objects.create(name="Brooklyn")
await Track.objects.create(album=album, title="The Bird", position=1)
# explicit preload of columns Album Model
track = await Track.objects.select_related("album").get(title="The Bird")
assert track.album.name == 'Brooklyn'
# Will produce: True
# even without explicit select_related if ForeignKey is not nullable,
# the Album Model is still preloaded.
track2 = await Track.objects.get(title="The Bird")
assert track2.album.name == 'Brooklyn'
# Will produce: True

View File

@ -0,0 +1,21 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
class Meta:
database = database
metadata = metadata
# define your constraints in Meta class of the model
# it's a list that can contain multiple constraints
# hera a combination of name and column will have to be unique in db
constraints = [ormar.UniqueColumns('name', 'completed')]
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False)

View File

@ -0,0 +1,28 @@
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Album(ormar.Model):
class Meta:
tablename = "album"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
class Track(ormar.Model):
class Meta:
tablename = "track"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album)
title: ormar.String(max_length=100)
position: ormar.Integer()

View File

@ -1,26 +1,42 @@
import ormar
import databases import databases
import sqlalchemy import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite") database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData() metadata = sqlalchemy.MetaData()
class Album(ormar.Model): class Department(ormar.Model):
__tablename__ = "album" class Meta:
__metadata__ = metadata database = database
__database__ = database metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
class Track(ormar.Model): class Course(ormar.Model):
__tablename__ = "track" class Meta:
__metadata__ = metadata database = database
__database__ = database metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
album = ormar.ForeignKey(Album) name: ormar.String(max_length=100)
title = ormar.String(length=100) completed: ormar.Boolean(default=False)
position = ormar.Integer() department: ormar.ForeignKey(Department)
department = Department(name='Science')
# set up a relation with actual Model instance
course = Course(name='Math', completed=False, department=department)
# set up relation with only related model pk value
course2 = Course(name='Math II', completed=False, department=department.pk)
# set up a relation with dictionary corresponding to related model
course3 = Course(name='Math III', completed=False, department=department.dict())
# explicitly set up None
course4 = Course(name='Math III', completed=False, department=None)

View File

@ -1,39 +1,48 @@
import ormar
import databases import databases
import ormar
import sqlalchemy import sqlalchemy
database = databases.Database("sqlite:///db.sqlite") database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData() metadata = sqlalchemy.MetaData()
class Album(ormar.Model): class Author(ormar.Model):
__tablename__ = "album" class Meta:
__metadata__ = metadata tablename = "authors"
__database__ = database database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) first_name: ormar.String(max_length=80)
last_name: ormar.String(max_length=80)
class Track(ormar.Model): class Category(ormar.Model):
__tablename__ = "track" class Meta:
__metadata__ = metadata tablename = "categories"
__database__ = database database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
album = ormar.ForeignKey(Album) name: ormar.String(max_length=40)
title = ormar.String(length=100)
position = ormar.Integer()
print(Track.__table__.columns['album'].__repr__()) class PostCategory(ormar.Model):
# Will produce: class Meta:
# Column('album', Integer(), ForeignKey('album.id'), table=<track>) tablename = "posts_categories"
database = database
metadata = metadata
print(Track.__pydantic_model__.__fields__['album']) # If there are no additional columns id will be created automatically as Integer
# Will produce:
# ModelField(
# name='album' class Post(ormar.Model):
# type=Optional[Album] class Meta:
# required=False tablename = "posts"
# default=None) database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
categories: ormar.ManyToMany(Category, through=PostCategory)
author: ormar.ForeignKey(Author)

View File

@ -1,44 +0,0 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class SchoolClass(ormar.Model):
__tablename__ = "schoolclasses"
__metadata__ = metadata
__database__ = database
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
class Category(ormar.Model):
__tablename__ = "categories"
__metadata__ = metadata
__database__ = database
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
class Student(ormar.Model):
__metadata__ = metadata
__database__ = database
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
schoolclass = ormar.ForeignKey(SchoolClass)
category = ormar.ForeignKey(Category)
class Teacher(ormar.Model):
__metadata__ = metadata
__database__ = database
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
schoolclass = ormar.ForeignKey(SchoolClass)
category = ormar.ForeignKey(Category)

View File

@ -1,12 +1,17 @@
site_name: Async ORM site_name: ormar
site_description: An simple async ORM with fastapi in mind and pydantic validation.
nav: nav:
- Home: index.md - Overview: index.md
- Installation: install.md
- Models: models.md - Models: models.md
- Fields: fields.md - Fields: fields.md
- Relations: relations.md - Relations: relations.md
- Queries: queries.md - Queries: queries.md
- Pydantic models: pydantic.md
- Use with Fastapi: fastapi.md - Use with Fastapi: fastapi.md
- Testing: testing.md
- Contributing: contributing.md
repo_name: collerek/ormar
repo_url: https://github.com/collerek/ormar
theme: theme:
name: material name: material
highlightjs: true highlightjs: true

View File

@ -63,7 +63,7 @@ class String(ModelFieldFactory):
def __new__( # type: ignore # noqa CFQ002 def __new__( # type: ignore # noqa CFQ002
cls, cls,
*, *,
allow_blank: bool = False, allow_blank: bool = True,
strip_whitespace: bool = False, strip_whitespace: bool = False,
min_length: int = None, min_length: int = None,
max_length: int = None, max_length: int = None,
@ -79,6 +79,7 @@ class String(ModelFieldFactory):
if k not in ["cls", "__class__", "kwargs"] if k not in ["cls", "__class__", "kwargs"]
}, },
} }
kwargs['nullable'] = kwargs['allow_blank']
return super().__new__(cls, **kwargs) return super().__new__(cls, **kwargs)
@classmethod @classmethod
@ -134,7 +135,7 @@ class Text(ModelFieldFactory):
_type = str _type = str
def __new__( # type: ignore def __new__( # type: ignore
cls, *, allow_blank: bool = False, strip_whitespace: bool = False, **kwargs: Any cls, *, allow_blank: bool = True, strip_whitespace: bool = False, **kwargs: Any
) -> Type[BaseField]: ) -> Type[BaseField]:
kwargs = { kwargs = {
**kwargs, **kwargs,
@ -144,6 +145,7 @@ class Text(ModelFieldFactory):
if k not in ["cls", "__class__", "kwargs"] if k not in ["cls", "__class__", "kwargs"]
}, },
} }
kwargs['nullable'] = kwargs['allow_blank']
return super().__new__(cls, **kwargs) return super().__new__(cls, **kwargs)
@classmethod @classmethod
@ -229,6 +231,32 @@ class BigInteger(Integer):
_bases = (pydantic.ConstrainedInt, BaseField) _bases = (pydantic.ConstrainedInt, BaseField)
_type = int _type = int
def __new__( # type: ignore
cls,
*,
minimum: int = None,
maximum: int = None,
multiple_of: int = None,
**kwargs: Any
) -> Type[BaseField]:
autoincrement = kwargs.pop("autoincrement", None)
autoincrement = (
autoincrement
if autoincrement is not None
else kwargs.get("primary_key", False)
)
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
kwargs["ge"] = kwargs["minimum"]
kwargs["le"] = kwargs["maximum"]
return super().__new__(cls, **kwargs)
@classmethod @classmethod
def get_column_type(cls, **kwargs: Any) -> Any: def get_column_type(cls, **kwargs: Any) -> Any:
return sqlalchemy.BigInteger() return sqlalchemy.BigInteger()

View File

@ -46,12 +46,17 @@ setup(
long_description=get_long_description(), long_description=get_long_description(),
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
keywords=['orm', 'sqlalchemy', 'fastapi', 'pydantic', 'databases', 'async', 'alembic'], keywords=['orm', 'sqlalchemy', 'fastapi', 'pydantic', 'databases', 'async', 'alembic'],
author="collerek", author="Radosław Drążkiewicz",
author_email="collerek@gmail.com", author_email="collerek@gmail.com",
packages=get_packages(PACKAGE), packages=get_packages(PACKAGE),
package_data={PACKAGE: ["py.typed"]}, package_data={PACKAGE: ["py.typed"]},
data_files=[("", ["LICENSE.md"])], data_files=[("", ["LICENSE.md"])],
install_requires=["databases", "pydantic>=1.5", "sqlalchemy"], install_requires=["databases", "pydantic>=1.5", "sqlalchemy"],
extras_require={
"postgresql": ["asyncpg", "psycopg2"],
"mysql": ["aiomysql", "pymysql"],
"sqlite": ["aiosqlite"],
},
classifiers=[ classifiers=[
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Environment :: Web Environment", "Environment :: Web Environment",