Skip to content

No eager-loading API: relation access across N records issues N queries #563

Description

@Yaraslaut

Lightweight is the only modern C++ library that models associations at all — BelongsTo, HasMany, HasOneThrough, HasManyThrough, CompositeForeignKey, with ddl2cpp inferring them from a live schema. What is missing is the mechanism every other ecosystem pairs with them: a way to load a relation for a set of records in one query.

Current behaviour

  • Query<Record>().All() calls ConfigureRelationAutoLoading per record in a loop (DataMapper.hpp:2458-2463), installing a lazy loader on each.
  • Touching the relation on N rows issues N queries — LoadHasMany runs one Query<Other>(...) per owner (DataMapper.hpp:2653-2666).
  • LoadRelations(record) is eager but still per record, and still one query per relation member (DataMapper.hpp:2825-2884).
  • HasMany::Each streams rather than materializing (HasMany.hpp:249-259), which bounds memory but not query count.

There is no .With() / .Include() / preload anywhere in QueryBuilders.hpp or DataMapper.hpp. The only mitigation is manual — WhereIn plus explicit joins, which is what the shipped Chinook example does (src/examples/test_chinook/main.cpp:154-161).

Prior art

  • Django: select_related (join) / prefetch_related (second query + in-memory stitch); 6.1 added FETCH_RAISE to turn an unintended lazy load into an exception
  • EF Core: .Include() / .ThenInclude()
  • Hibernate: fetch joins + @BatchSize
  • ActiveRecord: .includes()
  • SeaORM 2.0: find_with_related

Suggested shape

auto albums = dm.Query<Album>()
                .With<&Album::Track_1>()      // one extra SELECT ... WHERE album_id IN (...)
                .All();

Implementation sketch: after materializing the outer result set, collect the primary keys, issue one WhereIn query per requested relation, and distribute the rows into the per-record loaders instead of installing lazy ones. The WhereIn machinery and the per-relation select builders (BuildHasManySelectQuery, BuildHasManyThroughSelectQuery) already exist.

A cheaper interim step: a debug-mode counter on SqlLogger that warns when more than N lazy loads fire for the same relation within one scope — the N+1 detector before the N+1 fix. That composes well with #555 (metrics collector).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions