Saltar a contenido

Novedades en 4.3

Versión menor. Sin cambios incompatibles vs 4.2 — toda novedad es opt-in.

Bugs corregidos (release-blocker)

  • Manager.upsert(returning=True) pasaba objetos Field a bulk_create; ahora usa nombres + omite M2M.
  • InboxRecord docstring exige Meta.unique_together = [("message_id", "handler_name")] en subclase concreta — sin UNIQUE en DB los duplicados concurrentes se cuelan.
  • Saga.run() dentro de atomic() exterior loguea WARNING — los commits de cada step se vuelven savepoints y pierden durabilidad si la transacción externa hace rollback.
  • anonymize_model ahora usa cursor pagination (pk__gt=last_pk) en lugar de offset — O(N) en tablas millonarias.
  • two_phase_commit rechaza atomic() anidados con RuntimeError claro.
  • @temporal docstring advierte que bulk_create / bulk_update / queryset.update / queryset.delete saltan post_save / post_delete y no llenan la tabla temporal.

Tier 1 — estratégico

UPSERT cross-vendor

Model.objects.upsert(
    [Model(slug="a", score=1), Model(slug="b", score=2)],
    unique_fields=["slug"],
)

Sugar sobre bulk_create(update_conflicts=True). update_fields por defecto incluye todo excepto PK + unique.

Tablas temporales

from dorm.contrib.temporal import temporal, as_of

@temporal
class Article(dorm.Model):
    title = dorm.CharField(max_length=200)

snapshot = as_of(Article, t)
history = Article.history.filter(pk=42)

Tabla hermana <Model>Temporal con valid_from / valid_to / operation.

dorm.contrib.tasks — Celery-lite

from dorm.contrib.tasks import TaskQueue, task

queue = TaskQueue(model=BackgroundTask, channel="tasks")

@task(queue, name="send-welcome")
def send_welcome(user_id: int) -> None: ...

with dorm.transaction.atomic():
    user = User.objects.create(...)
    send_welcome.delay(user.id)

queue.run()

Sobre outbox + LISTEN/NOTIFY. Polling fallback si no hay LISTEN/NOTIFY.

Tier 2 — tipos extra

from dorm.contrib.extra_fields import (
    MoneyField, SemverField, PhoneField, ColorField, JSONSchemaField
)
  • MoneyField(currency="EUR") — Decimal + ISO-4217.
  • SemverField() — gramática SemVer 2.0.0.
  • PhoneField() — E.164.
  • ColorField()#RRGGBB / #RRGGBBAA.
  • JSONSchemaField(schema={...})JSONField + JSON Schema en asignación.

Tier 3 — async/sync

SyncDataLoader

loader = SyncDataLoader(lambda ids: Author.objects.in_bulk(ids))
for book in books:
    loader.load(book.author_id)
loader.flush()

BackgroundTasks

bg = BackgroundTasks(concurrency=8)
bg.add(send_email, user.email)
await bg.run()

Tier 4 — transacciones distribuidas

Saga

from dorm.contrib.saga import Saga, Step

saga = Saga(steps=[
    Step("reserve", reserve, release),
    Step("charge", charge, refund),
])
run = saga.run({"order_id": 42})

Two-phase commit (PG)

from dorm.contrib.two_phase import two_phase_commit

with two_phase_commit(["primary", "warehouse"]) as txn:
    txn.execute("primary", "INSERT INTO orders ...")

Inbox

from dorm.contrib.inbox import InboxRecord, idempotent

@idempotent(Inbox)
def handle_payment(message_id: str, payload: dict): ...

Tier 5 — observabilidad / performance

  • Plan-drift history(tag=None) + clear_history — ring acotado.
  • dorm_pool_warmup_seconds{alias} Prometheus gauge.
  • RowCache(Model, maxsize=...) — LRU con auto-invalidación.
  • suggest_fix(template, model_hint=...) — N+1 detector con sugerencia específica.

