+
Skip to content

Queryset-level delete method #111

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

Merged
merged 6 commits into from
Sep 21, 2021
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
153 changes: 86 additions & 67 deletions docs/making_queries.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
## Queryset methods

ORM supports a range of query methods which can be chained together.

Let's say you have the following model defined:

```python
Expand All @@ -22,54 +18,30 @@ class Note(orm.Model):
}
```

You can use the following queryset methods:

### Creating instances

You need to pass the required model attributes and values to the `.create()` method:
ORM supports two types of queryset methods.
Some queryset methods return another queryset and can be chianed together like `.filter()` and `order_by`:

```python
await Note.objects.create(text="Buy the groceries.", completed=False)
await Note.objects.create(text="Call Mum.", completed=True)
await Note.objects.create(text="Send invoices.", completed=True)
Note.objects.filter(completed=True).order_by("id")
```

### Querying instances

#### .all()

To retrieve all the instances:
Other queryset methods return results and should be used as final method on the queryset like `.all()`:

```python
notes = await Note.objects.all()
Note.objects.filter(completed=True).all()
```

#### .get()
## Returning Querysets

To get only one instance:

```python
note = await Note.objects.get(id=1)
```

**Note**: `.get()` expects to find only one instance. This can raise `NoMatch` or `MultipleMatches`.

#### .first()

This will return the first instance or `None`:

```python
note = await Note.objects.filter(completed=True).first()
```
### .exclude()

`pk` always refers to the model's primary key field:
To exclude instances:

```python
note = await Note.objects.get(pk=2)
note.pk # 2
notes = await Note.objects.exclude(completed=False).all()
```

#### .filter()
### .filter()

To filter instances:

Expand Down Expand Up @@ -97,89 +69,135 @@ notes = await Note.objects.filter(text__icontains="mum").all()
notes = await Note.objects.filter(id__in=[1, 2, 3]).all()
```

#### .exclude()
### .limit()

To exclude instances:
To limit number of results:

```python
notes = await Note.objects.exclude(completed=False).all()
await Note.objects.limit(1).all()
```

### .offset()

To apply offset to query results:

```python
await Note.objects.offset(1).all()
```

As mentioned before, you can chain multiple queryset methods together to form a query.
As an exmaple:

```python
await Note.objects.order_by("id").limit(1).offset(1).all()
await Note.objects.filter(text__icontains="mum").limit(2).all()
```

#### .order_by()
### .order_by()

To order query results:

```python
notes = await Note.objects.order_by("text", "-id").all()
```

**Note**: This will sort by ascending `text` and descending `id`.
!!! note
This will sort by ascending `text` and descending `id`.

#### .limit()
## Returning results

To limit number of results:
### .all()

To retrieve all the instances:

```python
await Note.objects.limit(1).all()
notes = await Note.objects.all()
```

#### .offset()
### .create()

To apply offset to query results:
You need to pass the required model attributes and values to the `.create()` method:

```python
await Note.objects.offset(1).all()
await Note.objects.create(text="Buy the groceries.", completed=False)
await Note.objects.create(text="Call Mum.", completed=True)
await Note.objects.create(text="Send invoices.", completed=True)
```

As mentioned before, you can chain multiple queryset methods together to form a query.
As an exmaple:
### .delete()

You can `delete` instances by calling `.delete()` on a queryset:

```python
await Note.objects.order_by("id").limit(1).offset(1).all()
await Note.objects.filter(text__icontains="mum").limit(2).all()
await Note.objects.filter(completed=True).delete()
```

It's not very common, but to delete all rows in a table:

```python
await Note.objects.delete()
```

#### .exists()
You can also call delete on a queried instance:

```python
note = await Note.objects.first()

await note.delete()
```

### .exists()

To check if any instances matching the query exist. Returns `True` or `False`.

```python
await Note.objects.filter(completed=True).exists()
```

### Updating instances
### .first()

`.update()` method is defined on model instances.
You need to query to get a `Note` instance first:
This will return the first instance or `None`:

```python
note = await Note.objects.first()
note = await Note.objects.filter(completed=True).first()
```

Then update the field(s):
`pk` always refers to the model's primary key field:

```python
await note.update(completed=True)
note = await Note.objects.get(pk=2)
note.pk # 2
```

### Deleting instances
### .get()

`.delete()` method is defined on model instances.
To get only one instance:

```python
note = await Note.objects.get(id=1)
```

!!! note
`.get()` expects to find only one instance. This can raise `NoMatch` or `MultipleMatches`.

### .update()

`.update()` method is defined on model instances.
You need to query to get a `Note` instance first:

```python
note = await Note.objects.first()
```

Then delete the instance:
Then update the field(s):

```python
await note.delete()
await note.update(completed=True)
```

### Convenience methods
## Convenience Methods

#### get_or_create()
### .get_or_create()

To get an existing instance matching the query, or create a new one.
This will retuurn a tuple of `instance` and `created`.
Expand All @@ -188,4 +206,5 @@ This will retuurn a tuple of `instance` and `created`.
note, created = await Note.objects.get_or_create(text="Going to car wash")
```

