Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add create method to relations #244

Merged
merged 3 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 24 additions & 8 deletions docs/queries/many-to-many.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions docs/queries/many-to-one.md
Original file line number Diff line number Diff line change
@@ -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
<foreignkey>s_set
```


[related_name]: ./related-name.md
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 22 additions & 0 deletions docs_src/queries/manytoone/example.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions edgy/core/db/relationships/relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 cast("QuerySet", self.to.query).create(*args, **kwargs)

async def add(self, child: "BaseModelType") -> Optional["BaseModelType"]:
"""
Adds a child to the model as a list
Expand Down
4 changes: 3 additions & 1 deletion edgy/protocols/many_relationship.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions tests/foreign_keys/test_foreignkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions tests/foreign_keys/test_many_to_many_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading