Skip to main content
Version: 1.11.0

First Entity

This guide walks you through defining your first Storm entity, creating an ORM template, and performing basic CRUD operations. By the end, you will have inserted a record into the database and read it back.

Define an Entity

Storm entities are plain data classes (Kotlin) or records (Java) that implement the Entity<ID> interface. Annotate the primary key with @PK and foreign keys with @FK. Storm maps field names to column names automatically using camelCase-to-snake_case conversion, so no XML or additional configuration is needed.

data class City(
@PK val id: Int = 0,
val name: String,
val population: Long
) : Entity<Int>

data class User(
@PK val id: Int = 0,
val email: String,
val name: String,
@FK val city: City
) : Entity<Int>

Non-nullable fields (like city: City) produce INNER JOIN queries. Nullable fields (like city: City?) produce LEFT JOIN queries. Kotlin's type system maps directly to Storm's null handling.

These entities map to the following database tables:

TableColumns
cityid, name, population
userid, email, name, city_id

Storm automatically appends _id to foreign key column names. See Entities for the full set of annotations, naming conventions, and customization options.

Create the ORM Template

The ORMTemplate is the central entry point for all database operations. It is thread-safe and typically created once at application startup (or provided as a Spring bean). You can create one from a JDBC DataSource, Connection, or JPA EntityManager.

Kotlin provides extension properties for concise creation:

// From a DataSource (most common)
val orm = dataSource.orm

// From a Connection
val orm = connection.orm

// From a JPA EntityManager
val orm = entityManager.orm

If you are using Spring Boot with one of the starter modules, the ORMTemplate bean is created automatically. See Spring Integration for details.

Insert a Record

Storm's Kotlin API provides infix operators for a concise syntax:

// Insert a city -- the returned object has the database-generated ID
val city = orm insert City(name = "Sunnyvale", population = 155_000)

// Insert a user that references the city
val user = orm insert User(
email = "[email protected]",
name = "Alice",
city = city
)

The insert operator sends an INSERT statement, retrieves the auto-generated primary key, and returns a new instance with the key populated. You do not need to set the id field yourself when using IDENTITY generation (the default).

Read a Record

// Find by ID
val user: User? = orm.entity<User>().findById(userId)

// Find by field value using the metamodel (requires storm-metamodel-processor)
val user: User? = orm.find(User_.email eq "[email protected]")

When Storm loads a User, it automatically joins the City table (because city is marked with @FK) and populates the full City object in a single query. There is no N+1 problem.

Update a Record

Since entities are immutable, you create a new instance with the changed fields and pass it to the update operation.

val updatedUser = orm update user.copy(name = "Alice Johnson")

Delete a Record

orm delete user

Transactions

Wrap multiple operations in a transaction to ensure they succeed or fail together.

Storm provides a transaction block that commits on success and rolls back on exception:

transaction {
val city = orm insert City(name = "Sunnyvale", population = 155_000)
val user = orm insert User(email = "[email protected]", name = "Bob", city = city)
}

See Transactions for programmatic transaction control, propagation modes, and savepoints.

Summary

You have now seen the core workflow:

  1. Define entities as data classes or records with @PK and @FK annotations
  2. Create an ORMTemplate from a DataSource
  3. Use insert, findById, update, and delete for basic CRUD

Next Steps

  • First Query -- custom queries, repositories, filtering, and streaming
  • Entities -- enumerations, versioning, composite keys, and naming conventions
  • Spring Integration -- auto-configuration and dependency injection