Persistence #
A DAO node service gives a custom service its own database. The DAO extends PartitionedDaoNodeService or UnpartitionedDaoNodeService, points at a liquibase changelog, and exposes typed methods that the parent service calls.
All examples on this page use the DemoProgressTrackerDaoNodeService from the DemoService reference service introduced in Custom services.
Define the database table #
Create a liquibase changelog in your project’s db-changelogs directory. Each changeset describes a DDL migration that the engine applies at startup:
--liquibase formatted sql
--changeset demo:1
CREATE TABLE `progress_tracker` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`count` BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `name_key` (`name`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE utf8mb4_unicode_ci;
Define the DAO config #
A DAO config embeds an UnpartitionedDatabaseConfig (or PartitionedDatabaseConfig) to provide database credentials. The config class is nested inside the DAO node service:
DemoProgressTrackerDaoNodeService.kt
package unicorn.demo.dao
import pragma.config.ConfigBackendModeFactory
import pragma.config.ServiceConfig
import pragma.databases.UnpartitionedDatabaseConfig
import pragma.settings.BackendType
//...
class DemoProgressTrackerDaoConfig private constructor(type: BackendType) :
ServiceConfig<DemoProgressTrackerDaoConfig>(type) {
override val description = "Configuration for the DemoProgressTrackerDaoNodeService."
var databaseConfig by types.embeddedObject(
UnpartitionedDatabaseConfig::class,
"Database config for the progress tracker DAO."
)
companion object : ConfigBackendModeFactory<DemoProgressTrackerDaoConfig> {
override fun getFor(type: BackendType) = DemoProgressTrackerDaoConfig(type).apply {
databaseConfig = UnpartitionedDatabaseConfig.getFor(type)
}
}
}
Declare the database config in YAML under game.serviceConfigs.<DaoConfigClassName>:
game:
serviceConfigs:
DemoProgressTrackerDaoNodeService.DemoProgressTrackerDaoConfig:
databaseConfig:
identifierSchema: local_demo
| Setting | Default | Description |
|---|---|---|
databaseConfig.identifierSchema | - | Database schema name for the DAO’s tables. |
Create the DAO node service #
A DAO node service extends PartitionedDatabaseConfig<T> or UnpartitionedDaoNodeService<T> where T is the DAO’s config class defined above. It will override two methods:
changelogFilepath()returns the path to the liquibase changelog relative todb-changelogs/.getDatabaseConfigFrom()extracts the database config class from the DAO’s config class.
The DAO then exposes domain methods (like addProgress below) that encapsulate SQL operations:
DemoProgressTrackerDaoNodeService.kt
package unicorn.demo.dao
import pragma.PragmaNode
import pragma.databases.SharedDatabaseConfigNodeService
import pragma.databases.UnpartitionedDaoNodeService
import pragma.databases.UnpartitionedDatabaseConfig
import pragma.services.PragmaService
import pragma.settings.BackendType
import pragma.settings.DatabaseValidator
//...
@PragmaService(
backendTypes = [BackendType.GAME],
dependencies = [SharedDatabaseConfigNodeService::class]
)
class DemoProgressTrackerDaoNodeService(
pragmaNode: PragmaNode,
databaseValidator: DatabaseValidator = pragmaNode.databaseValidator,
) : UnpartitionedDaoNodeService<DemoProgressTrackerDaoNodeService.DemoProgressTrackerDaoConfig>(
pragmaNode = pragmaNode,
databaseValidator = databaseValidator
) {
override fun changelogFilepath() = "db-changelogs/demo.sql"
override fun getDatabaseConfigFrom(serviceConfig: DemoProgressTrackerDaoConfig): UnpartitionedDatabaseConfig =
serviceConfig.databaseConfig
suspend fun addProgress(name: String, delta: Long): Long {
return transact(this::addProgress.name) { connection ->
connection.prepareStatement(
"""
INSERT INTO `${Table.tableName}` (`${Table.name}`, `${Table.count}`)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE `${Table.count}` = `${Table.count}` + VALUES(`${Table.count}`)
""".trimIndent()
).use { statement ->
statement.setString(1, name)
statement.setLong(2, delta)
statement.executeUpdate()
}
connection.prepareStatement(
"""
SELECT `${Table.count}` FROM `${Table.tableName}` WHERE `${Table.name}` = ?
""".trimIndent()
).use { statement ->
statement.setString(1, name)
val rs = statement.executeQuery()
rs.next()
rs.getLong(1)
}
}
}
Conformance notes for DAO node services:
- The
changelogFilepath()must be unique across all DAO node services. - The
@PragmaServiceannotation must includeSharedDatabaseConfigNodeServicein itsdependencies.
Build and run after creating the DAO to verify the table is created:
./pragma build
Use the DAO from a service #
The parent service accesses the DAO through two wiring steps: declaring the dependency in @PragmaService and retrieving the instance in run() via nodeServicesContainer.
The trackProgressV1 handler below demonstrates the full pattern. It is a partner RPC that calls the DAO’s addProgress method to persist a progress delta and return the new total:
Declare the dependency and wire in run():
DemoService.kt
package unicorn.demo
import java.util.UUID
import pragma.config.ConfigHandler
import pragma.PragmaNode
import pragma.services.DistributedService
import pragma.services.PragmaService
import pragma.settings.BackendType
import unicorn.demo.dao.DemoProgressTrackerDaoNodeService
//...
@PragmaService(
backendTypes = [BackendType.GAME],
dependencies = [DemoProgressTrackerDaoNodeService::class]
)
class DemoService(pragmaNode: PragmaNode, instanceId: UUID) :
DistributedService(pragmaNode, instanceId),
ConfigHandler<DemoServiceConfig> {
}
DemoService.kt
package unicorn.demo
import pragma.telemetry.client.TelemetryClientNodeService
import unicorn.demo.dao.DemoProgressTrackerDaoNodeService
//...
override suspend fun run() {
progressTrackerDao = nodeServicesContainer[DemoProgressTrackerDaoNodeService::class]
telemetryBackendClient = nodeServicesContainer[TelemetryClientNodeService::class]
}
Handle the partner RPC:
DemoService.kt
package unicorn.demo
import pragma.PartnerSession
import pragma.rpcs.PragmaRPC
import pragma.rpcs.RoutingMethod
import pragma.rpcs.SessionType
import pragma.telemetry.client.TelemetryClientEvent
import unicorn.demo.DemoServiceRpc.TrackProgressV1Request
import unicorn.demo.DemoServiceRpc.TrackProgressV1Response
//...
@PragmaRPC(SessionType.PARTNER, RoutingMethod.SESSION_PRAGMA_ID)
suspend fun trackProgressV1(session: PartnerSession, request: TrackProgressV1Request): TrackProgressV1Response {
val newTotal = progressTrackerDao.addProgress(request.name, request.count)
val telemetryEvent = TelemetryClientEvent(
sourceId = "DemoService",
name = "progress-tracker",
data = ProgressTelemetryPayload(request.name, newTotal)
)
telemetryBackendClient.recordEvent(telemetryEvent)
return TrackProgressV1Response.newBuilder()
.setNewTotal(newTotal)
.build()
}
Related topics #
- Custom services for creating the parent service.
- Configuration for override precedence, dynamic reload, and encrypted secrets.