Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API Reference - C++/Qt

This document is the API reference for Qleany-generated C++20/Qt6 code. It covers the APIs you interact with as a developer: Entity Controllers, Feature Controllers, and the Unit of Work macros you adapt when implementing custom use cases.

For general architecture and code structure, see Generated Code - C++/Qt.


Entity Controller

File: direct_access/{entity}/{entity}_controller.h/.cpp

Entity controllers are the public entry point for all CRUD and relationship operations on a single entity type. They are QObject-based, async (QCoro coroutines), and integrate with the undo/redo system.

Construction

// Create a controller for Car entities
auto controller = new CarController(parent);

// Optionally bind to a specific undo/redo stack (for per-document undo)
auto controller = new CarController(parent, /*undoRedoStackId=*/ 1);
controller->setUndoRedoStackId(2);  // change later
int stackId = controller->undoRedoStackId();

Dependencies (DbContext, EventRegistry, UndoRedoSystem) are resolved automatically from ServiceLocator at construction time.

CRUD Methods

All CRUD methods return QCoro::Task<T>.

create

// Only available if the entity has an owner (defined in the manifest)
QCoro::Task<QList<CarDto>> create(
    const QList<CreateCarDto> &cars,
    int ownerId,
    int index = -1     // insertion position; -1 = append
);

Creates entities and attaches them to their owner. For OneToOne/ManyToOne relationships, existing children are displaced (replaced). For list relationships (orderedOneToMany, oneToMany, manyToMany), new items are appended or inserted at index.

createOrphans

QCoro::Task<QList<CarDto>> createOrphans(const QList<CreateCarDto> &cars);

Creates entities without an owner. Useful for root entities or deferred ownership assignment.

get

QCoro::Task<QList<CarDto>> get(const QList<int> &carIds) const;

Fetches entities by their IDs. Returns DTOs in the same order as the input IDs.

getAll

QCoro::Task<QList<CarDto>> getAll() const;

Returns all entities of this type. Use with caution on large tables.

update

QCoro::Task<QList<CarDto>> update(const QList<CarDto> &cars);

Updates existing entities. If the entity has an updatedAt field, it is set to the current UTC time automatically.

remove

QCoro::Task<QList<int>> remove(const QList<int> &carIds);

Deletes entities by ID. Strong (owned) children are cascade-deleted. Returns the IDs that were actually removed.

getCreateDto (static)

static CreateCarDto getCreateDto();

Returns a default-constructed creation DTO. Convenience for UI code that needs an empty form.

Relationship Methods

Only available if the entity has forward relationships defined in the manifest.

getRelationshipIds

QCoro::Task<QList<int>> getRelationshipIds(
    int carId,
    CarRelationshipField relationship
) const;

Returns the IDs of related entities for a single entity.

setRelationshipIds

QCoro::Task<void> setRelationshipIds(
    int carId,
    CarRelationshipField relationship,
    const QList<int> &relatedIds
);

Replaces the relationship for a single entity. If the entity has an updatedAt field, it is touched.

getRelationshipIdsMany

QCoro::Task<QHash<int, QList<int>>> getRelationshipIdsMany(
    const QList<int> &carIds,
    CarRelationshipField relationship
) const;

Batch lookup: returns a map from entity ID to its related IDs.

getRelationshipIdsCount

QCoro::Task<int> getRelationshipIdsCount(
    int carId,
    CarRelationshipField relationship
) const;

Returns the count of related entities without fetching them.

getRelationshipIdsInRange

QCoro::Task<QList<int>> getRelationshipIdsInRange(
    int carId,
    CarRelationshipField relationship,
    int offset,
    int limit
) const;

Paginated access to related entity IDs.

Usage Examples

All controller methods return QCoro::Task<T>. Use QCoro::connect() to handle the result from non-coroutine code (slots, UI handlers), or .then() to chain dependent operations.

For more information about QCoro .then(), see the QCoro documentation and connect() here.

For QCoro on QML: https://qcoro.dev/qml/qmltask/

From C++ (QCoro::connect)

CarController *controller = new CarController(this);

// Create orphans
QCoro::connect(std::move(controller->createOrphans({CarController::getCreateDto()})),
    this, [](auto &&created) {
        qDebug() << "Created" << created.size() << "cars";
    });

