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
#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
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++)
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)
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)
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)
ConnectionPool pool{factory, cfg};
pool.warmup();- Pool reduces overhead
- Warmup avoids slow first query
3.4 Create the repository
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
auto id = repo.create(User{0, "Bob", "gaspardkirira@example.com", 30});- Uses
toInsertParams()internally - Executes an INSERT
- Returns the inserted id
4.2 Update
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)
if (auto u = repo.findById(id)) { ... }- Returns
std::optional<User> - Uses
fromRow()internally - If row missing -> empty optional
4.4 Delete
repo.removeById(id);- Deletes the row by id
- Returns affected rows (commonly 1)
5. Required SQL Table
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::anyin 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