Skip to content

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:

  1. Marks the class for compile-time metadata generation so type-safe PagedList paths (Tables.Foo_.name) and the entity registrar are emitted.
  2. 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 to false.
  • 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 to false.
  • creatable: Determines whether the field can be used when creating a new record. Defaults to true.
  • updatable: Determines whether the field can be used when updating a record. Defaults to true.
  • enumAsString: When true, stores enum values as their string name instead of an integer. Defaults to false. See Enums.

Example

import onl.ycode.stormify.DbField

data class User(
    @DbField(name = "custom_id", primaryKey = true, primarySequence = "id_seq")
    var id: Int = 0,

    @DbField(creatable = false, updatable = true)
    var name: String = ""
)
import onl.ycode.stormify.DbField;

public class User {
    @DbField(name = "custom_id", primaryKey = true, primarySequence = "id_seq")
    private int id;

    @DbField(creatable = false, updatable = true)
    private String name;

    // getters and setters
}

In this example:

  • The id field is mapped to the custom_id column, marked as a primary key, and uses a sequence named id_seq.
  • The name field 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, its name is 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 the name, insertable, and updatable attributes.

  • @JoinColumn: Specifies the column used for joining an entity association. Stormify reads the name, insertable, and updatable attributes.

  • @SequenceGenerator: Defines a primary key generator that uses a database sequence. As in JPA, the sequence is the one named by sequenceName; when sequenceName is empty, the generator name is used as the sequence name.

  • @GeneratedValue: When used with strategy = 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 Java transient keyword. On a delegated property, @delegate:Transient excludes 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).