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.
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.
// 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.
All CRUD methods return QCoro::Task<T>.
// 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.
QCoro::Task<QList<CarDto>> createOrphans(const QList<CreateCarDto> &cars);Creates entities without an owner. Useful for root entities or deferred ownership assignment.
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.
QCoro::Task<QList<CarDto>> getAll() const;Returns all entities of this type. Use with caution on large tables.
QCoro::Task<QList<CarDto>> update(const QList<UpdateCarDto> &cars);Updates scalar fields only (no relationship changes). Accepts UpdateCarDto which contains id + scalar fields. If the entity has an updatedAt field, it is set to the current UTC time automatically.
QCoro::Task<QList<CarDto>> updateWithRelationships(const QList<CarDto> &cars);Updates both scalar fields and relationship (junction table) data. Accepts the full CarDto. Use this when you need to change relationship fields alongside scalar fields in a single atomic operation.
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.
static CreateCarDto getCreateDto();Returns a default-constructed creation DTO. Convenience for UI code that needs an empty form.
static UpdateCarDto getUpdateDto();Returns a default-constructed update DTO. Convenience for UI code that needs an empty update form.
static UpdateCarDto toUpdateDto(const CarDto &dto);Converts a full CarDto to an UpdateCarDto, copying id + scalar fields and discarding relationship fields. Useful in QML where you fetch with get() and want to pass the result to update():
controller.get([itemId]).then(function(result) {
var updateDto = controller.toUpdateDto(result[0]);
updateDto.title = "new title";
controller.update([updateDto]);
});UpdateCarDto also has an explicit converting constructor from CarDto for C++ code:
UpdateCarDto updateDto(fullDto); // drops relationship fieldsOnly available if the entity has forward relationships defined in the manifest.
QCoro::Task<QList<int>> getRelationshipIds(
int carId,
CarRelationshipField relationship
) const;Returns the IDs of related entities for a single entity.
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.
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.
QCoro::Task<int> getRelationshipIdsCount(
int carId,
CarRelationshipField relationship
) const;Returns the count of related entities without fetching them.
QCoro::Task<QList<int>> getRelationshipIdsInRange(
int carId,
CarRelationshipField relationship,
int offset,
int limit
) const;Paginated access to related entity IDs.
QCoro::Task<QList<int>> moveRelationshipIds(
int carId,
CarRelationshipField relationship,
const QList<int> &idsToMove,
int newIndex // -1 = append at end
);Moves specific related IDs to a new position within an ordered relationship. Returns the reordered list of IDs. Supports undo/redo.
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/
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";
});// 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;
});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");
});QCoro::Task<void> MyClass::doWork()
{
auto cars = co_await controller->getAll(zzz);
// Process cars
...
auto updatedCars = co_await controller->update(cars);
}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.
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.
For each use case defined in the manifest, the controller generates a method. The shape depends on the use case configuration:
QCoro::Task<SaveResultDto> save(const SaveDto &saveDto);
// Convenience: get an empty input DTO
static SaveDto getSaveDto();QCoro::Task<ExportResultDto> exportData();QCoro::Task<bool> importData(const ImportDto &importDto);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;HandlingFileController *controller = new HandlingFileController(this);
QCoro::connect(std::move(controller->save(saveDto)),
this, [](auto &&result) {
qDebug() << "Save result:" << result.success;
});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);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.
The generated UoW inherits UnitOfWorkBase which provides transaction management (beginTransaction, commit, rollback). You pick which entity operations to expose using matching pairs of macros:
- Interface file (
i_{use_case}_uow.h): useDECLARE_UOW_ENTITY_*macros - Implementation file (
{use_case}_uow.h): use the matchingUOW_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.
Use in i_{use_case}_uow.h. All declared methods are pure virtual.
Individual operations:
| Macro | Declares 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> (scalar fields only) |
DECLARE_UOW_ENTITY_UPDATE_WITH_RELATIONSHIPS(Name) |
updateWithRelationshipsName(items) -> QList<SCE::Name> (scalars + relationships) |
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):
| Macro | Includes |
|---|---|
DECLARE_UOW_ENTITY_CRUD(Name) |
CREATE + CREATE_ORPHANS + GET_REL_FROM_OWNER + SET_REL_IN_OWNER + GET + GET_ALL + UPDATE + UPDATE_WITH_RELATIONSHIPS + REMOVE + SNAPSHOT |
DECLARE_UOW_ORPHAN_ENTITY_CRUD(Name) |
CREATE_ORPHANS + GET + GET_ALL + UPDATE + UPDATE_WITH_RELATIONSHIPS + REMOVE + SNAPSHOT |
DECLARE_UOW_ENTITY_RELATIONSHIPS(Name, RelFieldEnum) |
getNameRelationship, setNameRelationship, moveNameRelationship, getNameRelationshipMany, getNameRelationshipCount, getNameRelationshipInRange |
Use in {use_case}_uow.h. Each macro must match a declaration in the interface.
Individual operations:
| Macro | Implements |
|---|---|
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() (scalar fields only) |
UOW_ENTITY_UPDATE_WITH_RELATIONSHIPS(Name) |
updateWithRelationshipsName() (scalars + relationships) |
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:
| Macro | Includes |
|---|---|
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).
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{/* ... */};
}These are available on every UoW via UnitOfWorkBase. You call them in your use case execute():
| Method | Purpose |
|---|---|
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
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().
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.