A data persistence abstraction layer for Kotlin.
persistence.kt provides a unified API for accessing and persisting data
without coupling your application to a specific persistence library/framework.
dependencies {
implementation("io.github.briangits.persistence:core:<version>")
}Start with your domain models:
data class User(val id: String, val name: String, val email: String)
data class NewUser(val name: String, val email: String)Create a type-safe filter DSL for User:
class UserFilters : Filters<User, UserFilters>(::UserFilters) {
// Shorthands for accessing properries
val id = User::id
val name = User::name
val email = User::email
// You can add custom filter functions here
fun nameIsJohn() = name eq "John"
}Then define a repository:
interface UserRepository : Repository<User, NewUser, UserFilters>(
// Define how a user is uniquely identified
id = { id eq it.id },
// Filters
filters = ::UserFilters,
// Creating a new User from NewUser
create = { User(id = UUID.randomUUID().toString(), name, email) }
)Repositories provide a simple CRUD API for managing entities:
suspend fun userService(repo: UserRepository) {
// Create & Save
val newUser = repo.create { NewUser("Jane Doe", "janedoe@example.com") }
repo.save(newUser)
// Filters users with DSL
val user = repo.find {
email eq "janedoe@gmail.com"
}
// List all matches
val bobs = repo.findAll {
name startsWith "Bob"
}
}Use Persistence.transaction() to execute operation atomically.
Transactions are auto-committed or rolled back when an exception is thrown.
The block has a Transaction as its receiver, allowing you to resolve repositories
that are bound to the transaction.
suspend fun registerUser(persistence: Persistence, newUser: NewUser) {
persistence.transaction {
val repo = get<UserRepository>() // Resolve a repository
if (repo.exists { email eq newUser.email }) throw Exception("User already exists")
repo.save { create(newUser) }
}
}You can also create & manually manage a transaction:
fun registerUser(newUser: NewUser) {
val transaction = persistence.creqteTransaction()
val repo = transaction.get<UserRepository>()
try {
if (repo.exists { email eq newUser.email }) throw Exception("User already exists")
repo.save { create(newUser) }
transaction.commit()
} catch (e: Exception) {
transaction.rollback()
throw e
}
}Filters provides a type-safe DSL for constructing queries using Kotlin property references.
| Operator | DSL Usage | Description |
|---|---|---|
| Equality | prop eq value |
Property equals value (or isNull if value is null) |
prop neq value |
Property not equals (or isNotNull if value is null) |
|
| Comparison | prop gt / gte value |
Greater than / Greater than or equal |
prop lt / lte value |
Less than / Less than or equal | |
prop.between(a, b) |
Value is between a and b (inclusive) |
|
| String | prop contains str |
Substring search |
prop startsWith str |
Prefix search | |
prop endsWith str |
Suffix search | |
prop matches regex |
Regular expression match | |
| Collections | prop in values |
Value is in the provided collection |
prop notIn values |
Value is NOT in the provided collection | |
| Nullability | prop.isNull() |
Property is null |
prop.isNotNull() |
Property is not null |
Filters cab be grouped using:
allOf- ANDoneOf- ORnot- NOT
Multiple expressions at the top level of a filter block are implicitly grouped using allOf.
val users = repo.findAll {
oneOf {
name startsWith "Jane"
allOf {
email endsWith "@company.com"
name endsWith "Doe"
}
}
}UnitOfWork coordinates operations across multiple repositories
and provides lifecycle hooks around transaction completion.
Add the dependency:
dependencies {
implementation("io.github.briangits.persistence:uow:<version>")
}Define a unit of work:
class MyUnitOfWork(transaction: Transaction) : UnitOfWork<MyUnitOfWork>(transaction) {
val users = get<UserRepository>()
val posts = get<PostRepository>()
init {
afterCommit { /* Do some cleanup */ }
}
}You can also register hooks for individual operations in th execute() or run() blocks:
suspend fun complexOperation(uow: MyUnitOfWork) {
uow.run {
val user = users.find { id eq "123" }
// ... perform changes ...
beforeCommit {
println("Preparing to commit changes for ${user?.name}")
}
afterCommit {
println("Transaction successful!")
}
}
}persistence.kt is designed to be implementation-agnostic.
The core library defines the API used by your application layers, while adapters provide the actual database or storage implementation.
Currently supported implementations include:
- Exposed - Database persistence for Kotlin/JVM using JetBrains Exposed.