Skip to content

Repository files navigation

persistence.kt

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.


Quick Start

1. Add the dependency

dependencies {
    implementation("io.github.briangits.persistence:core:<version>")
}

2. Define your Domain

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) }
)

3. Basic CRUD Operations

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"
    }
}

3. Transactional Context

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
    }
}

Filter DSL

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

Logical Grouping

Filters cab be grouped using:

  • allOf - AND
  • oneOf - OR
  • not - 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"
        }
    }
}

Unit of Work

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!")
        }
    }
}

Implementations

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.

License

Apache-2.0

About

Data persistence abstraction for Kotlin

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages