Skip to content

The Record base model, automatic timestamps and soft deletes, Meta options, serialisation with to_dict and to_json, and the get_or_none and get_or_create shortcuts.

from sillo.record import Model
from tortoise import fields
class Post(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
body = fields.TextField()
author = fields.ForeignKeyField("models.User", related_name="posts")
class Meta:
table = "posts"
ordering = ["-created_at"]
def __str__(self):
return self.title

Model is Tortoise’s Model with three mixins already applied. Fields, relations, querysets and Meta all work as the Tortoise documentation describes.

Three fields are declared on the base class, so every model has them without saying so:

FieldBehaviour
created_atSet to UTC now on insert. Never updated.
updated_atSet to UTC now on every save.
deleted_atNullable. None means active.
post = await Post.create(title="Hello", body="…")
post.created_at # datetime, UTC
post.deleted_at # None

They are real columns and appear in your migrations. If you do not want them, inherit from Tortoise’s Model directly. There is no way to switch them off individually, because a base class that sometimes has a column is a base class whose migrations are unpredictable.

Anything that has to name a row without knowing what it is uses it — an admin panel, a log line, a repr in a traceback — and so does every debugging session. A model without one shows as Post object (4).

post.to_dict()
post.to_dict(exclude=["body"])
post.to_dict(include=["id", "title"])
post.to_json()
post.to_json(indent=2)

include wins when both are given: it is a whitelist, and a whitelist that also honoured a blacklist would be ambiguous about which one was the mistake.

Relations are not followed by default. Fetch them first:

await post.fetch_related("author")
post = await Post.get_or_none(id=4)

None instead of raising DoesNotExist. The right shape when absence is an expected answer, a lookup by a user-supplied id, say.

tag, created = await Post.get_or_create(
slug="python",
defaults={"title": "Python"},
)

Returns the instance and whether it was created. defaults supplies the fields used only on creation; the rest are the lookup.

await post.soft_delete() # sets deleted_at
await post.restore() # clears it
await post.delete() # actually deletes the row
await Post.active() # deleted_at IS NULL
await Post.deleted() # deleted_at IS NOT NULL
await Post.count_active()

active() and deleted() return querysets, so they chain:

recent = await Post.active().order_by("-created_at").limit(10)

More in Mixins.

Tortoise’s Meta options all apply: table, ordering, unique_together, indexes, abstract, table_description.

class Meta:
table = "posts"
ordering = ["-created_at"]
unique_together = (("author", "slug"),)

The base class sets manager = RecordManager(), which is what applies global scopes. If you set your own manager, subclass RecordManager or global scopes stop being applied.