From 6b0cfdbfd36772664f68d6db5351c2c9cf3e2c0f Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 13 Aug 2020 17:10:13 +0200 Subject: [PATCH] work on relations docs --- docs/relations.md | 96 +++++++++++++++++++++++++++++++++++ docs_src/relations/docs001.py | 26 ++++++++++ docs_src/relations/docs002.py | 39 ++++++++++++++ docs_src/relations/docs003.py | 44 ++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 docs_src/relations/docs001.py create mode 100644 docs_src/relations/docs002.py create mode 100644 docs_src/relations/docs003.py diff --git a/docs/relations.md b/docs/relations.md index e69de29..2fbe479 100644 --- a/docs/relations.md +++ b/docs/relations.md @@ -0,0 +1,96 @@ +# Relations + +## Defining a relationship + +### Foreign Key + +To define a relationship you simply need to create a ForeignKey field on one `Model` and point it to another `Model`. + +```Python hl_lines="24" +--8<-- "../docs_src/relations/docs001.py" +``` + +It automatically creates an sql foreign key constraint on a underlying table as well as nested pydantic model in the definition. + + +```Python hl_lines="29 33" +--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. + +### 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" +``` + +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. + +## Relationship Manager + +Since orm uses Sqlalchemy core under the hood to prepare the queries, +the orm needs a way to uniquely identify each relationship between to tables to construct working queries. + +Imagine that you have models as following: + +```Python +--8<-- "../docs_src/relations/docs003.py" +``` + +Now imagine that you want to go from school class to student and his category and to teacher and his category. + +```Python +classes = await SchoolClass.objects.select_related( +["teachers__category", "students__category"]).all() +``` + +!!!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 it won't work by default -> you would need to use aliases for category tables and columns. + +But don't worry - orm 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 RelationshipManager that you can access like this: + +```python +SchoolClass._orm_relationship_manager +``` + +It's the same object for all `Models` + +```python +print(Teacher._orm_relationship_manager == Student._orm_relationship_manager) +# will produce: True +``` + +You can even preview the alias used for any relation by passing two tables names. + +```python +print(Teacher._orm_relationship_manager.resolve_relation_join( +'students', 'categories')) +# will produce: KId1c6 (sample value) + +print(Teacher._orm_relationship_manager.resolve_relation_join( +'categories', 'students')) +# will produce: EFccd5 (sample value) +``` + +!!!note + The order that you pass the names matters -> as those are 2 different relationships depending on join order. + + As aliases are produced randomly you can be presented with different results. diff --git a/docs_src/relations/docs001.py b/docs_src/relations/docs001.py new file mode 100644 index 0000000..4a028c3 --- /dev/null +++ b/docs_src/relations/docs001.py @@ -0,0 +1,26 @@ +import orm +import databases +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Album(orm.Model): + __tablename__ = "album" + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + name = orm.String(length=100) + + +class Track(orm.Model): + __tablename__ = "track" + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + album = orm.ForeignKey(Album) + title = orm.String(length=100) + position = orm.Integer() \ No newline at end of file diff --git a/docs_src/relations/docs002.py b/docs_src/relations/docs002.py new file mode 100644 index 0000000..9e3dfda --- /dev/null +++ b/docs_src/relations/docs002.py @@ -0,0 +1,39 @@ +import orm +import databases +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Album(orm.Model): + __tablename__ = "album" + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + name = orm.String(length=100) + + +class Track(orm.Model): + __tablename__ = "track" + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + album = orm.ForeignKey(Album) + title = orm.String(length=100) + position = orm.Integer() + + +print(Track.__table__.columns['album'].__repr__()) +# Will produce: +# Column('album', Integer(), ForeignKey('album.id'), table=) + +print(Track.__pydantic_model__.__fields__['album']) +# Will produce: +# ModelField( +# name='album' +# type=Optional[Album] +# required=False +# default=None) diff --git a/docs_src/relations/docs003.py b/docs_src/relations/docs003.py new file mode 100644 index 0000000..aa54c29 --- /dev/null +++ b/docs_src/relations/docs003.py @@ -0,0 +1,44 @@ +import databases +import sqlalchemy +import orm + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class SchoolClass(orm.Model): + __tablename__ = "schoolclasses" + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + name = orm.String(length=100) + + +class Category(orm.Model): + __tablename__ = "categories" + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + name = orm.String(length=100) + + +class Student(orm.Model): + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + name = orm.String(length=100) + schoolclass = orm.ForeignKey(SchoolClass) + category = orm.ForeignKey(Category) + + +class Teacher(orm.Model): + __metadata__ = metadata + __database__ = database + + id = orm.Integer(primary_key=True) + name = orm.String(length=100) + schoolclass = orm.ForeignKey(SchoolClass) + category = orm.ForeignKey(Category)