perf: precompute field.relation_type and slim RelationsManager init#1651
Merged
collerek merged 1 commit intoperf/relationproxy-getattributefrom May 5, 2026
Merged
Conversation
Three micro-optimizations on the per-Model.__init__ relation bookkeeping path: 1. Precompute ``relation_type`` once in ``BaseField.__init__`` (priority- ordered branch on ``is_multi`` / ``is_through`` / ``virtual`` / ``is_relation``). ``RelationsManager._add_relation`` reads ``field.relation_type`` directly instead of calling ``_get_relation_type(field)`` per relation per Model.__init__. Saves ~100k method calls per all_with_related scenario. ``_get_relation_type`` deleted. ``RelationType`` is lazy-imported via a module-level ``_relation_type_cls()`` helper to break the circular ``ormar.relations`` → ``relation_manager`` → ``utils`` → ``foreign_key`` → ``base`` chain. 2. Drop ``RelationsManager._related_fields`` and ``_related_names`` attributes. ``__contains__`` now checks ``self._relations`` (already keyed by field name). Removes a per-init list-comp and two ``STORE_ATTR`` ops. 3. ``RelationsManager._add_relation`` reads ``field.relation_type`` directly; an ``assert`` narrows ``Optional[RelationType]`` for mypy and documents the invariant (only relation/through fields reach this path). Behavior unchanged. 628 tests pass at 100 % coverage. Benchmark deltas (median, pytest-benchmark): - test_initializing_models[250] -7.8 % - test_get_all[1000] -7.1 % - test_initializing_models_with_related_models[10] -5.7 % - test_initializing_models[500] -1.7 % - I/O-dominated single-row queries within noise cProfile (all_with_related): scenario 5.481 s -> 5.328 s (-2.8 %). ``_get_relation_type`` no longer in the trace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three micro-optimizations on the per-
Model.__init__relation-bookkeeping path. Stacked on top of #1650 (the__getattribute__cleanup).field.relation_typeinBaseField.__init__. The branch onis_multi/is_through/virtual/is_relationruns once at field-declaration time.RelationsManager._add_relationthen readsfield.relation_typedirectly instead of calling_get_relation_type(field)per relation perModel.__init__. Saves ~100 k method calls perall_with_relatedscenario;_get_relation_typedeleted.RelationTypeis lazy-imported via a module-level_relation_type_cls()helper to break the circularormar.relations→relation_manager→utils→foreign_key→basechain.RelationsManager._related_fields/_related_names.__contains__now checks the_relationsdict (already keyed by field name). Removes a per-init list-comp + twoSTORE_ATTRops._add_relationreadsfield.relation_typedirectly. Anassertnarrows theOptionalfor mypy and documents the invariant (only relation/through fields reach this path).Behavior unchanged.
Benchmark deltas (median, pytest-benchmark, --warmup, 10+ rounds)
test_initializing_models[250]test_get_all[1000]test_initializing_models_with_related_models[10]test_initializing_models[500]get_one/firstcProfile (
all_with_relatedscenario): 5.481 s → 5.328 s = −2.8 %._get_relation_typeno longer in the trace.Test plan
make pre-commit— clean (mypy strict)make coverage— 628 passed, 25 skipped, 100.00 % coverageall_with_relatedconfirms_get_relation_typeno longer in top-25 by tottimetests/test_relations/,tests/test_queries/test_queryproxy_on_m2m_models.py,test_aggr_functions.py,test_reverse_fk_queryset.pyNotes
perf/relationproxy-getattribute) — once that merges intocheck-optimisations-and-rs, the diff here will rebase to just the three files shown._resolve_relation_typewas simplified during review from function-attribute caching to a module-level_RELATION_TYPEvariable +global(idiomatic, no# type: ignoreneeded)._to_removesentinel change (None instead of always-emptyset()) was reverted during review — the saving (~7 µs / scenario) didn't justify the added Optional type + assert + conditional checks.🤖 Generated with Claude Code