From 9be93b4bfcd4838c1e9f66d857012a903a0617ce Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Dec 2024 12:46:37 +0100 Subject: [PATCH 1/3] WIP: add create to relations Changes: - add create to relations --- edgy/core/db/relationships/relation.py | 9 +++++++++ edgy/protocols/many_relationship.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/edgy/core/db/relationships/relation.py b/edgy/core/db/relationships/relation.py index c7339962..27855a06 100644 --- a/edgy/core/db/relationships/relation.py +++ b/edgy/core/db/relationships/relation.py @@ -108,6 +108,10 @@ def stage(self, *children: "BaseModelType") -> None: ) self.refs.append(self.expand_relationship(child)) + async def create(self, *args: Any, **kwargs: Any) -> Optional["BaseModelType"]: + """Creates and add a child""" + return await self.add(self.to(*args, **kwargs)) + async def add(self, child: "BaseModelType") -> Optional["BaseModelType"]: """ Adds a child to the model as a list @@ -264,6 +268,11 @@ async def save_related(self) -> None: while self.refs: await self.add(self.refs.pop()) + async def create(self, *args: Any, **kwargs: Any) -> Optional["BaseModelType"]: + """Creates and add a child""" + kwargs[self.to_foreign_key] = self.instance + return await self.to.query.create(*args, **kwargs) + async def add(self, child: "BaseModelType") -> Optional["BaseModelType"]: """ Adds a child to the model as a list diff --git a/edgy/protocols/many_relationship.py b/edgy/protocols/many_relationship.py index 0c8cb972..3cd9c43c 100644 --- a/edgy/protocols/many_relationship.py +++ b/edgy/protocols/many_relationship.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable if TYPE_CHECKING: # pragma: nocover from edgy.core.db.models.types import BaseModelType @@ -12,6 +12,8 @@ class ManyRelationProtocol(Protocol): async def save_related(self) -> None: ... + async def create(self, *args: Any, **kwargs: Any) -> Optional["BaseModelType"]: ... + async def add(self, child: "BaseModelType") -> Optional["BaseModelType"]: ... def stage(self, *children: "BaseModelType") -> None: From cc5d8d42392268a86818dd286a64dbec04aad6f4 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Dec 2024 16:23:41 +0100 Subject: [PATCH 2/3] Changes: - fix typings --- edgy/core/db/relationships/relation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edgy/core/db/relationships/relation.py b/edgy/core/db/relationships/relation.py index 27855a06..e19c0262 100644 --- a/edgy/core/db/relationships/relation.py +++ b/edgy/core/db/relationships/relation.py @@ -271,7 +271,7 @@ async def save_related(self) -> None: async def create(self, *args: Any, **kwargs: Any) -> Optional["BaseModelType"]: """Creates and add a child""" kwargs[self.to_foreign_key] = self.instance - return await self.to.query.create(*args, **kwargs) + return await cast("QuerySet", self.to.query).create(*args, **kwargs) async def add(self, child: "BaseModelType") -> Optional["BaseModelType"]: """ From d5f3b4950ac0454023dc77c1a85454fd1c307c32 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 15 Dec 2024 07:10:27 +0100 Subject: [PATCH 3/3] Changes: - add more documentation related one to many relations - document create method - add tests --- docs/fields.md | 2 + docs/queries/many-to-many.md | 32 ++++++-- docs/queries/many-to-one.md | 74 +++++++++++++++++++ docs/release-notes.md | 1 + docs_src/queries/manytoone/example.py | 22 ++++++ mkdocs.yml | 1 + tests/foreign_keys/test_foreignkey.py | 11 +++ tests/foreign_keys/test_many_to_many_field.py | 12 +++ 8 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 docs/queries/many-to-one.md create mode 100644 docs_src/queries/manytoone/example.py diff --git a/docs/fields.md b/docs/fields.md index 231ad39e..8341c020 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -499,6 +499,8 @@ The Profile object can be accessed by the `profile` attribute we choosed as seco When the second parameter is empty, the parent object is not included as attribute. +The reverse end of a `ForeignKey` is a [Many to one relation](./queries/many-to-one.md). + ##### Parameters diff --git a/docs/queries/many-to-many.md b/docs/queries/many-to-many.md index 6c3f3d6b..29e500d5 100644 --- a/docs/queries/many-to-many.md +++ b/docs/queries/many-to-many.md @@ -44,9 +44,10 @@ It is like a virtual path part which can be traversed via the `__` path building With the many to many you can perform all the normal operations of searching from normal queries to the [related name][related_name] as per normal search. -ManyToMany allows two different methods when using it (the same applies for the reverse side). +ManyToMany allows three different methods when using it (the same applies for the reverse side). * `add()` - Adds a record to the ManyToMany. +* `create()` - Create a new record and add it to the ManyToMany. * `remove()` - Removes a record to the ManyToMany. Let us see how it looks by using the following example. @@ -65,10 +66,25 @@ green_team = await Team.query.create(name="Green Team") organisation = await Organisation.query.create(ident="Acme Ltd") # Add teams to the organisation -organisation.teams.add(blue_team) -organisation.teams.add(green_team) +await organisation.teams.add(blue_team) +await organisation.teams.add(green_team) ``` +#### create() + +You can fuse this to: + + +```python hl_lines="4-5" +organisation = await Organisation.query.create(ident="Acme Ltd") + +# Add teams to the organisation +await organisation.teams.create(name="Blue Team") +await organisation.teams.create(name="Green Team") +``` + +This is also more performant because less transactions are required. + #### remove() You can now remove teams from organisations, something like this. @@ -80,13 +96,13 @@ red_team = await Team.query.create(name="Red Team") organisation = await Organisation.query.create(ident="Acme Ltd") # Add teams to organisation -organisation.teams.add(blue_team) -organisation.teams.add(green_team) -organisation.teams.add(red_team) +await organisation.teams.add(blue_team) +await organisation.teams.add(green_team) +await organisation.teams.add(red_team) # Remove the teams from the organisation -organisation.teams.remove(red_team) -organisation.teams.remove(blue_team) +await organisation.teams.remove(red_team) +await organisation.teams.remove(blue_team) ``` Hint: when unique, remove works also without argument. diff --git a/docs/queries/many-to-one.md b/docs/queries/many-to-one.md new file mode 100644 index 00000000..9940340d --- /dev/null +++ b/docs/queries/many-to-one.md @@ -0,0 +1,74 @@ +# Many-to-One relations + +Many to one relations are the inverse of a `ForeignKey`. There is only an implicit field for this, +which is added to the target model with the related name specified or automatically generated. +The interface is quite similar to [ManyToMany](./many-to-many.md). + + +## Operations + +With the many to many you can perform all the normal operations of searching from normal queries +to the [related_name][related_name] as per normal search. + +ManyToMany allows three different methods when using it (the same applies for the reverse side). + +* `add()` - Adds a record to the relation (Updates the ForeignKey). +* `create()` - Create a new record and add it to the relation. +* `remove()` - Removes a record to the relation (set the ForeignKey to None). + +Let us see how it looks by using the following example. + +```python+ +{!> ../docs_src/queries/manytoone/example.py !} +``` + +#### add() + +You can now add teams to organisations, something like this. + +```python +member = await TeamMember.query.create(name="member1") +blue_team = await Team.query.create(name="Blue Team")´ + +await blue_team.members.add(member) +``` + +#### create() + +You can fuse this to: + + +```python +blue_team = await Team.query.create(name="Blue Team") +green_team = await Team.query.create(name="Green Team") +member1 = await blue_team.members.create(name="edgy") +member2 = await green_team.members.create(name="fastapi") +``` + +This is also more performant because less transactions are required. + +#### remove() + +You can now remove teams from organisations, something like this. + +```python +blue_team = await Team.query.create(name="Blue Team")´ + +member = await blue_team.members.create(name="member1") +# and now remove +await blue_team.members.remove(member) +``` + +Hint: when unique, remove works also without argument. + +#### Related name + +When a [related_name][related_name] is not defined, Edgy will automatically generate one with the following +format: + +```shell +s_set +``` + + +[related_name]: ./related-name.md diff --git a/docs/release-notes.md b/docs/release-notes.md index 3236a05e..0be4eeec 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ hide: - Generalized `hash_to_identifier` function. - `get_name` function on `metadata_by_url` dict. - Differing databases can be passed via `database` attribute on models. +- `create` method on relations (reverse side of ForeignKeys and both sides of ManyToMany). ### Changed diff --git a/docs_src/queries/manytoone/example.py b/docs_src/queries/manytoone/example.py new file mode 100644 index 00000000..ce9493cc --- /dev/null +++ b/docs_src/queries/manytoone/example.py @@ -0,0 +1,22 @@ +from typing import List + +import edgy +from edgy import Database, Registry + +database = Database("sqlite:///db.sqlite") +models = Registry(database=database) + + +class Team(edgy.Model): + name: str = edgy.fields.CharField(max_length=100) + + class Meta: + registry = models + + +class TeamMember(edgy.Model): + name: str = edgy.fields.CharField(max_length=100) + team: Team = edgy.fields.ForeignKey(Team, related_name="members") + + class Meta: + registry = models diff --git a/mkdocs.yml b/mkdocs.yml index 946e6617..02331d42 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ nav: - Queries: "queries/queries.md" - Secrets: "queries/secrets.md" - Related Name: "queries/related-name.md" + - Many-to-One relations: "queries/many-to-one.md" - ManyToMany: "queries/many-to-many.md" - Prefetch Related: "queries/prefetch.md" - Transactions: "transactions.md" diff --git a/tests/foreign_keys/test_foreignkey.py b/tests/foreign_keys/test_foreignkey.py index fd397db7..036e5127 100644 --- a/tests/foreign_keys/test_foreignkey.py +++ b/tests/foreign_keys/test_foreignkey.py @@ -129,6 +129,17 @@ async def test_new_create2(): assert len(tracks) == 2 +async def test_create_via_relation_create(): + await Track.query.create(title="The Waters", position=3) + + album = await Album.query.create(name="Malibu") + await album.tracks_set.create(title="The Bird", position=1) + await album.tracks_set.create(title="Heart don't stand a chance", position=2) + tracks = await album.tracks_set.all() + + assert len(tracks) == 2 + + async def test_select_related(): album = await Album.query.create(name="Malibu") await Track.query.create(album=album, title="The Bird", position=1) diff --git a/tests/foreign_keys/test_many_to_many_field.py b/tests/foreign_keys/test_many_to_many_field.py index 99f0ded1..8cdd8aef 100644 --- a/tests/foreign_keys/test_many_to_many_field.py +++ b/tests/foreign_keys/test_many_to_many_field.py @@ -70,6 +70,18 @@ async def test_add_many_to_many(): assert len(total_tracks) == 3 +async def test_create_many_to_many(): + album = await Album.query.create(name="Malibu") + + await album.tracks.create(title="The Bird", position=1) + await album.tracks.create(title="Heart don't stand a chance", position=2) + await album.tracks.create(title="The Waters", position=3) + + total_tracks = await album.tracks.all() + + assert len(total_tracks) == 3 + + async def test_add_many_to_many_with_repeated_field(): track1 = await Track.query.create(title="The Bird", position=1) track2 = await Track.query.create(title="Heart don't stand a chance", position=2)