Database System 0.1.0
Advanced C++20 Database System with Multi-Backend Support
Loading...
Searching...
No Matches
ORM Entity Mapping Tutorial

The database_system ORM lets you describe a table as a C++ class using two macros, ENTITY_FIELD and ENTITY_METADATA. The result is a type-safe entity that can generate its own CREATE TABLE statement, expose typed field accessors, and integrate with the query builders.

This tutorial walks through defining an entity, attaching constraints, modelling relationships, and feeding entity metadata into a query.

Core Concepts

Concept Description
entity_base Abstract base class with virtual table_name(), save(), load(), update(), remove()
ENTITY_FIELD(type, name, constraints) Declares a typed field with metadata
ENTITY_METADATA() Hooks initialize_metadata() into the entity
entity_metadata Runtime container for fields, used to generate SQL
field_constraint Bitset enum: primary_key, auto_increment, not_null, unique, index, default_value
Note
The library currently exposes the ENTITY_FIELD macro plus a manual table_name() override. The optional ENTITY_TABLE() form simply wraps the same idiom.

Step 1: Define an Entity

The example below defines a user_entity with a primary key, a unique username and email, and a not-null age. The ENTITY_FIELD macro creates a typed accessor (e.g. user.username.get(), user.username = "alice") and registers a field_metadata entry.

using namespace database;
using namespace database::orm;
class user_entity : public entity_base
{
public:
using primary_key_type = int64_t;
std::string table_name() const override { return "users"; }
ENTITY_FIELD(int64_t, id,
field_constraint::primary_key | field_constraint::auto_increment)
ENTITY_FIELD(std::string, username,
field_constraint::not_null | field_constraint::unique)
ENTITY_FIELD(std::string, email,
field_constraint::not_null | field_constraint::unique)
ENTITY_FIELD(int32_t, age, field_constraint::not_null)
ENTITY_FIELD(bool, is_active, field_constraint::not_null)
bool save() override { return false; } // implement against your manager
bool load() override { return false; }
bool update() override { return false; }
bool remove() override { return false; }
private:
static inline entity_metadata metadata_{"users"};
};
void user_entity::initialize_metadata()
{
metadata_.add_field(id_metadata_);
metadata_.add_field(username_metadata_);
metadata_.add_field(email_metadata_);
metadata_.add_field(age_metadata_);
metadata_.add_field(is_active_metadata_);
}
Base class for all ORM entities.
Definition entity.h:143
virtual bool update()=0
virtual bool remove()=0
Metadata for entire entities including table mapping and relationships.
Definition entity.h:118
void add_field(const field_metadata &field)
Definition entity.cpp:87
int64_t primary_key_type
std::string table_name() const override
#define ENTITY_METADATA()
Definition entity.h:289
#define ENTITY_FIELD(type, name,...)
Definition entity.h:274

Once defined, you can ask the entity to generate its own DDL:

std::string ddl = u.get_metadata().create_table_sql();
manager->create_query_result(ddl);
virtual const entity_metadata & get_metadata() const =0
std::string create_table_sql() const
Definition entity.cpp:123

Step 2: Constraints

Constraints are bitwise OR'd onto each field. The most common combinations are summarized below.

Constraint Effect
primary_key Marks the column as PRIMARY KEY
auto_increment Backend-specific auto-increment (SERIAL, AUTOINCREMENT, AUTO_INCREMENT)
not_null Adds NOT NULL
unique Adds UNIQUE
index Generates a secondary index in create_indexes_sql()
default_value Field uses its default expression
class product_entity : public entity_base
{
public:
std::string table_name() const override { return "products"; }
ENTITY_FIELD(int64_t, id,
field_constraint::primary_key | field_constraint::auto_increment)
ENTITY_FIELD(std::string, sku, field_constraint::not_null | field_constraint::unique)
ENTITY_FIELD(std::string, description, field_constraint::none)
ENTITY_FIELD(double, price, field_constraint::not_null)
ENTITY_FIELD(int32_t, category_id, field_constraint::not_null | field_constraint::index)
/* save/load/update/remove omitted */
private:
static inline entity_metadata metadata_{"products"};
};

Step 3: Relationships

The ORM models relationships through foreign-key columns plus indexes; join logic lives in the query builder. The example below pairs a post_entity with the user_entity from above using author_id.

class post_entity : public entity_base
{
public:
std::string table_name() const override { return "posts"; }
ENTITY_FIELD(int64_t, id,
field_constraint::primary_key | field_constraint::auto_increment)
ENTITY_FIELD(std::string, title, field_constraint::not_null)
ENTITY_FIELD(std::string, body, field_constraint::none)
ENTITY_FIELD(int64_t, author_id, field_constraint::not_null | field_constraint::index)
ENTITY_FIELD(int64_t, created_at, field_constraint::not_null)
/* save/load/update/remove omitted */
private:
static inline entity_metadata metadata_{"posts"};
};
static entity_metadata metadata_
std::string table_name() const override

You can then materialize both schemas in dependency order:

manager->create_query_result(user_entity{}.get_metadata().create_table_sql());
manager->create_query_result(post_entity{}.get_metadata().create_table_sql());
manager->create_query_result(post_entity{}.get_metadata().create_indexes_sql());
Note
The index constraint on author_id triggers create_indexes_sql() to emit CREATE INDEX statements that accelerate join lookups.

Step 4: Query Builder Integration

Entity metadata pairs naturally with the query_builder to construct type-safe queries without hard-coded column lists.

query_builder qb(database_types::postgres);
// Use the metadata-driven table name to avoid stringly-typed bugs
auto sql = qb.select({"id", "username", "email"})
.where("is_active", "=", true)
.order_by("username", sort_order::asc)
.limit(50)
.build();
auto rows = manager->select_query_result(sql);
Universal query builder that adapts to different database types.

To join posts and users:

auto sql = qb.select({"posts.id", "posts.title", "users.username"})
.from("posts")
.join("users", "posts.author_id", "users.id")
.where("posts.created_at", ">=", int64_t{1700000000})
.order_by("posts.created_at", sort_order::desc)
.limit(20)
.build();

Inspecting Metadata at Runtime

Iterating over entity_metadata::fields() is useful for diagnostics, schema migration tools, and serialization helpers.

const auto& meta = user_entity{}.get_metadata();
for (const auto& field : meta.fields())
{
std::cout << field.name() << " : " << field.type_name();
if (field.is_primary_key()) std::cout << " [PK]";
if (field.is_unique()) std::cout << " [UNIQUE]";
if (field.is_not_null()) std::cout << " [NOT NULL]";
if (field.has_index()) std::cout << " [INDEX]";
std::cout << "\n";
}

Next Steps