// Get by IDs
QCoro::connect(std::move(controller->get({1, 2, 3})),
    this, [](auto &&cars) {
        for (const auto &car : cars)
            qDebug() << car.name;
    });

// Update
CarDto car = /* ... */;
car.name = u"Updated Name"_s;
QCoro::connect(std::move(controller->update({car})),
    this, [](auto &&updated) {
        qDebug() << "Updated:" << updated.first().name;
    });

// Remove
QCoro::connect(std::move(controller->remove({carId})),
    this, [](auto &&removedIds) {
        qDebug() << "Removed" << removedIds.size() << "cars";
    });

Chaining dependent operations with .then()

// Get relationship IDs, then fetch the related entities
auto task = controller->getRelationshipIds(carId, CarRelationshipField::Passengers)
    .then([passengerController](auto &&passengerIds) {
        return passengerController->get(passengerIds);
    });

QCoro::connect(std::move(task), this, [](auto &&passengers) {
    for (const auto &p : passengers)
        qDebug() << p.name;
});

From QML

Be careful with QML and async: you must use QCoroQMLTask’s then() to handle results, as QML does not support coroutines directly. This is not a Javascript async function, you can’t chain several .then(). Only one .then(), that’s all. See https://qcoro.dev/qml/qmltask/.

carController.createOrphans([dto]).then(function(result) {
    console.log("Created:", JSON.stringify(result));
});

carController.get([carId]).then(function(cars) {
    console.log("Fetched:", cars.length, "cars");
});

From another coroutine (co_await)

QCoro::Task<void> MyClass::doWork()
{
    auto cars = co_await controller->getAll(zzz);
    
    // Process cars
    ...
    
    auto updatedCars = co_await controller->update(cars);
}

Feature Controller

File: {feature}/{feature}_controller.h/.cpp

Feature controllers are the entry point for custom use cases grouped by feature. Like entity controllers, they are QObject-based and async. The controller is generated; you implement the use case logic.

Construction

Same pattern as entity controllers:

auto controller = new HandlingFileController(parent);
controller->setUndoRedoStackId(1);

Dependencies (DbContext, EventRegistry, FeatureEventRegistry, UndoRedoSystem, and optionally LongOperationManager) are resolved from ServiceLocator.

Generated Methods

For each use case defined in the manifest, the controller generates a method. The shape depends on the use case configuration:

Standard use case (with input DTO, with output DTO)

QCoro::Task<SaveResultDto> save(const SaveDto &saveDto);

// Convenience: get an empty input DTO
static SaveDto getSaveDto();

Standard use case (no input DTO, with output DTO)

QCoro::Task<ExportResultDto> exportData();

Standard use case (with input DTO, no output DTO)

QCoro::Task<bool> importData(const ImportDto &importDto);

Long operation use case

Long operations run on a background thread with progress tracking. They return synchronously (no co_await):

// Start the operation, returns an operation ID
QString generateCode(const GenerateCodeDto &generateCodeDto);

// Poll progress
std::optional<Common::LongOperation::OperationProgress> getGenerateCodeProgress(
    const QString &operationId) const;

// Get result (if use case has an output DTO)
std::optional<GenerateCodeResultDto> getGenerateCodeResult(
    const QString &operationId) const;

Usage Examples

Standard use case from C++

HandlingFileController *controller = new HandlingFileController(this);

QCoro::connect(std::move(controller->save(saveDto)),
    this, [](auto &&result) {
        qDebug() << "Save result:" << result.success;
    });

Long operation from C++

QString opId = controller->generateCode(dto);

// Check progress (e.g., from a timer)
auto progress = controller->getGenerateCodeProgress(opId);
if (progress)
    qDebug() << progress->message << progress->percentage() << "%";

// Get result when done
auto result = controller->getGenerateCodeResult(opId);

Custom Unit of Work (Macros)

Files you edit:

  • Interface: {feature}/use_cases/{use_case}_uc/i_{use_case}_uow.h
  • Implementation: {feature}/units_of_work/{use_case}_uow.h

When Qleany generates a custom feature use case, it scaffolds a UoW interface and implementation with TODO comments. Your job is to adapt the macros to expose only the entity operations your use case needs.

How It Works

