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://…") (or init(config=...) with a Tortoise config dict) |
| Create schema | Tortoise.generate_schemas() |
YaraOrm.generate_schemas() (auto-orders FK dependencies) |
| Shut down | Tortoise.close_connections() |
YaraOrm.close() (close_connections() kept as an alias) |
| 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 MySQL or SQLite by changing only the URL
(mysql://user:pass@localhost/app, sqlite:///app.db) — Tortoise's three
backends are all covered, and driver-qualified Tortoise URLs such as
mysql+aiomysql:// are normalised automatically. 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.
7. Compatibility helpers¶
A range of Tortoise spellings are accepted directly so large codebases migrate with fewer edits:
Fields & models
UUIDField(primary_key=True)applies theuuid4default (same aspk=True), and a foreign key set from a string id (obj.parent_id = str(uuid)) is coerced to the target primary key's type when bound.JSONField(encoder=..., decoder=...)value-transform hooks (e.g. to keep oversized integers JS-safe). With noencoder, exotic Python values stored in a JSON column (UUID,Decimal,datetime/date/time,set,Enum) are coerced to JSON-native forms rather than raising, matching a Tortoise + orjson setup.BooleanFieldcoerces non-bool writes withbool(value)(so1/0/"yes"round-trip), andMeta.extra_kwargs = "store"is inherited from a base/abstractMetaby subclasses that declare their ownMeta.Index(..., opclass="gin_trgm_ops")applies a per-column operator class (e.g.gin_trgm_ops,jsonb_path_ops) on PostgreSQL — dropped on MySQL and SQLite — replacing Tortoise'scontrib.postgres.indexes.GinIndex(opclass=...)._meta.db_tableis assignable (Model._meta.db_table = "...") alongside its read access.ManyToManyField(through_fields=(owner_col, target_col))is accepted in Django's(source, target)order (an alias ofbackward_key=/forward_key=); barefields.SET_NULL/fields.CASCADE… on-delete constants exist alongsidefields.OnDelete.*;blank=/max_length=on length-less fields are accepted and ignored.- Relation type hints (
ForeignKeyRelation,ReverseRelation,ManyToManyRelation, …) are importable fromyara_orm.fieldsand are real generics —books: fields.ReverseRelation["Book"]types exactly as in Tortoise (see Typing your relations). As in Tortoise, the field factories return relation-typed values and the underlying classes areForeignKeyFieldInstance/OneToOneFieldInstance/ManyToManyFieldInstanceforisinstancechecks. - A foreign key declared on an
abstract = Truebase is inherited by concrete subclasses (relation accessor included). _metaexposes the Tortoise aliasesdb_table,fields_map,db_fields,fields_db_projection, and fields carryhas_db_field.- Opt into Tortoise's lenient constructor with
Meta.extra_kwargs = "store"to keep unknown__init__kwargs as attributes (yara is strict by default).
Querysets & expressions
Model.get(...)andQuerySet.first()are chainable single-row results.await Model.get(id=x).prefetch_related(...)works, as does a plainawait Model.get(id=x);first()awaits to the instance orNone. Both accept.only(...),.values(...)and.values_list(...)soawait qs.first().values("a", "b")returns a single dict (orNone) — Tortoise'sQuerySetSingle.QuerySet.all()is a no-op terminator.Valueliteral expressions andQ.AND/Q.ORconstants exist.Aggregateis importable fromyara_orm(Tortoise'stortoise.functions.Aggregate).- Model instances compare by
(type, pk)— a refetched row equals one already held, andobj in [<same row>]/ set membership work (__eq__/__hash__). - Aggregates accept an expression or
Caseand an optional_filter=Q(...)(Count("x", _filter=Q(...))→... FILTER (WHERE ...)).QuerySet.using_db()accepts a connection name or object. -
order_by()traverses a forward relation —order_by("author__name")(and multi-hoporder_by("author__country__name")) sort by the related column. -
Pool & connection params via the URL —
application_nameand server settings carry through:?application_name=svc&options=-c%20search_path%3Dmyschema(plusmax_size/min_size/statement_cache_size), replacing Tortoise'scredentialsapplication_name/server_settings.
Manual SQL & lifecycle — connections.get() / the in_transaction() connection
expose execute_query() ((rowcount, rows)), execute_query_dict(),
fetch_one() and execute_script() (multi-statement). Database errors surface
as OperationalError. Register register_query_hook(fn) for SQLCommenter /
tracing on every statement. See Manual SQL.
What to double-check¶
- Dotted relation targets — rewrite
"models.Author"to"Author". - The
modulesargument — drop it;init()takes a URL (orconfig=). - Import paths for
Q,F,in_transaction,atomic— all move toyara_orm. order_by("rel__col")works for forward relations; ordering by a reverse / many-to-many relation raises (it has no single orderable value).- 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.