Migrating from Tortoise ORM¶
Yara ORM was designed to feel like Tortoise ORM: the same
Django-style models, the same lazy chainable querysets, the same __ field lookups and
Q objects. Most application code moves across with only the import lines and the
initialisation call changed — and runs 2–9× faster on the Rust engine
underneath.
This guide walks through what changes, mapped one concept at a time. If a Tortoise feature isn't mentioned here, the odds are good it works the same way — check the matching guide for the exact surface.
At a glance¶
| Concept | Tortoise ORM | Yara ORM |
|---|---|---|
| Import | from tortoise import fields, models |
from yara_orm import fields, Model |
| Base class | class User(models.Model) |
class User(Model) |
| Init | Tortoise.init(db_url=..., modules=...) |
YaraOrm.init("postgres://…") |
| Create schema | Tortoise.generate_schemas() |
YaraOrm.generate_schemas() |
| Shut down | Tortoise.close_connections() |
YaraOrm.close() |
| Field lookups | name__icontains=… |
name__icontains=… (identical) |
Q objects |
from tortoise.expressions import Q |
from yara_orm import Q |
F expressions |
from tortoise.expressions import F |
from yara_orm import F |
| Transaction | from tortoise.transactions import in_transaction |
from yara_orm import in_transaction |
| Eager loading | select_related / prefetch_related |
select_related / prefetch_related (identical) |
The headline difference is initialisation: Yara ORM does not take a modules list.
Models register themselves on definition, so you just hand init() a connection URL.
1. Models and fields¶
Field definitions are nearly identical — change the base class and the import.
from tortoise import fields
from tortoise.models import Model
class Author(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=120, index=True)
created_at = fields.DatetimeField(auto_now_add=True)
class Book(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
author = fields.ForeignKeyField("models.Author", related_name="books")
tags = fields.ManyToManyField("models.Tag", related_name="books")
from yara_orm import Model, fields
class Author(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=120, index=True)
created_at = fields.DatetimeField(auto_now_add=True)
class Book(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
author = fields.ForeignKeyField("Author", related_name="books")
tags = fields.ManyToManyField("Tag", related_name="books")
Two things to note:
Modelis imported directly fromyara_orm, not from amodelssubmodule.- Relation targets are bare model names (
"Author"), not the dotted"models.Author"Tortoise uses — there is no app/module registry to qualify.
See Models & fields for the full field catalogue and Meta options.
2. Initialisation and shutdown¶
This is the biggest change. Tortoise needs a modules mapping so it can discover your
models; Yara ORM discovers them automatically and only wants a URL.
The same code switches to SQLite by changing only the URL
(sqlite:///app.db) — see Backends.
3. Querying¶
Querysets are the part that moves across unchanged. Lazy, chainable, awaited to run — the same mental model as Tortoise.
# Identical on both ORMs:
books = await Book.filter(rating__gte=4).order_by("-rating").limit(10)
count = await Book.filter(author=ada).count()
ada = await Author.get(name="Ada Lovelace")
maybe = await Author.get_or_none(name="Nobody")
await Book.exclude(title__startswith="Draft")
Field lookups (__gte, __icontains, __in, __isnull, __startswith, …) are the same
spellings. The one import to redirect is Q (and F for column expressions):
from yara_orm import Q, F
await Book.filter(Q(rating__gte=4) | Q(title__icontains="sea"))
await Account.filter(id=src).update(balance=F("balance") - amount)
For read-heavy paths, Yara ORM's projections — values() and values_list() — skip model
construction and run noticeably faster; see
Querying → Projections.
4. Relations¶
Forward foreign keys are awaitable and reverse managers iterate, just like Tortoise:
book = await Book.get(title="Notes")
author = await book.author # forward FK — awaitable
async for book in ada.books: # reverse manager — iterable
print(book.title)
# Avoid N+1 with eager loading (same names as Tortoise):
for author in await Author.all().prefetch_related("books"):
...
await Book.all().select_related("author")
select_related collapses forward-FK joins into one query and prefetch_related batches
reverse/M2M relations into a second query — the benchmarks show this
paying off 10–38× versus naive N+1 access. See Relations.
5. Transactions¶
Tortoise's in_transaction and @atomic both have direct equivalents — only the import
path changes.
Nesting an in_transaction/@atomic block establishes a savepoint on the same
connection (independent inner rollback), and you can request an
isolation level with
in_transaction(isolation=IsolationLevel.SERIALIZABLE). See Transactions.
6. Migrations¶
Both ORMs offer operation-based, auto-generated migrations. If you currently use
Aerich with Tortoise, the workflow maps directly onto
Yara ORM's built-in commands — makemigrations, upgrade, downgrade — with no third-party
tool to install. See Migrations for the full command set, including rename
and constraint operations.
Switching an existing database
Migrations describe schema changes, not data. When pointing Yara ORM at a database
that Tortoise already created, generate an initial migration and reconcile it against the
live schema before applying further changes, rather than running generate_schemas()
against populated tables.
What to double-check¶
- Dotted relation targets — rewrite
"models.Author"to"Author". - The
modulesargument — drop it;init()takes only a URL. - Import paths for
Q,F,in_transaction,atomic— all move toyara_orm. close_connections()→close()at shutdown.- Signals and manual SQL — supported; see Signals and Manual SQL for the exact call shapes if you relied on Tortoise's.
Once those are done, the bulk of your query and model code should run as-is — on a much faster engine.