|
ORM C++
|
orm-cxx supports explicit one-level collection relations with orm::OneToMany<T> and orm::ManyToMany<T>. Collection fields are model metadata: they are not columns of the model's base table, are never written by insert, and are loaded only when a query explicitly requests them. Mapping descriptors use compile-time member pointers and FixedString names, and the complete relation graph is validated by orm::Schema<Models...>.
A collection field uses one of the ORM wrappers rather than std::vector<T>:
Both wrappers expose:
A default-constructed wrapper is empty and has isLoaded() == false. After an explicit include, it has isLoaded() == true, including when the database contains no related rows. This distinction lets callers tell "not requested" from "requested and empty".
Changing values() or an element of the wrapper changes only the C++ object. The ORM does not cascade-save or synchronize a collection. Use link and unlink for relation mutations, and insert or update endpoint models separately.
OneToMany is the inverse of an existing to-one field on the child. Declare the collection descriptor in the parent's static relations metadata and name the child's to-one field with mappedBy:
mappedBy is required. The collection side is always selected by the typed member pointer &Author::books. When the target type is complete, prefer .mappedBy<&Book::author>(). The FixedString fallback .mappedBy<"author">() is necessary in the common layout above because Book is still incomplete when Author::relations is defined. In either form the field must be a compatible to-one relation whose target is Author. The Book table owns the foreign-key columns already generated for Book::author; no hidden column or additional relation table is created for Author::books.
Use std::optional<Author> for the child field when the relation must be nullable:
This choice also determines whether unlink(author, "books", book) is valid.
The owning side declares the junction table with through:
through<"user_roles">() is required on the owning side. The inverse side is optional; when present, mappedBy<&User::roles>() points to the owning collection field. Both sides support include, collection predicates, link, and unlink.
Both endpoints and every other relation target must belong to the database's closed schema:
The junction table contains only the complete primary keys of both endpoints. Every junction column is NOT NULL; all columns together form its composite primary key. It has one foreign key to each endpoint with ON DELETE CASCADE. Deleting an endpoint therefore deletes only its junction rows, never the model at the other endpoint. No surrogate id, ordering column, or payload columns are added.
ownerColumns and targetColumns are optional. By default each junction column is named <endpoint-table>_<primary-key-column>. Database column names, including columns_names mappings, are used for the primary-key part.
For a composite primary key there must be exactly one junction column name per primary-key column, in primary-key order:
Self-referencing many-to-many mappings must specify distinct owner and target column names explicitly, because the default names would collide.
Create endpoint tables first, then create junction tables from the owning model:
createRelationTables<T>() creates only owning many-to-many junction tables declared by T. A one-to-many relation needs no relation table, and calling the method for a model that has only inverse mappings is a no-op. Endpoint tables must already exist before junction-table DDL can reference them.
Drop junction tables before endpoint tables:
Relation-table creation and deletion are idempotent. They remain explicit; createTable<T>() and deleteTable<T>() do not recursively alter relation tables. Connecting to SQLite enables PRAGMA foreign_keys=ON automatically.
Use endpoint objects with complete, non-null primary-key values:
For many-to-many, link inserts one junction row and unlink deletes it. Both are idempotent: they return 1 when the stored relation changed and 0 when it was already in the requested state. Foreign-key enforcement rejects missing endpoint rows.
For one-to-many, pass the parent first and child last:
link assigns the child's mapped foreign key and may move the child from a different parent. unlink stores SQL NULL in that foreign key. It throws std::invalid_argument when the mapped child relation is non-nullable. As with many-to-many, the return value is 1 only when the stored foreign key changed and 0 when it was already in the requested state.
These operations read primary keys only. They do not insert, update, or cascade-save either endpoint. All primary-key components must be present and non-null, including every component of a composite key. For an auto-increment model, select the inserted row to obtain its generated key before calling link.
Collections are not joined into the main model query. Request one level explicitly with Query<T>::include:
Duplicate includes are idempotent. The ORM first runs the unchanged parent query, then one or more parameter-bounded batched queries per included field and groups rows by the complete parent primary key. It never issues one relation query per parent.
LIMIT, OFFSET, DISTINCT, and parent ordering apply only to the main query, so each returned parent's collection is complete. Large primary-key sets are split into batches that respect the selected backend's parameter limit. Element order inside a collection is not guaranteed.
Includes are available only on full-model Query<T>, not on flat ProjectionQuery results. Only one include level is supported: collection fields on included elements remain unloaded. disableJoining() still controls to-one hydration and is propagated to collection elements, but does not cancel an explicit collection include.
Collection predicates use correlated EXISTS subqueries:
The predicate passed to any or none is relative to the collection's target model. It may use scalar target fields and existing one-level to-one paths. any renders correlated EXISTS, none renders NOT EXISTS, and exists checks only whether the collection is non-empty. Values remain SOCI bind parameters.
Filtering does not load the collection. Add include("books") separately when the result objects also need the elements. Nested collection predicates are not supported.
A collection is not a flat column path. Expressions such as col("books.title") are rejected, as are collection fields in ORDER BY, GROUP BY, projections, aggregate expressions, and Update assignments.
The complete lifecycle below uses both relation kinds. Endpoint rows are inserted first, links are written explicitly, and collection values are loaded only by include:
For generated primary keys, insert the endpoint and select it back before step
A complete program containing the model declarations and this lifecycle is available in examples/relations.cpp. It is built as the relations-example CMake target.
Schema operations, link, unlink, and collection queries participate in the database's current explicit transaction. They do not start or commit a private transaction.
An included select uses multiple SQL statements. It does not create an automatic snapshot around them; wrap the select in an explicit transaction when the parent rows and included collections must be observed consistently.
Deleting a one-to-many parent is blocked by the child's foreign key unless the application detaches or removes the children first. The ORM does not generate cascade deletion for child rows.
Instantiating orm::Schema<Models...> rejects invalid collection mappings at compile time, including:
Collection mapping cycles are supported because relation targets are resolved lazily from cached metadata rather than recursively copied.
Lazy loading, nested includes, nested collection predicates, automatic collection synchronization, cascade-save, ordered collections, junction payload models, and schema migrations are outside the current contract.
Adding relation metadata does not migrate an existing database. Apply schema changes manually:
Test migration SQL on a copy of the database before enabling the new mapping in an application release.