Ktorm bindings for Vaadin. Glues Ktorm entities to Vaadin's UI primitives so you don't have to write the plumbing yourself.
Features:
- Bind Ktorm entities to forms via Vaadin
Binder, with JSR-303 validation that respects Ktorm's interface-based entities. - Back a
GridorComboBoxwithEntityDataProvider— pagination, sorting, and filtering are translated to SQL automatically. - Drive Grids off arbitrary joins / projections with
QueryDataProvider. - Ready-made filter components (text, boolean, enum, date range, number range) that produce
Ktorm
ColumnDeclaring<Boolean>expressions. ActiveEntitymixin addssave(),create(),validate(), plus table-level DAO helpers (findAll,count,single,deleteAll).
See the bundled :testapp (run with ./gradlew :testapp:run) or the larger
beverage-buddy-ktorm example.
Requirements: JDK 21+, Kotlin 2.3+, Vaadin 25.1+, Ktorm 4.1+. Licensed under MIT. Contributions and bug reports welcome — see CONTRIBUTING.md.
- Adding to your project
- Entities and DAOs
- Transactions and Active Entities
- EntityDataProvider
- Joins via
QueryDataProvider - Grid Sorting
- Grid Filters
- Forms
- Further Documentation
First, add a dependency on ktorm-vaadin to your project:
dependencies {
implementation("com.github.mvysny.ktorm-vaadin:ktorm-vaadin:0.2")
}ktorm-vaadin pulls in Ktorm, Hibernate-Validator for JSR-303 validation, but you also want to add Hikari-CP for connection pooling, and FlyWay for keeping your database up-to-speed.
To initialize the database, we'll add the start/stop listener:
// Called by Jetty before the app starts serving requests, and afterwards when it's killed.
@WebListener
class Bootstrap : ServletContextListener {
// Called by Jetty when the app starts up.
override fun contextInitialized(servletContextEvent: ServletContextEvent?) {
log.info("Connecting to the database")
val cfg = HikariConfig().apply {
driverClassName = "org.h2.Driver"
jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
username = "sa"
password = ""
}
dataSource = HikariDataSource(cfg)
ActiveKtorm.database = Database.connect(dataSource)
log.info("Started")
}
// Called by Jetty when the app is stopped.
override fun contextDestroyed(sce: ServletContextEvent?) {
log.info("Closing database connections")
dataSource.close()
log.info("Destroyed")
}
companion object {
private val log = LoggerFactory.getLogger(Bootstrap::class.java)
private lateinit var dataSource: HikariDataSource
}
}A database connection is created and a Ktorm Database instance is
stored into ActiveKtorm.database. ktorm-vaadin assumes you connect to just one database
and uses it to make all database calls.
See testapp's Bootstrap.kt file for a full example using FlyWay to create database structure.
Make sure to go through Ktorm documentation to learn how Entity-ies and Tables work.
We'll bind entities to forms via Vaadin Binder, and we'll return Entity instances via DataProvider,
so it's crucial that every table has an entity defined.
Every database call in ktorm-vaadin goes through the db { } block, which opens a Ktorm
transaction against ActiveKtorm.database and exposes a KtormContext(transaction, database)
to the block. The block commits on normal return and rolls back on exception:
val all: List<Employee> = db { database.sequenceOf(Employees).toList() }For typical CRUD you don't need to call db { } directly — the Table<E> extension
helpers in dao.kt wrap it for you:
Employees.create(Employee { name = "Alice" })
Employees.findAll() // List<Employee>
Employees.count() // Int
Employees.single() // single row, fails on 0 or 2+
Employees.deleteAll()If your entities extend ActiveEntity<E> (instead of plain Entity<E>), each instance also
gets:
validate()— runsjakarta.validationconstraints. Annotate getters (@get:NotNull,@get:Size(...)) since Ktorm entities are interfaces.isValid—validate()wrapped in try/catch.save()— callsflushChanges()if the entity has a primary key, otherwise inserts a new row.create()— always inserts.
ActiveEntity requires each entity to expose its Table<E> via the table property. Hibernate
Validator 9+ is required — HV 8 doesn't run validators on interfaces.
The EntityDataProvider provides instances of given entity.
To set the data provider to your Grid:
val dp: EntityDataProvider<Person> = Persons.dataProvider
// optionally set an unremovable filter, to always filter the records.
dp.setFilter(Persons.age gte 18)
grid.dataProvider = dpTo set the data provider to your ComboBox:
val dp: EntityDataProvider<Person> = Persons.dataProvider
// optionally set an unremovable filter, to always filter the records.
dp.setFilter(Persons.age gte 18)
comboBox.setDataProvider(dp.withStringFilterOn(Persons.name))When using Ktorm Reference Bindings,
you can use EntityDataProvider to select one main entity and then reference all left-joined columns
in where clauses. Unfortunately the values of joined entities do not seem to be populated; for example when selecting Employees
from Ktorm documentation, reading Employee.department.name will yield null. That's
where QueryDataProvider comes into play.
To hold a left-join of Employee and Department (taken verbatim from
testapp/src/main/kotlin/testapp/EmployeesRoute.kt):
data class EmployeeDept(val employee: Employee, val department: Department) {
companion object {
fun from(row: QueryRowSet): EmployeeDept = EmployeeDept(
Employees.createEntity(row), Departments.createEntity(row)
)
val dataProvider: QueryDataProvider<EmployeeDept> get() = QueryDataProvider(
{ it.from(Employees).leftJoin(Departments, on = Employees.departmentId eq Departments.id)
.select(*Employees.columns.toTypedArray(), *Departments.columns.toTypedArray()) },
{ from(it) }
)
}
}You need to set the Grid Column key to the Ktorm Column name; that way we can reconstruct the Ktorm expression back from Vaadin's QuerySortOrder:
// Pick `.e.key` when the Grid is backed by EntityDataProvider, `.q.key` for QueryDataProvider.
val idColumn = personGrid.addColumn { it.id }
.setHeader("ID")
.setSortable(true)
.setKey(Persons.id.e.key) // or Persons.id.q.key for QueryDataProvider
dataProvider.setSortOrders(listOf(Persons.name.e.asc, Persons.age.e.asc))One way of adding filters to your grid is to add a Grid header bar just for filter components, then add filter components as cells to the header bar:
// append first header row: the column captions and the sorting indicator will appear here.
personGrid.appendHeaderRow()
// the second header row will host filter components.
val filterBar = personGrid.appendHeaderRow()
val nameFilter = FilterTextField()
val nameColumn = personGrid.addColumn { it.name }
.setHeader("Name")
.setSortable(true)
.setKey(Persons.name.e.key)
filterBar.getCell(nameColumn).setComponent(nameFilter)
nameFilter.addValueChangeListener { updateFilter() }Then, when any of the filter component
changes, you need to calculate the ColumnDeclaring<?> from the values of all filters as follows:
private fun update() {
val conditions = mutableListOf<ColumnDeclaring<Boolean>?>()
if (nameFilter.value.isNotBlank()) {
conditions += Employees.name.ilike(nameFilter.value.trim() + "%")
}
if (jobFilter.value.isNotBlank()) {
conditions += Employees.job.ilike(jobFilter.value.trim() + "%")
}
conditions += Employees.hireDate.between(hireDateFilter.value)
conditions += Employees.salary.between(salaryFilter.value.asLongInterval())
dataProvider.setFilter(conditions.and())
}Alternatively, you might have a FilterBean populated by the dialog. Whenever the "Apply" button
of the dialog is pressed, you populate the FilterBean from the components; you can then
calculate the ColumnDeclaring from the bean in a similar way as above.
See the bundled testapp example
project for more details.
This project offers additional filter components:
BooleanFilterField: allows the user to selecttrueorfalseor clear the selection and disable this filter.EnumFilterField: allows the user to select one or more enum constants. If all constants or no constant is selected, the filter is disabled.FilterTextField: a simple TextField filter. Usually matched with a column using thelikeIgnoreCase()operator.DateRangePopup: allows the user to select a date range. The range may be open (only the 'from' or 'to' date filled in, but not both). Usually matched using thebetween()operator.NumberRangePopup: allows the user to select a numeric range. The range may be open (only the 'from' or 'to' number filled in, but not both). Usually matched using thebetween()operator.
Note on
ilike: thewithStringFilterOn(...)helpers and several filter examples useorg.ktorm.support.postgresql.ilike.ktorm-vaadinpullsktorm-support-postgresqlin transitively so the operator is always available — H2 understandsiliketoo, which is why tests pass. If you target a database that doesn't (e.g. MySQL), substitute your own case-insensitive comparison when composing the filter expression.
Ktorm entities are interfaces, but have all the usual getters/setters so that they work with the Binder. However, make sure to have Hibernate-Validator 9+ since HV 8 doesn't run validators on interfaces.
The form example below uses two helpers from Karibu-DSL (not from ktorm-vaadin):
beanValidationBinder<T>()— a thin wrapper around Vaadin'sBeanValidationBinder<T>.HasBinder<T>— marker interface that letsbind(binder)find the binder from the enclosing form. Implement it on your form class (as below) or pass the binder explicitly.
The bind(...) and toId(...) calls inside the form are provided by ktorm-vaadin — see
binder.kt. bind(column) binds the field to the entity property by name (rather than
getter/setter lambdas) so that BeanValidationBinder can run JSR-303 against it; toId(idColumn)
adapts an entity-valued field to a foreign-key ID? column.
A very simple example of an employee form:
class EmployeeForm : FormLayout(), HasBinder<Employee> {
override val binder = beanValidationBinder<Employee>()
init {
textField("Name") {
setId("name")
bind(binder).bind(Employees.name)
}
textField("Job") {
setId("job")
bind(binder).bind(Employees.job)
}
comboBox<Employee>("Manager") {
setId("manager")
setItems(Employees.dataProvider.withStringFilterOn(Employees.name))
itemLabelGenerator = ItemLabelGenerator { it.name }
bind(binder).toId(Employees.id).bind(Employees.managerId)
}
datePicker("Hire Date") {
setId("hireDate")
bind(binder).bind(Employees.hireDate)
}
integerField("Salary") {
setId("salary")
bind(binder).withConverter(IntegerToLongConverter()).bind(Employees.salary)
}
comboBox<Department>("Department") {
setId("department")
setItems(Departments.dataProvider.withStringFilterOn(Departments.name))
itemLabelGenerator = ItemLabelGenerator { it.name }
bind(binder).toId(Departments.id).bind(Employees.departmentId)
}
}
}Notice how the Manager and Department ComboBoxes populate themselves, and how they
bind to an Int field which holds the ID of the manager/department.
This project contains a bundled app named testapp. You can run it easily:
$ ./gradlew testapp:runThe sources are simple and easy to follow and demo all features of ktorm-vaadin.
For development setup, running tests, and release procedure see CONTRIBUTING.md. Underlying APIs are documented in the Ktorm docs and the Vaadin Binder docs.