Module: EcsRails::Preloading

Included in:
Entity
Defined in:
lib/ecs_rails/preloading.rb

Overview

Batch component loads so a list view issues a bounded number of queries instead of one per component per row.

Implements RFC-0011, decided by ADR-0012. Surfaced by the demo, where the 2-post index fired 14 queries (one per component per row).

Post.published.includes_components # all declared components User.includes_components(Name, Email) # a named subset Post.with_component(PublishState).includes_components(Title, Body)

Extended into EcsRails::Entity, so these are class methods on every entity class. Like Querying, ActiveRecord delegates class methods to relations, so User.where(...).includes_components(...) chains: the method runs with all returning the current relation, which already carries the entity-model scope.

The finding this rests on (ADR-0012)

This method builds no preload machinery. Each component is a has_one (RFC-0004), and RFC-0006’s lazy reader overrides that has_one but calls super, reaching it underneath — so ActiveRecord’s own preload already batches component loads AND the lazy reader still returns a virtual instance for an entity with no row (the has_one preloads to nil-and-loaded, the reader builds the virtual). includes_components is a thin, discoverable wrapper over preload(*association_names); the regression tests in spec/preloading_spec.rb pin the native path so it cannot silently break.

Instance Method Summary collapse

Instance Method Details

#includes_components(*component_classes) ⇒ ActiveRecord::Relation

Preloads the given components and returns a chainable relation.

With no arguments, preloads every declared component of the entity — walking inherited declarations (RFC-0004) via #components — which is the one affordance the raw preload(:a, :b, :c) cannot offer.

Takes component classes, not association symbols, consistent with with_component / add / has?. Each must be a component declared on the entity; otherwise EcsRails::InvalidComponent naming it, rather than leaking ActiveRecord’s AssociationNotFoundError and the has_one abstraction the gem otherwise hides (ADR-0012).

Uses preload (separate queries), never includes/eager_load: one extra query per component, predictable, and no surprise JOIN that changes row identity or interacts with with_component’s EXISTS. A developer who wants a JOIN calls eager_load on the association names directly.

Built from all — like Querying — so it chains onto any prior scope and keeps the entity-model default scope (ADR-0002/ADR-0011).

Examples:

Every declared component

Post.published.includes_components

A named subset, chained onto a component filter

Post.with_component(PublishState).includes_components(Title, Body)

Parameters:

  • component_classes (Array<Class<EcsRails::Component>>)

    components to preload; empty preloads every component the entity declares

Returns:

  • (ActiveRecord::Relation)

    chainable, entity-model scoped

Raises:

See Also:



63
64
65
66
67
68
69
# File 'lib/ecs_rails/preloading.rb', line 63

def includes_components(*component_classes)
  declared = components
  targets = component_classes.empty? ? declared : component_classes
  targets.each { |component_class| ecs_validate_declared_component!(component_class, declared) }

  all.preload(*targets.map { |component_class| ecs_component_association_name(component_class) })
end