The generated UoW inherits UnitOfWorkBase which provides transaction management (beginTransaction, commit, rollback). You pick which entity operations to expose using matching pairs of macros:

  1. Interface file (i_{use_case}_uow.h): use DECLARE_UOW_ENTITY_* macros
  2. Implementation file ({use_case}_uow.h): use the matching UOW_ENTITY_* macros

Each macro expands to a method named after the entity. For example, DECLARE_UOW_ENTITY_UPDATE(Work) declares virtual QList<SCE::Work> updateWork(const QList<SCE::Work> &items) = 0.

Interface Declaration Macros

Use in i_{use_case}_uow.h. All declared methods are pure virtual.

Individual operations:

MacroDeclares method
DECLARE_UOW_ENTITY_CREATE(Name)createName(items, ownerId, index) -> QList<SCE::Name>
DECLARE_UOW_ENTITY_CREATE_ORPHANS(Name)createOrphanName(items) -> QList<SCE::Name>
DECLARE_UOW_ENTITY_GET(Name)getName(ids) -> QList<SCE::Name>
DECLARE_UOW_ENTITY_GET_ALL(Name)getAllName() -> QList<SCE::Name>
DECLARE_UOW_ENTITY_UPDATE(Name)updateName(items) -> QList<SCE::Name>
DECLARE_UOW_ENTITY_REMOVE(Name)removeName(ids) -> QList<int>
DECLARE_UOW_ENTITY_SNAPSHOT(Name)snapshotName(ids) + restoreName(snap)
DECLARE_UOW_ENTITY_GET_REL_FROM_OWNER(Name)getNameRelationshipsFromOwner(ownerId) -> QList<int>
DECLARE_UOW_ENTITY_SET_REL_IN_OWNER(Name)setNameRelationshipsInOwner(itemIds, ownerId)

Composite macros (shorthand for common combinations):

MacroIncludes
DECLARE_UOW_ENTITY_CRUD(Name)CREATE + CREATE_ORPHANS + GET_REL_FROM_OWNER + SET_REL_IN_OWNER + GET + GET_ALL + UPDATE + REMOVE + SNAPSHOT
DECLARE_UOW_ORPHAN_ENTITY_CRUD(Name)CREATE_ORPHANS + GET + GET_ALL + UPDATE + REMOVE + SNAPSHOT
DECLARE_UOW_ENTITY_RELATIONSHIPS(Name, RelFieldEnum)getNameRelationship, setNameRelationship, getNameRelationshipMany, getNameRelationshipCount, getNameRelationshipInRange

Implementation Macros

Use in {use_case}_uow.h. Each macro must match a declaration in the interface.

Individual operations:

MacroImplements
UOW_ENTITY_CREATE(Name)createName()
UOW_ENTITY_CREATE_ORPHANS(Name)createOrphanName()
UOW_ENTITY_GET(Name)getName()
UOW_ENTITY_GET_ALL(Name)getAllName()
UOW_ENTITY_UPDATE(Name)updateName()
UOW_ENTITY_REMOVE(Name)removeName()
UOW_ENTITY_SNAPSHOT(Name)snapshotName() + restoreName()
UOW_ENTITY_GET_REL_FROM_OWNER(Name)getNameRelationshipsFromOwner()
UOW_ENTITY_SET_REL_IN_OWNER(Name)setNameRelationshipsInOwner()

Composite macros:

MacroIncludes
UOW_ENTITY_CRUD(Name)All CRUD + owner relationship + snapshot
UOW_ORPHAN_ENTITY_CRUD(Name)All CRUD + snapshot (no owner ops)
UOW_ENTITY_RELATIONSHIPS(Name, RelFieldEnum)All relationship operations

All implementation macros internally create a repository via RepositoryFactory::createNameRepository(m_dbSubContext, m_eventRegistry, m_signalBuffer).

Full Example

Given a “Save” use case in the “HandlingManifest” feature that needs to read and update Work and Setting entities:

Interface (use_cases/save_uc/i_save_uow.h):

class ISaveUnitOfWork : public virtual Common::UnitOfWork::ITransactional
{
  public:
    ~ISaveUnitOfWork() override = default;

