Skip to content

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:

MethodPurpose
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 UPDATE

If 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 */ });