Exposed DB
- Kotlin course
- Advanced
- Lesson 81/83
- 6 min
Write the database layer with Exposed.
DB abstraction with UsersTable
The third lesson adds an object named UsersTable. It mimics Exposed's table abstraction at a small scale. UserRepo now uses UsersTable instead of a direct list.
Example: object UsersTable { val rows = mutableListOf<User>(); fun insert(u) { rows.add(u) }; fun select(id) = rows.find { it.id == id } }. UserRepo.save now calls UsersTable.insert(u). This separation splits business logic from the data layer.
UserRepo vs UsersTable separation
UserRepo handles business logic; UsersTable handles data storage. This separation makes it easy to swap the table for a real database later. During tests UsersTable can be replaced with a fake; you keep the repo unchanged to isolate behavior.
Example: UserRepo.save(u) { UsersTable.insert(u) }. UserRepo.find(id) { UsersTable.select(id) }. You can wire UsersTable to a real DB without changing the repo. This pattern is the foundation of layered architecture.
Practice Add UsersTable and wire UserRepo
Keep the existing User, UserRepo and Api. Add object UsersTable with val rows = mutableListOf<User>(), fun insert(u: User) and fun select(id: Int): User?. Change UserRepo's save and find methods to call UsersTable.insert and UsersTable.select. main stays the same: repo.save(User(1, "Alex")) and println(api.route("/users/1")).
Starter code
data class User(val id: Int, val name: String)
class UserRepo {
private val users = mutableListOf<User>()
fun save(u: User) { users.add(u) }
fun find(id: Int): User? = users.find { it.id == id }
}
class Api(val repo: UserRepo) {
fun route(path: String): String {
return when {
path.startsWith("/users/") -> {
val id = path.removePrefix("/users/").toInt()
repo.find(id)?.name ?: "not found"
}
else -> "404"
}
}
}
fun main() {
val repo = UserRepo()
repo.save(User(1, "Alex"))
val api = Api(repo)
println(api.route("/users/1"))
}
// add UsersTable object, wire UserRepo to it
// keep main the sameSample solution
data class User(val id: Int, val name: String)
object UsersTable {
val rows = mutableListOf<User>()
fun insert(u: User) { rows.add(u) }
fun select(id: Int): User? = rows.find { it.id == id }
}
class UserRepo {
fun save(u: User) { UsersTable.insert(u) }
fun find(id: Int): User? = UsersTable.select(id)
}
class Api(val repo: UserRepo) {
fun route(path: String): String {
return when {
path.startsWith("/users/") -> {
val id = path.removePrefix("/users/").toInt()
repo.find(id)?.name ?: "not found"
}
else -> "404"
}
}
}
fun main() {
val repo = UserRepo()
repo.save(User(1, "Alex"))
val api = Api(repo)
println(api.route("/users/1"))
}Expected output
Alex
Questions and answers
Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.
1 Match the UsersTable parts
| Term | Meaning |
|---|---|
object UsersTable |
Singleton table |
rows |
Row list |
insert(u) |
Insert |
select(id) |
Query |
2 What is the main reason to separate UserRepo and UsersTable?
- Increase speed
- Avoid tests
- Write less code
- Layered architecture Correct answer
Explanation: The separation enables layered architecture.
3 Complete the object UsersTable definition
____ UsersTable { val rows = mutableListOf<User>() }
object UsersTable { val rows = mutableListOf<User>() }
Explanation: object UsersTable declares a singleton table.