Migrar desde el ORM de Django¶
La superficie de dorm está intencionadamente cerca de la de Django, así que la mayoría del código se porta con renames + cambios de import. Esta página recoge las diferencias con las que te vas a topar de verdad.
Imports¶
# Django
from django.db import models, transaction
class User(models.Model):
name = models.CharField(max_length=100)
# dorm
import dorm
class User(dorm.Model):
name = dorm.CharField(max_length=100)
dorm.transaction.atomic se corresponde con
django.db.transaction.atomic (tanto context manager como
decorador).
Settings¶
No hay setup tipo INSTALLED_APPS = ["django.contrib.auth", ...].
O bien:
- Pones un
settings.pyjunto a tus paquetes y dejas que dorm haga autodiscover, o - Llamas a
dorm.configure(DATABASES={...}, INSTALLED_APPS=["miapp"])programáticamente.
dorm no trae auth, admin, staticfiles, ni ninguna otra
batería de Django — tráete las tuyas.
Cheat sheet de campos¶
| Django | dorm |
|---|---|
models.CharField(max_length=N) |
dorm.CharField(max_length=N) |
models.TextField() |
dorm.TextField() |
models.IntegerField() |
dorm.IntegerField() |
models.BigIntegerField() |
dorm.BigIntegerField() |
models.DecimalField(...) |
dorm.DecimalField(...) |
models.BooleanField() |
dorm.BooleanField() |
models.DateField() / DateTimeField() |
igual |
models.JSONField() |
dorm.JSONField() |
models.UUIDField() |
dorm.UUIDField() |
models.EmailField() |
dorm.EmailField() (valida al asignar) |
models.ForeignKey(To, on_delete=CASCADE) |
dorm.ForeignKey(To, on_delete=dorm.CASCADE) |
models.OneToOneField(...) |
dorm.OneToOneField(...) |
models.ManyToManyField(...) |
dorm.ManyToManyField(...) |
ArrayField (contrib postgres) |
dorm.ArrayField(base_field) |
BinaryField |
dorm.BinaryField() |
models.SlugField |
dorm.SlugField() |
auto_now=True / auto_now_add=True |
igual en DateTimeField |
default=, null=, blank= |
igual |
validators=[...] |
igual |
Cheat sheet de QuerySet¶
| Django | dorm | Notas |
|---|---|---|
qs.filter(x=1) |
qs.filter(x=1) |
idéntico |
qs.exclude(x=1) |
qs.exclude(x=1) |
idéntico |
qs.get(pk=1) |
qs.get(pk=1) |
lanza Model.DoesNotExist |
qs.aget(pk=1) (Django 4.2+) |
qs.aget(pk=1) |
idéntico |
qs.values("a", "b") |
qs.values("a", "b") |
devuelve QS encadenable de dicts |
qs.count() |
qs.count() |
idéntico |
qs.aggregate(Sum(...)) |
qs.aggregate(Sum(...)) |
idéntico |
qs.bulk_create(objs) |
qs.bulk_create(objs) |
idéntico |
qs.bulk_update(objs, fields) |
qs.bulk_update(objs, fields) |
dorm usa un CASE WHEN por batch |
qs.iterator(chunk_size=N) |
qs.iterator(chunk_size=N) |
cursor server-side en PG |
qs.explain() |
qs.explain(analyze=True) |
extra de dorm: imprime el plan |
qs.using("replica") |
qs.using("replica") |
idéntico |
qs.select_for_update() |
qs.select_for_update() |
idéntico |
Q(a=1) | Q(b=2) |
Q(a=1) | Q(b=2) |
idéntico |
F("col") |
F("col") |
idéntico |
Métodos que tienes en dorm y no (todavía) en Django:
qs.aexplain(analyze=True)— EXPLAIN async.await qs— cada QuerySet es awaitable; equivalente a materializar con[x async for x in qs].
Migraciones¶
makemigrations, migrate, showmigrations, squashmigrations se
comportan como sus hermanos de Django. Nuevo en dorm:
dorm migrate --dry-run— imprime SQL sin ejecutar.dorm dbcheck— diff de cada modelo contra el esquema vivo.dorm sql users.User— imprime elCREATE TABLEde un modelo.
dorm migrate --fake y dorm migrate --fake-initial (3.0+)
registran migraciones como aplicadas sin ejecutar sus operaciones —
útil al adoptar dorm contra una base legacy administrada a mano.
No necesitas asgiref (3.0+)¶
Django incluye asgiref.sync porque el ORM de Django fue sync-only
durante años — toda llamada al modelo dentro de una vista async
necesitaba envolverse en sync_to_async(...) para no bloquear el
event loop. dorm tiene path async nativo desde el día uno, así
que el puente es innecesario.
| Django (ORM sync) | dorm (async-nativo) |
|---|---|
await sync_to_async(User.objects.get)(pk=1) |
await User.objects.aget(pk=1) |
await sync_to_async(list)(qs) |
[u async for u in qs.aiterator()] o await qs |
await sync_to_async(User.objects.create)(...) |
await User.objects.acreate(...) |
await sync_to_async(user.save)() |
await user.asave() |
await sync_to_async(qs.update)(...) |
await qs.aupdate(...) |
with transaction.atomic(): ... |
async with aatomic(): ... |
Cada método de queryset / manager en dorm tiene contraparte a*
que pasa por el wrapper async del backend — sin thread pool, sin
puente sync-async, sin alloc de Token por llamada. No importes
asgiref para código de ORM. Si te ves alcanzando sync_to_async
alrededor de un Model.objects.*, cambia al método a* que toca.
Para detectarlo en dev / tests, activa el async-guard:
# conftest.py o startup de app (solo desarrollo)
from dorm.contrib.asyncguard import enable_async_guard
enable_async_guard(mode="warn") # WARNING por call site infractor
# enable_async_guard(mode="raise") # raise en todos los infractores
El guard engancha pre_query y recorre el call stack — llamadas
sync al ORM dentro de un event loop disparan la acción configurada;
las llamadas async pasan en silencio.
Lo que falta a propósito¶
- Sin admin. dorm es un ORM, no un framework CMS.
- Sin middleware request/response. dorm no tiene capa HTTP.
- Datetimes con timezone llegan en 3.0+: pon
settings.USE_TZ = Truepara activar el comportamiento Django ≥4 (conversión naive→aware, normalización UTC en INSERT,TIMESTAMP WITH TIME ZONEen PG). DefaultFalsemantiene el comportamiento previo a 3.0. dorm.contrib.authopcional (3.0+). Modelos User / Group / Permission con hashing PBKDF2 stdlib. Tokens de reset stateless llegan en 3.0 (dorm.contrib.auth.tokens.PasswordResetTokenGenerator) para el flujo de password-reset / email-verification.Meta.permissions = [...](3.0+) — declara permisos custom y materialízalos enauth_permissioncondorm.contrib.auth.management.sync_permissions().Meta.proxy = True(3.0+) — proxy models comparten la tabla del padre; el autodetector los salta asímakemigrationsno emite unCreateModelfantasma.Model.from_db(db, field_names, values)(3.0+) — hook de Django para hidratación custom. Stampa el alias en_state.db.QuerySet.dates(field, kind)/datetimes(field, kind)(3.0+) — devuelvenlist[date]/list[datetime]truncados distinct para listings de archivo.dorm.transaction.savepoint()/savepoint_commit()/savepoint_rollback()(3.0+) — savepoints manuales dentro de unatomic(). Misma forma que la familiadjango.db.transaction.savepoint.- Operadores PG sobre JSONField (3.0+):
__contained_by,__has_key,__has_keys,__has_any_keys,__overlap,__len. Misma forma quecontrib.postgresde Django. GenericForeignKeyvive endorm.contrib.contenttypes, con la misma forma que Django.- Cifrado opcional (3.0+) via
dorm.contrib.encrypted(EncryptedCharField/EncryptedTextField). AES-GCM, modo determinista para lookups de igualdad, rotación de claves. Requierepip install 'djanorm[encrypted]'. - Exporter Prometheus opcional (3.0+) via
dorm.contrib.prometheus— contadores + histogramas en formato text-exposition plano, sin SDK externo. - Multi-tenant
dorm.contrib.tenants(3.0+) — switch de PostgreSQLsearch_pathviaTenantContext/aTenantContext; runner de migraciones por tenant llega en v3.1. - Scaffold MySQL / MariaDB (3.0+).
ENGINE = "mysql"parsea porparse_database_urly el wrapper de conexión raiseaImproperlyConfiguredapuntando al milestone v3.1. Permite pinear con un config string forward-compatible hoy.
Lo que es mejor que Django¶
- Pool async con retry ante errores transitorios y detección de slow queries — funciona con FastAPI / Starlette out of the box.
- Generics
Field[T]— tu IDE sabe queuser.nameesstry flageauser.naem. Plugindjanorm-mypylo extiende a kwargsfilter()y suffixes lookup en compile-time. DormSchemapara FastAPI — esquemas single-source-of-truth conclass Meta: model = User, incluyendo relaciones anidadas.- Footprint diminuto de dependencias:
psycopg+aiosqlite, opcionalmentepydantic. Sin Django. - Production hardening built-in — circuit breaker, query budget, lag-aware routing, outbox, sharding, idempotency keys.
Tabla equivalencias rápidas (4.0)¶
Imports cotidianos¶
| Django | dorm |
|---|---|
from django.db import models |
import dorm |
models.Model |
dorm.Model |
models.CharField / IntegerField / etc. |
dorm.CharField / etc. |
models.Q, models.F, models.Subquery, models.Exists |
dorm.Q, dorm.F, dorm.Subquery, dorm.Exists |
models.Count, Sum, Avg, ... |
dorm.Count, dorm.Sum, dorm.Avg, ... |
models.OuterRef |
dorm.OuterRef |
models.FilteredRelation |
dorm.FilteredRelation |
models.UniqueConstraint, CheckConstraint |
dorm.UniqueConstraint, dorm.CheckConstraint |
models.Index |
dorm.Index |
from django.db import transaction |
from dorm import transaction |
transaction.atomic() |
dorm.transaction.atomic() |
from django.db import connection |
from dorm.db.connection import get_connection |
contrib.postgres¶
| Django | dorm |
|---|---|
contrib.postgres.fields.ArrayField |
dorm.ArrayField |
contrib.postgres.fields.JSONField |
dorm.JSONField (también en SQLite) |
contrib.postgres.fields.HStoreField |
dorm.HStoreField (4.0+; fallback TEXT en SQLite) |
contrib.postgres.fields.RangeField |
dorm.RangeField y subclases |
contrib.postgres.search.SearchVector |
dorm.search.SearchVector |
contrib.postgres.search.SearchQuery |
dorm.search.SearchQuery |
contrib.postgres.search.SearchRank |
dorm.search.SearchRank |
contrib.postgres.search.SearchHeadline |
dorm.search.SearchHeadline |
contrib.postgres.search.TrigramSimilarity |
dorm.search.TrigramSimilarity (4.0+) |
contrib.postgres.aggregates.StringAgg |
dorm.StringAgg |
contrib.postgres.aggregates.ArrayAgg |
dorm.ArrayAgg |
contrib.postgres.aggregates.BoolAnd/BoolOr |
dorm.BoolAnd / dorm.BoolOr |
Choices / Enums¶
# Django
class Status(models.TextChoices):
ACTIVE = "active", "Active"
ARCHIVED = "archived", "Archived"
class Article(models.Model):
status = models.CharField(max_length=10, choices=Status.choices)
# dorm — usa enum.Enum + EnumField
import enum
class Status(enum.Enum):
ACTIVE = "active"
ARCHIVED = "archived"
class Article(dorm.Model):
status = dorm.EnumField(Status, default=Status.ACTIVE)
# O ENUM nativo PG (4.0+):
# status = dorm.EnumField(Status, native=True, type_name="article_status")
Forms¶
Django tiene ModelForm. dorm no tiene forms intencionalmente
— el target es 99% FastAPI / Litestar / aiohttp donde la
validación va a Pydantic. Para schemas input/output usa
dorm.contrib.pydantic:
from dorm.contrib.pydantic import (
schema_for, create_schema_for, update_schema_for,
)
AuthorOut = schema_for(Author)
AuthorCreate = create_schema_for(Author) # POST body
AuthorUpdate = update_schema_for(Author) # PATCH body, todo opcional
Admin¶
Django tiene contrib.admin. dorm no tiene admin built-in. El
target FastAPI tiende a sqladmin o dashboard custom. Para
generar JSON Schema desde modelos (input para tools de admin
externas) usa dorm export-json-schema --out schemas/.
select_for_update / señales / migraciones¶
Mismos APIs que Django. Operaciones de migración nuevas en 4.0
que Django no tiene: AddFieldOnline, BackfillBatch,
SetNotNullOnline, CreateMaterializedView,
CreatePartitionedTable, CreatePGEnum.
Multi-tenancy¶
| Django | dorm |
|---|---|
django-tenants (3rd party, schema) |
dorm.contrib.tenants (built-in, schema) |
| Manager middleware con filter manual | dorm.contrib.tenants_row.TenantModel (4.0+) |
GIS¶
| Django | dorm |
|---|---|
contrib.gis.db.models.PointField |
dorm.contrib.gis.PointField (4.0+) |
contrib.gis.db.models.PolygonField |
dorm.contrib.gis.PolygonField (4.0+) |
__intersects, __within, __contains, __distance_lte |
mismo, en dorm.contrib.gis |
Tooling dev¶
| Django | dorm |
|---|---|
django-stubs (mypy plugin) |
djanorm-mypy (paquete hermano) |
pytest-django |
pytest-djanorm (paquete hermano) |