    DECLARE_UOW_ENTITY_GET(Work);
    DECLARE_UOW_ENTITY_UPDATE(Work);
    DECLARE_UOW_ENTITY_GET_ALL(Setting);
    DECLARE_UOW_ENTITY_UPDATE(Setting);
    DECLARE_UOW_ENTITY_RELATIONSHIPS(Work, WorkRelationshipField);

    virtual void publishSaveSignal() = 0;
};

Implementation (units_of_work/save_uow.h):

class SaveUnitOfWork : public Common::UnitOfWork::UnitOfWorkBase,
                       public ISaveUnitOfWork
{
  public:
    SaveUnitOfWork(SCDatabase::DbContext &db,
                   QPointer<SCD::EventRegistry> er,
                   QPointer<SCF::FeatureEventRegistry> fer)
        : UnitOfWorkBase(db, er), m_featureEventRegistry(fer) {}

    UOW_ENTITY_GET(Work)
    UOW_ENTITY_UPDATE(Work)
    UOW_ENTITY_GET_ALL(Setting)
    UOW_ENTITY_UPDATE(Setting)
    UOW_ENTITY_RELATIONSHIPS(Work, WorkRelationshipField)

    void publishSaveSignal() override
    {
        m_featureEventRegistry->handlingManifestEvents()->publishSaveSignal();
    }

  private:
    QPointer<SCF::FeatureEventRegistry> m_featureEventRegistry;
};

Use case (use_cases/save_uc.cpp) – this is where you write your logic:

SaveResultDto SaveUseCase::execute(const SaveDto &saveDto) const
{
    try
    {
        if (!m_uow->beginTransaction())
            throw std::runtime_error("Failed to begin transaction");

        // Use the UoW methods you declared:
        auto works = m_uow->getWork({saveDto.workId});
        auto settings = m_uow->getAllSetting();

        // ... your business logic ...

        auto updated = m_uow->updateWork(works);

        if (!m_uow->commit())
            throw std::runtime_error("Failed to commit transaction");
    }
    catch (...)
    {
        m_uow->rollback();
        throw;
    }

    m_uow->publishSaveSignal();

    return SaveResultDto{/* ... */};
}

ITransactional Methods

These are available on every UoW via UnitOfWorkBase. You call them in your use case execute():

MethodPurpose
beginTransaction()Start a DB transaction and begin signal buffering
commit()Commit; flush buffered signals on success, discard on failure
rollback()Roll back the transaction and discard buffered signals
createSavepoint()Create a named savepoint within the current transaction
rollbackToSavepoint()Roll back to the last savepoint
releaseSavepoint()Release the last savepoint

The signal buffering ensures that if a transaction fails, no events are emitted and the UI stays consistent.

Do not use savepoint without understanding the implications: please read Undo-Redo Architecture # savepoints

Undoable Custom Use Cases

If a custom use case is marked undoable: true in the manifest, the controller calls executeUndoableCommand which expects undo() and redo() methods on the use case. The generated scaffold only has execute()you must add undo() and redo() yourself.

Both methods return UndoRedo::Result<void>. A default-constructed Result<void> means success; construct with an error message to signal failure.

class SaveUseCase
{
  public:
    explicit SaveUseCase(std::unique_ptr<ISaveUnitOfWork> uow);
    [[nodiscard]] SaveResultDto execute(const SaveDto &saveDto) const;

    // Add these for undoable use cases:
    UndoRedo::Result<void> undo();
    UndoRedo::Result<void> redo();

  private:
    std::unique_ptr<ISaveUnitOfWork> m_uow;

    // Store whatever state you need to undo/redo
    // (e.g., snapshots taken during execute)
};
UndoRedo::Result<void> SaveUseCase::undo()
{
    // TODO: restore previous state (e.g., via m_uow->restoreWork(m_snapshot))
    return {};  // success
}

UndoRedo::Result<void> SaveUseCase::redo()
{
    // TODO: re-apply the operation
    return {};  // success
}

The SNAPSHOT macro pair (DECLARE_UOW_ENTITY_SNAPSHOT / UOW_ENTITY_SNAPSHOT) is typically used to capture entity state during execute() and restore it in undo().

publish*Signal()

Every custom UoW has a publishSignalName() method that emits a feature-level event via the FeatureEventRegistry. Call it after a successful commit so subscribers (UI, other features) are notified. The generated scaffold calls it automatically in the use case template.