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 abulk_create; ahora usa nombres + omite M2M.InboxRecorddocstring exigeMeta.unique_together = [("message_id", "handler_name")]en subclase concreta — sin UNIQUE en DB los duplicados concurrentes se cuelan.Saga.run()dentro deatomic()exterior loguea WARNING — los commits de cada step se vuelven savepoints y pierden durabilidad si la transacción externa hace rollback.anonymize_modelahora usa cursor pagination (pk__gt=last_pk) en lugar de offset — O(N) en tablas millonarias.two_phase_commitrechazaatomic()anidados con RuntimeError claro.@temporaldocstring 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¶
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¶
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="".
Tier 4 — DX¶
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¶
Tier 13 — sugar ampliado¶
Tier 14 — slow transactions¶
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.