← Persistence

PERSISTENCE FEATURE

N+1 Without Magic

No magic, no lazy loading, no bloated entity graphs. A screen asks for one slice, the ORM hydrates exactly that slice.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

Semitexa avoids N+1 by letting each screen define the exact table slice it needs, then batch-loading explicit relations for the whole result set.

How it works

You can model multiple resource abstractions over the same table. A slim list resource selects only the required columns, and the ResourceModel relation loader batch-loads explicit relations for all hydrated rows in one pass instead of per-row lazy lookups.

Why it matters

This removes the usual ORM trade-off between over-fetching giant entities and hiding extra queries behind lazy loading. The fetch plan is explicit, reviewable, and stable under load.

Key concepts

N+1
A query anti-pattern where one base query triggers one extra query per returned row.
ResourceModelRelationLoader
Batch-loads explicit relations once for the whole result set on the ResourceModel layer.
Resource slice
A narrow resource abstraction over a table containing only the columns and relations required by one use case.

No Magic, No Problem

N+1 solved by shaping the model, not hiding the database

Semitexa does not need lazy-loading theater to avoid N+1. A list page can define a small resource slice for one table, then batch-load only the relations it really needs for that screen.

The usual pain

  • In other ORMs, fat-entity models often over-fetch columns first, then hide follow-up queries behind property access.
  • Implicit relation loading can make local code look simple while database traffic quietly scales with the number of rows on screen.
  • Traditionally this is handled with heavier fetch plans, special loading modes, or proxy behavior that becomes hard to reason about.
4 base columns for a product card
2 queries for list + relation batch
0 lazy loads hidden in view code

Traditional ORM

Whole Product entity + proxy relations

1 base query + 12 lazy category/review lookups

Query count grows with the number of rendered rows.

Semitexa ORM

ProductCardResource + optional ProductCardWithReviewsResource

1 product slice query + 1 batch relation query

The fetch plan stays explicit and stable.

Selected column Why it belongs in the slice Ownership
id Needed for identity and links. Card slice
name Rendered on the list card. Card slice
price Displayed immediately in the UI. Card slice
category_id Enough to batch-resolve category when needed. Card slice
ResourceModelRelationLoader resource slice no lazy loading #[FromTable] batch relations

Verified against Semitexa Ultimate 2026.09.19.1020

N+1 Without Magic

Instead of a single fat entity with lazy-loaded relations, Semitexa uses screen-specific resource slices that declare exactly the columns they need. Relations are batch-loaded explicitly.

How it works

A screen defines its own ResourceModel subclass — for example ProductCardResource — pointing at the same table via #[FromTable] but declaring only the four columns it renders. If the screen also needs related data, ResourceModelRelationLoader batch-loads all related rows for the full result set in a second query, never one per row. There are no proxies and no lazy property access.

Why this matters

Implicit lazy loading makes local code look simple while database traffic scales silently with row count. An explicit resource slice keeps the fetch plan visible in code review, stable in production, and free from the runtime surprises that fat-entity ORMs introduce when a new template starts touching a previously unloaded property.

© Harold Abelson: "Programs must be written for people to read, and only incidentally for machines to execute."

Fat Entity Implementation slice
<?phpdeclare(strict_types=1);namespace LegacyOrm\Entity;final class Product{    public string $id;    public string $name;    public string $description;    public string $status;    public string $price;    public string $createdAt;    public string $updatedAt;    private ?Category $category = null;    /** @var list<Review>|null */    private ?array $reviews = null;    public function getCategory(): ?Category    {        // Hidden database query on first access.        if ($this->category === null) {            $this->category = $this->lazyLoadCategory();        }        return $this->category;    }    /** @return list<Review> */    public function getReviews(): array    {        // Another hidden query per row.        if ($this->reviews === null) {            $this->reviews = $this->lazyLoadReviews();        }        return $this->reviews;    }}

Fetch Plan Rules

Why Semitexa stays explicit

A screen is free to define its own resource abstraction for the same table instead of inheriting one bloated canonical entity.

The slim slice contains only the columns that the screen actually renders.

If relations are needed, the ResourceModel relation loader batch-loads them for the whole result set instead of firing lookups row by row.

Because there is no lazy loading, the fetch plan is visible in the code review and stable in production.

How it works

You can model multiple resource abstractions over the same table. A slim list resource selects only the required columns, and the ResourceModel relation loader batch-loads explicit relations for all hydrated rows in one pass instead of per-row lazy lookups.

Why it matters

This removes the usual ORM trade-off between over-fetching giant entities and hiding extra queries behind lazy loading. The fetch plan is explicit, reviewable, and stable under load.

Key concepts

N+1
A query anti-pattern where one base query triggers one extra query per returned row.
ResourceModelRelationLoader
Batch-loads explicit relations once for the whole result set on the ResourceModel layer.
Resource slice
A narrow resource abstraction over a table containing only the columns and relations required by one use case.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal