Appearance
Models
Models extend framework::database::model<Derived> and provide an active-record pattern for database access. Each model maps to a database table and encapsulates the logic for reading, writing, and validating records.
Defining a Model
Create a struct that inherits from model<Derived>. You must implement three pure virtual methods to map between the database row and your C++ struct.
cpp
#include <framework/database/orm.hpp>
using namespace framework;
using namespace framework::database;
struct User : model<User> {
static inline const std::string table = "users";
std::string name;
std::string email;
// Maps a database row to this struct's fields
void from_row(const boost::mysql::row_view& row) override {
name = row.at(1).as_string();
email = row.at(2).as_string();
}
// Maps this struct's fields back to database columns
std::vector<boost::mysql::field> to_fields() const override {
return {name, email};
}
// Defines the column order used by to_fields()
std::vector<std::string> column_names() const override {
return {"name", "email"};
}
};Required overrides:
| Method | Purpose |
|---|---|
from_row(row) | Populate fields from a MySQL result row. Index 0 is usually the primary key. |
to_fields() | Return field values in the same order as column_names(). |
column_names() | Return column names in the order they should be inserted/updated. |
The static table member tells the ORM which database table this model maps to.
Primary Key Convention
The ORM assumes the primary key column is id with type BIGINT AUTO_INCREMENT. Access and manage it via:
cpp
// Get the primary key value (0 means new/unsaved)
auto pk = user->get_key();
// Set the primary key (typically after INSERT)
user->set_key(42);
// The model's `key_` member starts at 0 for new records
// `save()` uses get_key() == 0 to decide INSERT vs UPDATEIf your table uses a different primary key column, override the relevant query methods or use where_key() in your queries.
Lifecycle Hooks
Lifecycle hooks are callbacks that fire automatically during save, update, and delete operations. Returning false from a before hook cancels the operation.
Before hooks — return false to abort
cpp
struct User : model<User> {
// Called before any save (create or update). Return false to cancel.
bool on_saving() override { return true; }
// Called only before INSERT. Return false to cancel.
bool on_creating() override { return true; }
// Called only before UPDATE. Return false to cancel.
bool on_updating() override { return true; }
// Called before DELETE. Return false to cancel.
bool on_deleting() override { return true; }
};Use before hooks for validation, setting default values, or enforcing business rules.
After hooks — notification only
cpp
struct User : model<User> {
void on_created() override {} // After INSERT
void on_updated() override {} // After UPDATE
void on_saved() override {} // After INSERT or UPDATE
void on_deleted() override {} // After DELETE
};Use after hooks for logging, cache invalidation, or triggering side effects.
Finding Records
find(db, cb, id) — Find by primary key
cpp
User::find(pool, [](auto ec, auto user) {
if (user) {
fmt::println("Found: {}", user->name);
} else {
fmt::println("User not found");
}
}, 1);find_or_fail(db, cb, id) — Find or error
Same as find(), but calls the callback with an error code instead of a null pointer when the record does not exist.
cpp
User::find_or_fail(pool, [](auto ec, auto user) {
if (ec) { /* handle not found */ }
}, 1);all(db, cb) — All records
Returns every row in the table.
cpp
User::all(pool, [](auto ec, auto users) {
for (auto& user : users) {
fmt::println("User: {}", user->name);
}
});Saving Records
save(db, cb) — Insert or update
If the model's key (get_key()) is 0, an INSERT is performed. Otherwise, an UPDATE is performed on the row with that key.
cpp
// Create a new record
auto user = std::make_shared<User>();
user->name = "Alice";
user->email = "alice@example.com";
user->save(pool, [](auto ec) {
if (!ec) fmt::println("User saved!");
});
// Update an existing record
user->name = "Alice Updated";
user->save(pool, callback);save_with_timestamps(db, cb) — Save with automatic timestamps
Same as save(), but also calls on_timestamp_created() / on_timestamp_updated() so you can set created_at / updated_at columns automatically.
cpp
user->save_with_timestamps(pool, callback);Refreshing Records
fresh(db, cb) — Reload from database
Returns a new model instance with the latest data from the database. The original model is not modified.
cpp
user->fresh(pool, [](auto ec, auto fresh_user) {
fmt::println("Latest name: {}", fresh_user->name);
});refresh(db, cb) — Update in place
Updates the current model's fields with the latest database values.
cpp
user->refresh(pool, [](auto ec) {
fmt::println("Refreshed: {}", user->name);
});Deleting Records
remove(db, cb) — Delete
Permanently deletes the record. If the model has soft deletes enabled, it marks the record as deleted instead.
cpp
user->remove(pool, callback);force_delete(db, cb) — Force delete
Bypasses soft deletes and permanently removes the row.
cpp
user->force_delete(pool, callback);soft_delete(db, cb) — Soft delete
Marks the record as deleted without removing the row. The model must set static constexpr bool soft_deletes = true;:
cpp
struct User : model<User> {
static constexpr bool soft_deletes = true;
// ...
};cpp
user->soft_delete(pool, callback);is_soft_deleted() — Check if soft-deleted
cpp
if (user->is_soft_deleted()) {
fmt::println("This record was soft-deleted");
}on_soft_deleted(column) / on_restored() — Soft delete hooks
cpp
struct User : model<User> {
void on_soft_deleted(const std::string& column) override {
// column is the timestamp column name (e.g., "deleted_at")
}
void on_restored() override {
// Called after restore()
}
};restore(db, cb) — Restore soft-deleted
Reverses a soft delete.
cpp
user->restore(pool, callback);Accessors & Mutators
Accessors transform values when reading from the database. Mutators transform values when writing to the database.
cpp
struct User : model<User> {
// Called when reading a value from the database
boost::mysql::field get_accessor(const std::string& name, boost::mysql::field val) const override {
if (name == "email") {
return val; // transform here (e.g., lowercase)
}
return val;
}
// Called when writing a value to the database
boost::mysql::field get_mutator(const std::string& name, boost::mysql::field val) override {
if (name == "email") {
return val; // transform here
}
return val;
}
};Touch & Push
touch(db, cb) — Update timestamps
Updates the model's timestamp columns without changing any other data.
cpp
user->touch(pool, callback);push(db, cb) — Save model and relations
Saves the model and all registered pushable relations in a single operation.
cpp
user->push(pool, callback);To register a pushable relation:
cpp
template<typename T>
user->register_pushable(related_model);Model Factories
Factories generate model instances with sensible defaults for testing and seeding.
Defining a factory
cpp
using UserFactory = factory<User>;
UserFactory::define([](User& user) {
user.name = "Default Name";
user.email = "default@example.com";
});Creating instances
cpp
UserFactory factory;
// Override specific states
factory.state([](User& user) { user.name = "Admin"; });
// Generate 10 instances
factory.count(10);
// Create in memory only
factory.make([](auto users) { /* 10 User objects */ });
// Create and persist to database
factory.create(pool, [](auto ec, auto users) { /* 10 persisted Users */ });