Annotations
Stormify supports both its own annotations (@DbTable, @DbField) and standard JPA annotations (@Id, @Table, @Column, etc.) — see JPA annotations below.
@DbTable Annotation
The @DbTable annotation marks a Kotlin class as a Stormify entity. It serves two roles:
- Marks the class for compile-time metadata generation so type-safe PagedList paths (
Tables.Foo_.name) and the entity registrar are emitted. - Overrides the table name when the database table doesn't match the class name under the current naming policy.
When the table name already matches the policy (e.g. class User ↔ table user), use @DbTable without arguments as a pure marker. Provide name = "..." only when the database table name differs. On JVM with reflection-based discovery the annotation is optional — the class is still picked up. For KSP path generation, @DbTable (or JPA @Entity) is the explicit marker, but classes are also discovered implicitly when any of their properties carries @DbField, @Id, @Column, or @JoinColumn, so those get type-safe paths generated as well.
Attributes
name: Specifies the name of the table in the database. If not provided, the class name will be used, converted using the current naming policy.
Example
import onl.ycode.stormify.DbTable
import onl.ycode.stormify.DbField
// Marker only — table name is derived from the class name
@DbTable
data class User(
@DbField(primaryKey = true)
var id: Int = 0,
var name: String = ""
)
// Explicit table name — different from what the naming policy would produce
@DbTable(name = "tbl_legacy_customer")
data class Customer(
@DbField(primaryKey = true)
var id: Int = 0,
var name: String = ""
)
import onl.ycode.stormify.DbTable;
import onl.ycode.stormify.DbField;
// Marker only — table name is derived from the class name
@DbTable
public class User {
@DbField(primaryKey = true)
private int id;
private String name;
// getters and setters
}
// Explicit table name — different from what the naming policy would produce
@DbTable(name = "tbl_legacy_customer")
public class Customer {
@DbField(primaryKey = true)
private int id;
private String name;
// getters and setters
}
@DbField Annotation
The @DbField annotation provides additional information about a specific field. This annotation
is optional and allows you to customize how fields are mapped to database columns.
Attributes
name: Specifies the name of the field in the database. If not provided, the field name in the class will be used, converted using the current naming policy.primaryKey: Indicates whether the field is a primary key. Defaults tofalse.primarySequence: Specifies the name of the primary key sequence in the database. If not provided, the primary key value generation relies on the database.autoIncrement: Indicates the field value is auto-generated by the database (AUTO_INCREMENT, SERIAL, IDENTITY). When true, the field is excluded from INSERT statements and its value is read back after insertion. Defaults tofalse.creatable: Determines whether the field can be used when creating a new record. Defaults totrue.updatable: Determines whether the field can be used when updating a record. Defaults totrue.enumAsString: Whentrue, stores enum values as their string name instead of an integer. Defaults tofalse. See Enums.
Example
In this example:
- The
idfield is mapped to thecustom_idcolumn, marked as a primary key, and uses a sequence namedid_seq. - The
namefield is configured to be updatable but not creatable.
Other Supported Annotations
Stormify provides support for several standard annotations from the javax.persistence package (JPA), making it easy to integrate with existing applications. The following annotations are supported:
-
@Id: Marks a field as the primary key of the entity. -
@Table: Specifies the table in the database that maps to the entity. Stormify uses the table name from this annotation to map your classes to the corresponding database tables. -
@Entity: Marks the class as an entity. As in JPA, itsnameis used as the table name when neither@DbTable(name)nor@Table(name)gives one. The precedence is@DbTable, then@Table, then@Entity, whatever the order of the annotations. -
@Column: Maps a field to a specific column. Stormify reads thename,insertable, andupdatableattributes. -
@JoinColumn: Specifies the column used for joining an entity association. Stormify reads thename,insertable, andupdatableattributes. -
@SequenceGenerator: Defines a primary key generator that uses a database sequence. As in JPA, the sequence is the one named bysequenceName; whensequenceNameis empty, the generatornameis used as the sequence name. -
@GeneratedValue: When used withstrategy = GenerationType.IDENTITY, marks a field as auto-generated by the database (equivalent to@DbField(autoIncrement = true)). -
@Enumerated: Controls how enum fields are stored.@Enumerated(EnumType.STRING)stores the enum name as a string (equivalent to@DbField(enumAsString = true)).@Enumerated(EnumType.ORDINAL)(or omitting the annotation) stores the ordinal integer. -
@Transient: Marks a field to be ignored during database operations. All three forms are supported: JPA@javax.persistence.Transient, Kotlin@kotlin.jvm.Transient, and the Javatransientkeyword. On a delegated property,@delegate:Transientexcludes it as well.
@DbField and the JPA annotations can be combined on one field, and they are merged the same way on every platform and whatever their order: the column name comes from the first of @DbField, @Column, @JoinColumn that sets one; a field is a primary key, auto-generated, or an enum stored as a string if any annotation says so; and it is left out of INSERT or UPDATE if any annotation sets creatable/insertable or updatable to false.
A column may be mapped twice, once as a plain value and once as a read-only reference, to expose both the raw foreign key and the associated entity. The reference must be excluded from INSERT and UPDATE so the column is written once:
@Column(name = "parent_id")
private Integer parentId;
@JoinColumn(name = "parent_id", insertable = false, updatable = false)
private Parent parent;
On read, the plain field receives the column value and the reference receives the entity.
Only properties that store a value are mapped. A computed property with no backing field
(val upper get() = name.uppercase()) is not a column, and neither is a delegated property
marked @delegate:Transient (for example by lazy).
With JVM reflection, a read-only property that has a backing field (a val, a var with a
private or protected setter, a Java field without setter) is a normal column: it is loaded
from the database into its field and written by INSERT and UPDATE from that same field, so
an UPDATE after a load writes back exactly the value the database held, even when the
property has a custom getter that transforms it. This includes a val primary key. A read-only
property backed by any other delegate than db (for example val x by lazy { } without
@delegate:Transient) cannot hold a loaded value, so the class is rejected when its
metadata is built: mark it @delegate:Transient or use by db(). A val x by db() is
rejected the same way: the db delegate can only be filled through a var.
Properties under the annotation processor
The annotation processor (annproc, used on Kotlin/Native, Android and wherever the
Gradle plugin generates the registrar) assigns every column through generated code, so
it requires every mapped property to be a var or to use by db(). Each Kotlin
property is either a column, or explicitly not one, or a compile error:
| Property | Under annproc |
|---|---|
var x, or var x with any delegate (by db(), by Delegates.observable(...)) |
column, assigned through its setter |
val x get() = ... (computed: no backing field, no delegate) |
not a column (a warning if it carries a column annotation) |
any property marked @Transient / @delegate:Transient (e.g. by lazy) |
not a column |
val x = ..., a val constructor parameter |
compile error: "must be a var (or use by db())" |
var x (delegated or not) with a private or protected setter |
compile error: "has no public setter" |
val x by db() |
compile error: "must be a var" |
any other read-only delegate without @delegate:Transient (e.g. val x by lazy { }) |
compile error: "mark it @delegate:Transient, or make it a var ... by db()" |
JVM reflection has no generated code: it loads read-only properties through their field,
as described above. That is the one difference between the two paths: the annotation
processor requires a var or by db().
These annotations help bridge the gap between your classes and the database schema. By leveraging standard JPA annotations, Stormify ensures compatibility with existing JPA setups while providing additional flexibility.
Generated Metadata
Stormify needs entity metadata (field names, types, primary keys) to perform ORM
operations. The Gradle plugin (Installation) handles
this automatically: it runs the annproc KSP processor at compile time to scan
@DbTable and JPA @Entity annotations and emit a GeneratedEntities registrar
plus a Tables object with type-safe paths. Pass the registrar to Stormify(dataSource, GeneratedEntities).
On JVM-only projects using Maven or with generateRegistrar.set(false) in Gradle,
kotlin-reflect discovers the same metadata at runtime — no annotations required
beyond @DbField(primaryKey = true) for primary keys (or a primary key resolver).