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.
- Kotlin
- Java
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.
@Builder(toBuilder = true)
record City(@PK Integer id,
String name,
long population
) implements Entity<Integer> {}
@Builder(toBuilder = true)
record User(@PK Integer id,
String email,
String name,
@FK City city
) implements Entity<Integer> {}
In Java, record components are nullable by default. Use @Nonnull on fields that must always have a value. Primitive types (int, long, etc.) are inherently non-nullable.
The @Builder annotation is from Lombok and is optional. It generates a builder that lets you construct entities without specifying the primary key, and creates modified copies via toBuilder(). Without Lombok, you can pass null as the primary key (e.g., new City(null, "Sunnyvale", 155_000)) or define a convenience constructor that omits it. See Modifying Entities for details.
These entities map to the following database tables:
| Table | Columns |
|---|---|
city | id, name, population |
user | id, 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
- Java
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
Use the ORMTemplate.of(...) factory methods:
// From a DataSource (most common)
var orm = ORMTemplate.of(dataSource);
// From a Connection
var orm = ORMTemplate.of(connection);
// From a JPA EntityManager
var orm = ORMTemplate.of(entityManager);
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
- Kotlin
- Java
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).
var cities = orm.entity(City.class);
var users = orm.entity(User.class);
// Insert a city -- the returned object has the database-generated ID
City city = cities.insertAndFetch(City.builder()
.name("Sunnyvale")
.population(155_000)
.build());
// Insert a user that references the city
User user = users.insertAndFetch(User.builder()
.email("[email protected]")
.name("Alice")
.city(city)
.build());
The insertAndFetch method sends an INSERT statement, retrieves the auto-generated primary key, and returns a new record with the key populated.
Read a Record
- Kotlin
- Java
// 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]")
// Find by ID
Optional<User> user = orm.entity(User.class).findById(userId);
// Find by field value using the metamodel (requires storm-metamodel-processor)
Optional<User> user = orm.entity(User.class)
.select()
.where(User_.email, EQUALS, "[email protected]")
.getOptionalResult();
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.
- Kotlin
- Java
val updatedUser = orm update user.copy(name = "Alice Johnson")
users.update(new User(user.id(), user.email(), "Alice Johnson", user.city()));
Delete a Record
- Kotlin
- Java
orm delete user
users.delete(user);
Transactions
Wrap multiple operations in a transaction to ensure they succeed or fail together.
- Kotlin
- Java
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)
}
With Spring's @Transactional:
@Transactional
public User createUser(String email, String name, City city) {
return orm.entity(User.class)
.insertAndFetch(User.builder()
.email(email)
.name(name)
.city(city)
.build());
}
See Transactions for programmatic transaction control, propagation modes, and savepoints.
Summary
You have now seen the core workflow:
- Define entities as data classes or records with
@PKand@FKannotations - Create an
ORMTemplatefrom aDataSource - Use
insert,findById,update, anddeletefor 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