**Note**: Since this is doing a [get()](#get), it can raise `MultipleMatches` exception.
!!! note
Since `get_or_create()` is doing a [get()](#get), it can raise `MultipleMatches` exception.
22 changes: 13 additions & 9 deletions orm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def database(self):
return self.model_cls.registry.database

@property
def table(self):
def table(self) -> sqlalchemy.Table:
return self.model_cls.table

@property
Expand Down Expand Up @@ -389,10 +389,9 @@ async def create(self, **kwargs):
)
kwargs = validator.validate(kwargs)

# TODO: Better to implement after UUID, probably need another database
# for key, value in fields.items():
# if value.validator.read_only and value.validator.has_default():
# kwargs[key] = value.validator.get_default_value()
for key, value in fields.items():
if value.validator.read_only and value.validator.has_default():
kwargs[key] = value.validator.get_default_value()

# Build the insert expression.
expr = self.table.insert()
Expand All @@ -403,6 +402,13 @@ async def create(self, **kwargs):
instance.pk = await self.database.execute(expr)
return instance

async def delete(self) -> None:
expr = self.table.delete()
for filter_clause in self.filter_clauses:
expr = expr.where(filter_clause)

return await self.database.execute(expr)

async def get_or_create(self, **kwargs) -> typing.Tuple[typing.Any, bool]:
try:
instance = await self.get(**kwargs)
Expand Down Expand Up @@ -449,7 +455,7 @@ def build_table(cls):
return sqlalchemy.Table(tablename, metadata, *columns, extend_existing=True)

@property
def table(self):
def table(self) -> sqlalchemy.Table:
return self.__class__.table

async def update(self, **kwargs):
Expand All @@ -472,12 +478,10 @@ async def update(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)

async def delete(self):
# Build the delete expression.
async def delete(self) -> None:
pk_column = getattr(self.table.c, self.pkname)
expr = self.table.delete().where(pk_column == self.pk)

# Perform the delete.
await self.database.execute(expr)

async def load(self):
Expand Down
54 changes: 33 additions & 21 deletions tests/test_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class StatusEnum(Enum):
RELEASED = "Released"


class Example(orm.Model):
class Product(orm.Model):
registry = models
fields = {
"id": orm.Integer(primary_key=True),
Expand All @@ -41,6 +41,14 @@ class Example(orm.Model):
}


class User(orm.Model):
registry = models
fields = {
"id": orm.UUID(primary_key=True, default=uuid.uuid4),
"name": orm.String(allow_null=True, max_length=16),
}


@pytest.fixture(autouse=True, scope="module")
def create_test_database():
models.create_all()
Expand All @@ -56,30 +64,34 @@ async def rollback_transactions():


async def test_model_crud():
await Example.objects.create()

example = await Example.objects.get()
assert example.created.year == datetime.datetime.now().year
assert example.created_day == datetime.date.today()
assert example.data == {}
assert example.description == ""
assert example.huge_number == 0
assert example.price is None
assert example.status == StatusEnum.DRAFT
assert example.uuid is None
assert example.value is None

await example.update(
await Product.objects.create()

product = await Product.objects.get()
assert product.created.year == datetime.datetime.now().year
assert product.created_day == datetime.date.today()
assert product.data == {}
assert product.description == ""
assert product.huge_number == 0
assert product.price is None
assert product.status == StatusEnum.DRAFT
assert product.value is None
assert product.uuid is None

await product.update(
data={"foo": 123},
value=123.456,
status=StatusEnum.RELEASED,
price=decimal.Decimal("999.99"),
uuid=uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b"),
)

example = await Example.objects.get()
assert example.value == 123.456
assert example.data == {"foo": 123}
assert example.status == StatusEnum.RELEASED
assert example.price == decimal.Decimal("999.99")
assert example.uuid == uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b")
product = await Product.objects.get()
assert product.value == 123.456
assert product.data == {"foo": 123}
assert product.status == StatusEnum.RELEASED
assert product.price == decimal.Decimal("999.99")
assert product.uuid == uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b")

await User.objects.create(name="Chris")
user = await User.objects.get(name="Chris")
assert user.name == "Chris"
12 changes: 12 additions & 0 deletions tests/test_foreignkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,15 @@ async def test_multiple_fk():
assert len(members) == 4
for member in members:
assert member.team.org.ident == "ACME Ltd"


async def test_queryset_delete_with_fk():
malibu = await Album.objects.create(name="Malibu")
await Track.objects.create(album=malibu, title="The Bird", position=1)

wall = await Album.objects.create(name="The Wall")
await Track.objects.create(album=wall, title="The Wall", position=1)

await Track.objects.filter(album=malibu).delete()
assert await Track.objects.filter(album=malibu).count() == 0
assert await Track.objects.filter(album=wall).count() == 1
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载