Skip to content

ORM Example Guide: Repository CRUD (Full)

This guide explains the repository_crud_full example step by step.

Goal: - Define a domain model (User) - Teach the ORM how to map database rows to User - Use BaseRepository<User> to perform full CRUD: - Create - Read - Update - Delete

This is the core pattern for building real applications with Vix ORM.

1. What this example demonstrates

You will learn:

  • How to define a model struct (User)
  • How to specialize vix::orm::Mapper<T> for your model
  • How fromRow() works (row -> object)
  • How toInsertParams() / toUpdateParams() work (object -> params)
  • How to use BaseRepository<T> with a connection pool
  • How CRUD operations return ids and results

2. Full Example Code

cpp
#include <vix/orm/orm.hpp>

#include <any>
#include <cstdint>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

struct User
{
  std::int64_t id{};
  std::string name;
  std::string email;
  int age{};
};

namespace vix::orm
{
  template <>
  struct Mapper<User>
  {
    static User fromRow(const ResultRow &row)
    {
      User u{};
      u.id = row.getInt64Or(0, 0);
      u.name = row.getStringOr(1, "");
      u.email = row.getStringOr(2, "");
      u.age = static_cast<int>(row.getInt64Or(3, 0));
      return u;
    }

    static std::vector<std::pair<std::string, std::any>>
    toInsertParams(const User &u)
    {
      return {
          {"name", u.name},
          {"email", u.email},
          {"age", u.age},
      };
    }

    static std::vector<std::pair<std::string, std::any>>
    toUpdateParams(const User &u)
    {
      return {
          {"name", u.name},
          {"email", u.email},
          {"age", u.age},
      };
    }
  };
} // namespace vix::orm

int main(int argc, char **argv)
{
  using namespace vix::orm;

  const std::string host = (argc > 1 ? argv[1] : "tcp://127.0.0.1:3306");
  const std::string user = (argc > 2 ? argv[2] : "root");
  const std::string pass = (argc > 3 ? argv[3] : "");
  const std::string db = (argc > 4 ? argv[4] : "vixdb");

  try
  {
    auto factory = make_mysql_factory(host, user, pass, db);

    PoolConfig cfg;
    cfg.min = 1;
    cfg.max = 8;

    ConnectionPool pool{factory, cfg};
    pool.warmup();

    BaseRepository<User> repo{pool, "users"};

    // Create
    const std::int64_t id = static_cast<std::int64_t>(
        repo.create(User{0, "Bob", "gaspardkirira@example.com", 30}));
    std::cout << "[OK] create -> id=" << id << "\n";

    // Update
    (void)repo.updateById(id, User{id, "Adastra", "adastra@example.com", 31});
    std::cout << "[OK] update -> id=" << id << "\n";

    // (Optional) Read back
    if (auto u = repo.findById(id))
    {
      std::cout << "[OK] findById -> name=" << u->name
                << " email=" << u->email
                << " age=" << u->age << "\n";
    }

    // Delete
    (void)repo.removeById(id);
    std::cout << "[OK] delete -> id=" << id << "\n";

    return 0;
  }
  catch (const DBError &e)
  {
    std::cerr << "[DBError] " << e.what() << "\n";
    return 1;
  }
  catch (const std::exception &e)
  {
    std::cerr << "[ERR] " << e.what() << "\n";
    return 1;
  }
}

3. Step by Step Explanation

3.1 Define your model

cpp
struct User
{
  std::int64_t id{};
  std::string name;
  std::string email;
  int age{};
};

This is your domain object. It represents one row in the database table.

3.2 Teach ORM how to map User

Vix ORM uses a Mapper<T> specialization. This is how the ORM becomes type-aware.

fromRow() (DB -> C++)

cpp
static User fromRow(const ResultRow &row)
{
  User u{};
  u.id = row.getInt64Or(0, 0);
  u.name = row.getStringOr(1, "");
  u.email = row.getStringOr(2, "");
  u.age = static_cast<int>(row.getInt64Or(3, 0));
  return u;
}
  • Column index 0 -> id
  • Column index 1 -> name
  • Column index 2 -> email
  • Column index 3 -> age

The Or methods provide defaults if the value is NULL or missing.

Production tip: If you require strict schema, use the non-Or getters (if available) and fail fast.

toInsertParams() (C++ -> DB)

cpp
static std::vector<std::pair<std::string, std::any>>
toInsertParams(const User &u)
{
  return {
      {"name", u.name},
      {"email", u.email},
      {"age", u.age},
  };
}

This returns column/value pairs used by repo.create().

Important: - id is not included because it is auto-generated by MySQL.

toUpdateParams() (C++ -> DB)

cpp
static std::vector<std::pair<std::string, std::any>>
toUpdateParams(const User &u)
{
  return {
      {"name", u.name},
      {"email", u.email},
      {"age", u.age},
  };
}

This returns column/value pairs used by repo.updateById().

3.3 Create the pool (reusable DB connections)

cpp
ConnectionPool pool{factory, cfg};
pool.warmup();
  • Pool reduces overhead
  • Warmup avoids slow first query

3.4 Create the repository

cpp
BaseRepository<User> repo{pool, "users"};

This binds the repository to:

  • type: User
  • table: users
  • pool: pool

Now you can call CRUD methods directly.

4. CRUD Operations Explained

4.1 Create

cpp
auto id = repo.create(User{0, "Bob", "gaspardkirira@example.com", 30});
  • Uses toInsertParams() internally
  • Executes an INSERT
  • Returns the inserted id

4.2 Update

cpp
repo.updateById(id, User{id, "Adastra", "adastra@example.com", 31});
  • Uses toUpdateParams() internally
  • Updates the row with that id
  • Returns how many rows were updated (commonly 1)

4.3 Read (Optional)

cpp
if (auto u = repo.findById(id)) { ... }
  • Returns std::optional<User>
  • Uses fromRow() internally
  • If row missing -> empty optional

4.4 Delete

cpp
repo.removeById(id);
  • Deletes the row by id
  • Returns affected rows (commonly 1)

5. Required SQL Table

sql
CREATE TABLE IF NOT EXISTS users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  age INT NOT NULL
);

6. Production Notes

Recommended improvements for real apps:

  • Add a unique index on email
  • Add validation before create/update
  • Add transactions when doing multiple operations
  • Use UnitOfWork for multi-table business operations
  • Avoid using std::any in hot paths if you need extreme performance (use typed binders when available)

Summary

You learned:

  • How to define a model (User)
  • How Mapper<User> makes ORM type-safe
  • How BaseRepository<User> provides CRUD APIs
  • How create/update/find/delete work internally