Skip to main content
Version: 1.14.1

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 = 161_884)

// Insert a user that references the city
val user = orm insert User(
email = "alice@example.com",
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 "alice@example.com")

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")

Remove a Record​

orm remove 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 = 161_884)
val user = orm insert User(email = "bob@example.com", 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 remove 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