Petals are a way to manage Jetbrains Exposed database interactions without boilerplate.
This means:
- No creating multiple classes or interfaces with the same values for exposed
- No manual creation of accessor or serializable data classes
- No manual database migrations
- No manual transaction management (still available if your use case needs it)
Petals manage this in 2 ways:
-
Database precheck/migrations
- Checks your current database to see if the table already exists
- Creates a petal meta table to manage versions (if table match is found, it is assumed to be schema version 1)
- Compares schema values to validate data before migrating
- Runs through all migrations in order starting with the current database version in the meta table (or creates a new table for schema version 1 if no existing table is found)
-
Generated database classes
- Exposed Table/Entity classes
- Data accessor class to manage transactions under the hood
- Kotlin Data class, along with extension methods for the entity and accessor classes to export to it
In its current beta state, it supports Postgres databases using Hikari as your database connector.
Add the annotations and kapt processor to your gradle dependencies:
implementation("com.casadetasha:petals:1.6.5-beta")
kapt("com.casadetasha:petals-processor:1.6.5-beta")
(For instructions on setting up kapt, see https://kotlinlang.org/docs/kapt.html#using-in-gradle)
Inside your application's setup: call Exposed's Database.connect(dbSource)
method, then call the generated setupAndMigrateTables(dbSource)
method with the same dbSource
.
fun setupDb() {
val dbSource = createHikariDataSource()
Database.connect(dbSource)
PetalTables.setupAndMigrateTables(dbSource)
}
fun createHikariDataSource(): HikariDataSource = // configure your own hikari connection here
To create a Petal, you will need to define both the Petal
and the PetalSchema
.
-
The Petal contains the table name, primary ID type, and generated accessor class name.
-
The PetalSchema contains a reference to the Petal, version number, and all column data.
ExamplePetalSchema.kt
@Petal(tableName = "example_table", className = "ExampleClass", primaryKeyType = PetalPrimaryKey.INT)
interface ExamplePetal
@PetalSchema(petal = ExamplePetal::class)
interface ExamplePetalSchema {
val name: String
}
MyServer.kt
ExampleClass.create(name = "Luke")
MyServer.kt
ExampleClass.load(id)
MyServer.kt
val myExample: ExampleClass // already loaded ExampleClass
myExample.name = "Formerly Luke"
ExampleClass.store(myExample)
MyServer.kt
val myExample: ExampleClass // already loaded ExampleClass
ExampleClass.delete(myExample)
MyServer.kt
ExampleClass.loadFromQuery { row -> row.name eq "John" }
MyServer.kt
val loadedExample = ExampleClass.load(id)
return Json.encodeToString( loadedExample.asData() )
Petals with no primary key type specified will default to UUID
Petals with no version specified will default to version 1
Here is a sample PetalSchema with all supported column types
@PetalSchema(petal = BasicPetal::class)
interface BasicPetalSchema {
val uuidColumn: UUID
val intColumn: Int
val longColumn: Long
val stringColumn: String
@VarChar(charLimit = 10) val varCharColumn: String
}
All column types recognize Kotlin's null declaration. Simply add ?
to the column type. This is true for Nested Petals as well.
@PetalSchema(petal = BasicPetal::class)
interface BasicPetalSchema {
val uuidColumn: UUID?
val intColumn: Int?
val longColumn: Long?
val stringColumn: String?
@VarChar(charLimit = 10) val varCharColumn: String?
}
To nest a Petal, create a column with the type of the nested Petal class (the class annotated with @Petal)
@Petal(tableName = "parent_petal", className = "ParentPetalClass")
interface ParentPetal
@Petal(tableName = "nested_petal", className = "NestedPetalClass")
interface NestedPetal
@PetalSchema(petal = ParentPetal::class)
interface ParentPetalSchema {
val name: String
val nestedPetal: NestedPetal
}
@PetalSchema(petal = NestedPetal::class)
interface NestedPetalSchema {
val name: String
}
val nestedPetal = NestedPetalClass.create(name = "hola nestie")
val parentPetal: ParentPetalClass = ParentPetalClass.create(
name = "My name",
nestedPetal = nestedPetal
)
val loadedNestedPetal = ParentPetalClass.load(parentPetal.id).nestedPetal
To add a reference to a Parent table, create a column with the type of the parent Petal class (the class annotated with @Petal). Annotate the column with @ReferencedBy({name of column in parent petal})
@Petal(tableName = "parent_petal", className = "ParentPetalClass")
interface ParentPetal
@Petal(tableName = "nested_petal", className = "NestedPetalClass")
interface NestedPetal
@PetalSchema(petal = ParentPetal::class)
interface ParentPetalSchema {
val name: String
val nestedPetal: NestedPetal
}
@PetalSchema(petal = NestedPetal::class)
interface NestedPetalSchema {
val name: String
@ReferencedBy("nestedPetal") val parents: ParentPetal
}
val nestedPetal = NestedPetalClass.create(name = "hola nestie")
val parentPetal: ParentPetalClass = ParentPetalClass.create(
name = "My name",
nestedPetal = nestedPetal
)
val loadedParentPetal = NestedPetalClass.load(nestedPetal.id).loadParents().first()
Adding and removing columns is managed by creating a new Schema with an incremented version number.
@PetalSchema(petal = MigratedPetal::class, version = 1)
interface MigratedPetalSchemaV1 {
val uuid: UUID
}
// Add a column
@PetalSchema(petal = MigratedPetal::class, version = 2)
interface MigratedPetalSchemaV2 {
val uuid: UUID
val color: String
}
// Remove a column
@PetalSchema(petal = MigratedPetal::class, version = 3)
interface MigratedPetalSchemaV3 {
val color: String
}
// Add and remove a column
@PetalSchema(petal = MigratedPetal::class, version = 4)
interface MigratedPetalSchemaV4 {
val uuid: UUID
}
Create a new migration with the new column name, and annotate it with @AlterColumn
with a renameFrom
value matching the previous column name.
@PetalSchema(petal = MigratedPetal::class, version = 1)
interface MigratedPetalSchemaV1 {
val uuid: UUID
}
@PetalSchema(petal = MigratedPetal::class, version = 2)
abstract class MigratedPetalSchemaV3 {
@AlterColumn(renameFrom = "uuid") val renamed_uuid: UUID
}
Use the matching @DefaultType() annotation on the column
@PetalSchema(petal = DefaultValuePetal::class)
interface DefaultValuePetalSchema {
@DefaultString("default string value") val stringValue: String
@DefaultInt(10) val intValue: Int
@DefaultLong(200) val longValue: Long
}
To run custom sql before a migration, add a @ExecuteSqlBeforeMigration("SQL GOES HERE")
annotation to the migration you want to run it before.
This example showcases dropping a table with the same name before creating a new one
@Petal(tableName = "example_table", className = "ExampleClass")
interface ExamplePetal
@ExecuteSqlBeforeMigration("DROP TABLE example_table")
@PetalSchema(petal = ExamplePetal::class)
interface DefaultValuePetalSchema {
val intValue: Int
}
Post-migration sql is planned to be supported, but has not yet been implemented.
The Exposed DB entity is a field on every accessor called dbEntity
MyServer.kt
fun loadEntity(id: Int): ExampleClassEntity {
return ExampleClass.load(id).dbEntity
}