Skip to content

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:

  • Model is imported directly from yara_orm, not from a models submodule.
  • 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.

from tortoise import Tortoise

await Tortoise.init(
    db_url="postgres://user:pass@localhost/app",
    modules={"models": ["myapp.models"]},
)
await Tortoise.generate_schemas()
# …
await Tortoise.close_connections()
from yara_orm import YaraOrm

await YaraOrm.init("postgres://user:pass@localhost/app")
await YaraOrm.generate_schemas()
# …
await YaraOrm.close()

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.

from tortoise.transactions import in_transaction, atomic

async with in_transaction():
    ...

@atomic()
async def f(): ...
from yara_orm import in_transaction, atomic

async with in_transaction():
    ...

@atomic()
async def f(): ...

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 modules argument — drop it; init() takes only a URL.
  • Import paths for Q, F, in_transaction, atomic — all move to yara_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.