Model Guide
This guide explains how models work in Vona within the Cabloy monorepo.
Why models matter in the backend contract loop
Models are the persistence-facing layer of the backend contract loop.
A useful split is:
- controllers define HTTP-facing contracts
- services orchestrate business flow
- models own ORM-facing persistence behavior
- entities and DTOs shape the data contract around that behavior
That means model design affects not only SQL behavior, but also CRUD defaults, DTO assumptions, OpenAPI-facing contracts, and test setup.
Create a model
Example: create a model named student in module demo-student.
npm run vona :create:bean model student -- --module=demo-studentModel definition
Representative pattern:
import { BeanModelBase, Model } from 'vona-module-a-orm';
import { EntityStudent } from '../entity/student.ts';
@Model({ entity: EntityStudent })
export class ModelStudent extends BeanModelBase<EntityStudent> {}The key relationship is that a model is bound to an entity and exposes ORM-oriented operations for that entity.
Using a model
Using models in Vona supports both dependency injection and dependency lookup, but dependency lookup is usually the clearer default.
Lookup in the current module
class ServiceStudent {
async findAll(): Promise<EntityStudent[]> {
return await this.scope.model.student.select();
}
}Lookup across modules
class ServiceStudent {
async findAll(): Promise<EntityStudent[]> {
return await this.$scope.demoStudent.model.student.select();
}
}Direct container access
class ServiceStudent {
async findAll(): Promise<EntityStudent[]> {
return await this.bean._getBean('demo-student.model.student').select();
}
}This is one of the important backend layer boundaries:
- services should usually orchestrate through models
- models should remain the main persistence-facing abstraction instead of bypassing directly to ad hoc SQL everywhere
Basic CRUD patterns
Create
return await this.scope.model.student.insert(student);Read
return await this.scope.model.student.select();
return await this.scope.model.student.getById(id);Update
return await this.scope.model.student.updateById(id, student);Delete
return await this.scope.model.student.deleteById(id);These methods are also part of the generated CRUD thread; see CRUD Workflow.
Query-builder support
Vona models are built on Knex, so the model layer also supports lower-level query builder access when needed.
Representative patterns:
this.scope.model.student.builder().where('name', 'tom').orderBy('name');
this.scope.model.student.builderSelect().where('name', 'tom').orderBy('name');
this.scope.model.student.query('select * from demoStudent');A practical rule is:
- prefer model methods and model-aware query paths first
- use builder or raw SQL only when the higher-level model surface is not enough
Important model options
Several important model options include:
entitytabledisableDeleteddisableInstancedisableCreateTimedisableUpdateTimesoftDeletionPruneclientcacherelations
These options matter because Vona models are not only raw database adapters. They also carry framework-level behavior such as soft deletion, multi-instance support, datasource selection, caching, and relations.
App-config overrides
Model options can also be configured in app config.
Representative pattern:
config.onions = {
model: {
'demo-student:student': {
disableDeleted: true,
disableInstance: true,
client: 'mysql',
cache: false,
},
},
};This is one of the key contract-loop ideas: model behavior is not defined only inside the model file. It can also be refined through project configuration.
Dynamic table partitioning
Model supports dynamic table partitioning.
Representative pattern:
@Model({
table: (ctx: VonaContext, where: EntityOrder | undefined, defaultTable: keyof ITableRecord) => {
return `${defaultTable}_${moment().format('YYYYMMDD')}`;
},
})
class ModelOrder {}This matters because some persistence decisions belong at the model abstraction level even when they materially change how the table is resolved.
Soft deletion and multi-instance defaults
By default, Vona models support:
- soft deletion
- multi-instance or multi-tenancy filtering
That means model behavior can be richer than a naïve SQL wrapper. For example, a normal select() already participates in instance filtering and deleted-record filtering unless configured otherwise.
A practical implication is:
- contract and test behavior should not assume raw-table semantics when the model layer is active
Instance-aware model semantics
One important current-runtime distinction is that ordinary model usage is instance-aware by default.
A practical interpretation is:
- normal model operations participate in the current instance context
- instance-aware filtering is part of normal model behavior unless disabled
- request-scoped code should usually preserve that behavior instead of bypassing it casually
This is why disableInstance is a meaningful semantic choice rather than a minor ORM toggle.
Use it only when the model truly should ignore the active instance boundary.
Another practical implication is:
- lower-level builder or raw-SQL flows may need extra care when you are reproducing behavior that the ordinary model path would have applied automatically
For the broader instance/config story, also see Config Guide.
Datasource selection
Vona supports multi-database and multi-datasource behavior at the model layer.
Representative modes include:
- default datasource
- static datasource in model options
- app-config datasource override
- adaptive datasource through a function
- dynamic datasource chosen in code with
newInstance(...)
Representative dynamic pattern:
const modelMysql = this.scope.model.student.newInstance('mysql');
return await modelMysql.select();This matters because backend contract behavior can depend on datasource choice, not just on query shape.
In the current runtime, datasource choice can also change indirectly through instance isolation. If the active instance is configured as isolated, the effective default datasource may switch through that instance’s isolateClient setting even when the calling code did not name a datasource explicitly.
That is why model behavior should be read together with:
Cache behavior
Vona models enable cache behavior by default.
A practical distinction is:
- entity cache stores entity records
- query cache stores query-derived entity id sets
That means model behavior affects performance and consistency semantics, not only data retrieval.
For the broader cache story, also see Cache Guide.
Relationship to the backend contract loop
Read this guide together with:
Implementation checks for model-layer changes
When creating backend persistence logic:
- start from the model and entity pairing
- prefer model methods and model-aware query paths
- preserve Vona soft-delete and instance-aware behavior unless there is a real reason not to
- choose deliberately among local scope, cross-module scope, and direct bean access
- remember that datasource, cache, and model options can change the behavior of the backend contract loop
- drop to raw SQL or lower-level query builder logic only when the higher-level model surface is insufficient