klite

klite-jdbc

Provides simple extension functions for JDBC standard classes for a simple way to query a DB. Transaction management is also supported.

A more concise alternative to Spring’s JdbcTemplate, and also includes SQL statement generation. Tested mostly with PostgreSQL.

Registration with Server instance:

  use<DBModule>() // to register a DataSource and connection pool using Config variables.
  use<DBMigrator>() // to migrate the DB using .sql files or changesets in code, see below
  use<RequestTransactionHandler>() // to enable per-request transactions

Stand-alone usage without klite-server is also possible:

  val db = PooledDataSource(...) // or ConfigDataSource / HikariDataSource
  DBMigrator(db).migrate()

Usage:

  // obtain the registered DataSource (or declare it as a constructor parameter in your Repository class)
  val db = require<DataSource>()

  // basic insert into a table
  db.insert("table", mapOf("col1" to value, "col2" to value2))

  // insert of all fields of an entity
  db.insert("table", entity.toValues())
  // redefine some entity field value before inserting
  db.upsert("table", entity.toValues(Entity::field to "another value"))

  // basic query from a table (mapper runs in context of ResultSet)
  db.select("table", "column" to value) { MyEntity(getId(), getString("column")) }
  // or if all entity properties are contained in the result set
  db.select("table", "column" to value) { create<MyEntity>() }
  // if you need to add several criteria for a single column (map key), use SqlExpr and friends
  db.select("table", sql("(column is null or column >= ?)", 10))
  // where can also be written in a type-safe way, and some common operators are available
  db.select("table", MyEntity::column gte 123) { create<MyEntity>() }

  // where tokens are "and"-ed together, nulls filtered out, so convenient conditionals are possible
  db.select("table", MyEntity::column gte 123, (MyEntity::other to something).takeIf { something != null }) { create<MyEntity>() }
  // or another option is to use list filtering
  db.select("table", notNullValues(MyEntity::column gte 123, MyEntity::other to something)) { create<MyEntity>() }

  // "or" is also possible
  db.select("table", MyEntity::column gte 123, or(MyEntity::other to something, "hello" to "world")) { create<MyEntity>() }

  // more advanced query with suffix and create() auto-mapper
  db.select("table", "col1" to notNull, "col2" gte  value, suffix = "order by col3 limit 10") { create<MyEntity>() }
  // single row, with joins, etc
  db.select("table1 left join table2 on table1.id = table2.megaId", listOf("table2.field" to value), "limit 1") {
    create<MyEntity>(MyEntity::other to create<OtherEntity>("table2.")) // you can provide table alias to create (PostgresSQL only)
  }.first()

  // or you can write full sql manually using db.query() and db.exec()

Note: before Klite 1.5 query/select functions had the opposite meaning, but users found select(“select…”) not nice.

See all available functions.

All the above can run without a transaction: in this case, every query will obtain and release its own connection from the pool, in autocommit mode. If Transaction is active, then it will obtain a connection on first use, set autoCommit=false and reuse it until the transaction is closed with either commit or rollback.

In query mappers you can either use ResultSet methods and extensions to build your entities or use the ResultSet.create. Likewise, Any.toValues is provided to simplify conversion of entities to Maps for use with insert/update/upsert.

JdbcConverter can be used to register conversion of custom types to be sent to the DB.

Base entity classes and repositories

BaseRepository and CrudRepository are provided for convenience.

They work with entity classes implementing BaseEntity<ID>, where you can provide your own ID class, like UUID or TSID.

Experimental:

Migrations

DBMigrator is provided for simple SQL-based DB migrations, it supports a very similar syntax to Liquibase SQL Format, see sample.

Advantages over Liquibase:

Advantages over Flyway:

Not supported:

Different DB user for migration and running

It is better for security to use a user with fewer rights for the running application.

Your changesets can actually create this user and grant only select/insert/update permissions, but not e.g. create/drop table.

  // start dev db, for developer convenience
  if (Config.isDev) startDevDB()
  // migrate with the default all-rights user
  use<DBMigrator>()
  // use app user for the application DataSource and connection pool
  useAppDBUser()
  use(DBModule {
    // override any other connection pool settings
  })

Best practices

It’s more convenient to treat DB objects as maintainable code, e.g.