Tier 6 — DX

dorm init --template fastapi-postgres
dorm init --template litestar-sqlite

Tier 7 — azúcar

F("nick").coalesce("anon")
subtree_filter(Node, root_pk=1, where_sql="is_active = ?", where_params=[True])

Tier 8 — seguridad

from dorm.contrib.anonymizer import anonymize_model

anonymize_model(User, {
    "email": "random_email",
    "phone": "random_phone",
    "notes": "redact",
})

from dorm.contrib.auth.tokens import rotate_short_lived_token
new_tok, old_tok = rotate_short_lived_token(current_token)

from dorm.db.utils import add_sensitive_pattern
add_sensitive_pattern("ssn", "iban")

Tier 2 — TaskQueue ampliado

from dorm.contrib.tasks import task, run_cron_tick

@task(queue, name="reindex", priority=5)
def reindex(): ...

@task(queue, name="nightly", cron="0 3 * * *")
def nightly_job(): ...

reindex.delay(eta=datetime.utcnow() + timedelta(minutes=10))
reindex.delay(delay_seconds=30, priority=1)
run_cron_tick(queue)

Helpers: retry_with_backoff, dead_letters.

Tier 3 — migraciones

  • DORM-M005 lint: operación irreversible.
  • DORM-M006 lint: RunSQL destructivo con reverse_sql="".
from dorm.migrations.operations import (
    AddCheckConstraintOnline,
    SeederMigration,
)

Tier 4 — DX

DORM_PROFILE=prod dorm migrate
# Carga settings/prod.py transparentemente.

Tier 6 — campos nuevos

class User(dorm.Model):
    id = dorm.UUIDField(primary_key=True, version=7)  # UUIDv7 time-ordered
    network = IPRangeField(null=True)
    tz = TimezoneField(default="UTC")
    completion = PercentageField()
    country = CountryField()
    slug = dorm.SlugField(default=autoslug("name"))

Tier 7 — permisos + rate limit

from dorm.contrib.permissions import requires, requires_any
from dorm.contrib.rate_limit import TokenBucket, rate_limited

@requires("article.edit", scope="article")
def edit(user, article): ...

bucket = TokenBucket(rate_per_second=10, burst=20)

@rate_limited(bucket, key=lambda req: req.ip)
def signup(req): ...

Tier 8 — concurrencia

from dorm.contrib.concurrency import (
    named_lock, SerializableSnapshot, with_optimistic_lock,
)

with named_lock("nightly-report"):
    run_nightly_report()

SerializableSnapshot().run(transfer_money)
with_optimistic_lock(account)

Tier 9 — vector + FTS rerank

from dorm.contrib.pgvector.reranker import rerank

results = rerank(Document, vector_field="embedding",
                 text_field_tsv="search_vector",
                 query_vector=embed("ML"), query_text="ML")

Tier 11 — active/passive routing

from dorm.contrib.active_passive import ActivePassiveRouter

DATABASE_ROUTERS = [
    ActivePassiveRouter(aliases=["a", "b"], probe_seconds=10.0),
]

Plus await awarmup_pool(target=5).

Tier 12 — serialización

from dorm.contrib.serializers import (
    stream_msgpack, avro_schema_for, openapi_schema_for,
)

Tier 13 — sugar ampliado

F("age").between(18, 65)
User.objects.exists_or_create(email="x@y", defaults={...})

Tier 14 — slow transactions

from dorm.contrib import slow_tx
slow_tx.install(threshold_ms=500)

Logs WARNING + OTel event en bloques atomic() lentos.

Diferido a v4.4

  • Backends MSSQL / ClickHouse / Snowflake / Spanner.
  • Composite Foreign Keys.
  • Helpers TimescaleDB.
  • dorm shell --notebook (kernel Jupyter).
  • Factories en pytest-djanorm.
  • Squash de migraciones que preserve RunPython.

Subir 4.2 → 4.3 no necesita modificar código.