ScalikeJDBC
ScalikeJDBC is a Scala-exclusive database library designed as "a tidy SQL-based database access library for Scala developers" that naturally wraps JDBC APIs and provides easy-to-use APIs. Based on the philosophy that "JDBC stands as a steadfast standard interface. Given its widespread support across most RDBMS, accessing databases remains consistent," it achieves robust and expressive database operations that leverage the full benefits of Scala's type system and functional programming through type-safe QueryDSL and string interpolation SQL queries.
GitHub Overview
scalikejdbc/scalikejdbc
A tidy SQL-based DB access library for Scala developers. This library naturally wraps JDBC APIs and provides you easy-to-use APIs.
Topics
Star History
Library
ScalikeJDBC
Overview
ScalikeJDBC is a Scala-exclusive database library designed as "a tidy SQL-based database access library for Scala developers" that naturally wraps JDBC APIs and provides easy-to-use APIs. Based on the philosophy that "JDBC stands as a steadfast standard interface. Given its widespread support across most RDBMS, accessing databases remains consistent," it achieves robust and expressive database operations that leverage the full benefits of Scala's type system and functional programming through type-safe QueryDSL and string interpolation SQL queries.
Details
ScalikeJDBC 2025 edition provides a more type-safe and functional programming-focused database access layer with complete support for the latest features of Scala 3 and Scala 2.13 (Union Types, Opaque Types, Given/Using, etc.). By adhering to JDBC standards, it enables consistent API usage across any RDBMS including Oracle, SQL Server, PostgreSQL, MySQL, and SQLite. Through type-safe query construction with QueryDSL, intuitive query writing with SQL interpolation, and simple annotation-free design, it strongly supports high-quality Scala application development in enterprise environments. Furthermore, it supports modern reactive application development through relationship management and N+1 problem resolution with scalikejdbc-orm, and asynchronous support (ScalikeJDBC-Async).
Key Features
- JDBC Standard Compliance: Consistent connections with any JDBC-compatible database
- Type-safe QueryDSL: Safe query construction leveraging Scala's type system
- SQL Interpolation: Intuitive and type-safe SQL writing
- Annotation-free: Simple design minimizing learning costs
- Production Ready: Stability for reliable use in real projects
- ORM Features: Advanced object mapping through scalikejdbc-orm
Pros and Cons
Pros
- Consistent API usage across a wide range of databases through JDBC standards
- Leverages benefits of Scala's type system and functional programming
- Can utilize SQL knowledge while benefiting from type safety
- Low learning cost due to annotation-free, simple design
- High reliability with extensive production environment track record
- N+1 problem resolution and relationship management through scalikejdbc-orm
Cons
- Scala-exclusive, not usable for cross-platform development
- Limited modern features compared to other Scala libraries (Doobie, Quill, etc.)
- Asynchronous processing requires separate library (ScalikeJDBC-Async) still in alpha stage
- No support for NoSQL databases
- Advanced ORM features like Hibernate or Active Record not provided by default
- Configuration management may become complex in large-scale Scala projects
Reference Pages
Code Examples
Setup
// build.sbt
ThisBuild / scalaVersion := "2.13.12"
libraryDependencies ++= Seq(
// ScalikeJDBC core
"org.scalikejdbc" %% "scalikejdbc" % "4.3.0",
"org.scalikejdbc" %% "scalikejdbc-config" % "4.3.0",
"org.scalikejdbc" %% "scalikejdbc-streams" % "4.3.0",
// ORM functionality
"org.scalikejdbc" %% "scalikejdbc-orm" % "4.3.0",
// Database drivers
"org.postgresql" % "postgresql" % "42.7.1",
"mysql" % "mysql-connector-java" % "8.0.33",
"com.h2database" % "h2" % "2.2.224",
// Logging
"ch.qos.logback" % "logback-classic" % "1.4.14",
// Testing
"org.scalikejdbc" %% "scalikejdbc-test" % "4.3.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.17" % Test
)
// src/main/resources/application.conf
db {
default {
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://localhost:5432/myapp"
user = "myuser"
password = "mypassword"
# Connection pool settings
poolInitialSize = 10
poolMaxSize = 20
poolConnectionTimeoutMillis = 1000
poolValidationQuery = "select 1 as one"
poolFactoryName = "commons-dbcp2"
}
test {
driver = "org.h2.Driver"
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
user = "sa"
password = ""
}
}
// src/main/scala/config/DBConfig.scala
package config
import scalikejdbc._
import scalikejdbc.config._
object DBConfig {
def initialize(): Unit = {
// Automatic configuration from config file
DBs.setupAll()
// Global settings
GlobalSettings.loggingSQLErrors = true
GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(
enabled = true,
singleLineMode = false,
printUnprocessedStackTrace = false,
stackTraceDepth = 15,
logLevel = Symbol("info"),
warningEnabled = true,
warningThresholdMillis = 3000L,
warningLogLevel = Symbol("warn")
)
}
def close(): Unit = {
DBs.closeAll()
}
}
Basic CRUD Operations
// src/main/scala/models/User.scala
package models
import scalikejdbc._
import java.time.{LocalDateTime, ZonedDateTime}
import java.util.UUID
case class User(
id: Option[Long] = None,
username: String,
email: String,
hashedPassword: String,
isActive: Boolean = true,
createdAt: LocalDateTime = LocalDateTime.now(),
updatedAt: Option[LocalDateTime] = None
)
object User extends SQLSyntaxSupport[User] {
override val tableName = "users"
override val columns = Seq("id", "username", "email", "hashed_password", "is_active", "created_at", "updated_at")
// Column aliases
val u = User.syntax("u")
// Convert ResultSet to User object
def apply(rs: WrappedResultSet): User = User(
id = rs.longOpt("id"),
username = rs.string("username"),
email = rs.string("email"),
hashedPassword = rs.string("hashed_password"),
isActive = rs.boolean("is_active"),
createdAt = rs.localDateTime("created_at"),
updatedAt = rs.localDateTimeOpt("updated_at")
)
def apply(u: SyntaxProvider[User])(rs: WrappedResultSet): User = apply(rs)
// CRUD operations
def create(username: String, email: String, hashedPassword: String)(implicit session: DBSession = autoSession): User = {
val now = LocalDateTime.now()
val id = withSQL {
insert.into(User).namedValues(
column.username -> username,
column.email -> email,
column.hashedPassword -> hashedPassword,
column.isActive -> true,
column.createdAt -> now
)
}.updateAndReturnGeneratedKey.apply()
User(
id = Some(id),
username = username,
email = email,
hashedPassword = hashedPassword,
isActive = true,
createdAt = now
)
}
def findById(id: Long)(implicit session: DBSession = readOnlySession): Option[User] = {
withSQL {
select.from(User as u).where.eq(u.id, id)
}.map(User(u)).single.apply()
}
def findByUsername(username: String)(implicit session: DBSession = readOnlySession): Option[User] = {
withSQL {
select.from(User as u).where.eq(u.username, username)
}.map(User(u)).single.apply()
}
def findByEmail(email: String)(implicit session: DBSession = readOnlySession): Option[User] = {
withSQL {
select.from(User as u).where.eq(u.email, email)
}.map(User(u)).single.apply()
}
def findAll(limit: Int = 100, offset: Int = 0)(implicit session: DBSession = readOnlySession): List[User] = {
withSQL {
select.from(User as u)
.orderBy(u.createdAt.desc)
.limit(limit).offset(offset)
}.map(User(u)).list.apply()
}
def findActiveUsers()(implicit session: DBSession = readOnlySession): List[User] = {
withSQL {
select.from(User as u)
.where.eq(u.isActive, true)
.orderBy(u.username.asc)
}.map(User(u)).list.apply()
}
def update(user: User)(implicit session: DBSession = autoSession): Int = {
withSQL {
scalikejdbc.update(User).set(
column.username -> user.username,
column.email -> user.email,
column.hashedPassword -> user.hashedPassword,
column.isActive -> user.isActive,
column.updatedAt -> LocalDateTime.now()
).where.eq(column.id, user.id)
}.update.apply()
}
def delete(id: Long)(implicit session: DBSession = autoSession): Int = {
withSQL {
deleteFrom(User).where.eq(column.id, id)
}.update.apply()
}
def deactivate(id: Long)(implicit session: DBSession = autoSession): Int = {
withSQL {
scalikejdbc.update(User).set(
column.isActive -> false,
column.updatedAt -> LocalDateTime.now()
).where.eq(column.id, id)
}.update.apply()
}
// Search functionality
def search(query: String, limit: Int = 20)(implicit session: DBSession = readOnlySession): List[User] = {
val pattern = s"%$query%"
withSQL {
select.from(User as u)
.where(
sqls.like(u.username, pattern)
.or.like(u.email, pattern)
)
.and.eq(u.isActive, true)
.orderBy(u.username.asc)
.limit(limit)
}.map(User(u)).list.apply()
}
// Statistics
def countActiveUsers()(implicit session: DBSession = readOnlySession): Long = {
withSQL {
select(sqls.count).from(User as u).where.eq(u.isActive, true)
}.map(_.long(1)).single.apply().getOrElse(0L)
}
}
Related Entities and JOIN Operations
// src/main/scala/models/Post.scala
package models
import scalikejdbc._
import java.time.LocalDateTime
case class Post(
id: Option[Long] = None,
userId: Long,
title: String,
content: String,
published: Boolean = false,
createdAt: LocalDateTime = LocalDateTime.now(),
updatedAt: Option[LocalDateTime] = None
)
object Post extends SQLSyntaxSupport[Post] {
override val tableName = "posts"
override val columns = Seq("id", "user_id", "title", "content", "published", "created_at", "updated_at")
val p = Post.syntax("p")
def apply(rs: WrappedResultSet): Post = Post(
id = rs.longOpt("id"),
userId = rs.long("user_id"),
title = rs.string("title"),
content = rs.string("content"),
published = rs.boolean("published"),
createdAt = rs.localDateTime("created_at"),
updatedAt = rs.localDateTimeOpt("updated_at")
)
def apply(p: SyntaxProvider[Post])(rs: WrappedResultSet): Post = apply(rs)
// Related object including joined data
case class PostWithUser(post: Post, user: User)
def create(userId: Long, title: String, content: String, published: Boolean = false)(implicit session: DBSession = autoSession): Post = {
val now = LocalDateTime.now()
val id = withSQL {
insert.into(Post).namedValues(
column.userId -> userId,
column.title -> title,
column.content -> content,
column.published -> published,
column.createdAt -> now
)
}.updateAndReturnGeneratedKey.apply()
Post(
id = Some(id),
userId = userId,
title = title,
content = content,
published = published,
createdAt = now
)
}
def findById(id: Long)(implicit session: DBSession = readOnlySession): Option[Post] = {
withSQL {
select.from(Post as p).where.eq(p.id, id)
}.map(Post(p)).single.apply()
}
def findByUserId(userId: Long, limit: Int = 50)(implicit session: DBSession = readOnlySession): List[Post] = {
withSQL {
select.from(Post as p)
.where.eq(p.userId, userId)
.orderBy(p.createdAt.desc)
.limit(limit)
}.map(Post(p)).list.apply()
}
def findPublishedPosts(limit: Int = 20, offset: Int = 0)(implicit session: DBSession = readOnlySession): List[Post] = {
withSQL {
select.from(Post as p)
.where.eq(p.published, true)
.orderBy(p.createdAt.desc)
.limit(limit).offset(offset)
}.map(Post(p)).list.apply()
}
// JOIN query: Join posts with user information
def findPostsWithUsers(limit: Int = 20)(implicit session: DBSession = readOnlySession): List[PostWithUser] = {
withSQL {
select
.from(Post as p)
.innerJoin(User as User.u).on(p.userId, User.u.id)
.where.eq(p.published, true)
.and.eq(User.u.isActive, true)
.orderBy(p.createdAt.desc)
.limit(limit)
}.map { rs =>
PostWithUser(
post = Post(p)(rs),
user = User(User.u)(rs)
)
}.list.apply()
}
// Search functionality (full content search)
def searchPosts(query: String, publishedOnly: Boolean = true, limit: Int = 20)(implicit session: DBSession = readOnlySession): List[PostWithUser] = {
val pattern = s"%$query%"
withSQL {
select
.from(Post as p)
.innerJoin(User as User.u).on(p.userId, User.u.id)
.where(
sqls.like(p.title, pattern)
.or.like(p.content, pattern)
)
.and(if (publishedOnly) sqls.eq(p.published, true) else sqls.empty)
.and.eq(User.u.isActive, true)
.orderBy(p.createdAt.desc)
.limit(limit)
}.map { rs =>
PostWithUser(
post = Post(p)(rs),
user = User(User.u)(rs)
)
}.list.apply()
}
def update(post: Post)(implicit session: DBSession = autoSession): Int = {
withSQL {
scalikejdbc.update(Post).set(
column.title -> post.title,
column.content -> post.content,
column.published -> post.published,
column.updatedAt -> LocalDateTime.now()
).where.eq(column.id, post.id)
}.update.apply()
}
def publish(id: Long)(implicit session: DBSession = autoSession): Int = {
withSQL {
scalikejdbc.update(Post).set(
column.published -> true,
column.updatedAt -> LocalDateTime.now()
).where.eq(column.id, id)
}.update.apply()
}
def delete(id: Long)(implicit session: DBSession = autoSession): Int = {
withSQL {
deleteFrom(Post).where.eq(column.id, id)
}.update.apply()
}
}
Aggregate Queries and Statistics
// src/main/scala/models/Analytics.scala
package models
import scalikejdbc._
object Analytics {
case class UserPostCount(user: User, postCount: Long)
case class PostStats(totalPosts: Long, publishedPosts: Long, unpublishedPosts: Long)
case class MonthlyPostCount(year: Int, month: Int, count: Long)
// Get post count by user
def getUserPostCounts(limit: Int = 50)(implicit session: DBSession = readOnlySession): List[UserPostCount] = {
withSQL {
select(
User.u.result.*,
sqls.count(Post.p.id).as("post_count")
)
.from(User as User.u)
.leftJoin(Post as Post.p).on(User.u.id, Post.p.userId)
.where.eq(User.u.isActive, true)
.groupBy(User.u.id)
.orderBy(sqls"post_count".desc)
.limit(limit)
}.map { rs =>
UserPostCount(
user = User(User.u)(rs),
postCount = rs.long("post_count")
)
}.list.apply()
}
// Post statistics
def getPostStats()(implicit session: DBSession = readOnlySession): PostStats = {
val totalPosts = withSQL {
select(sqls.count).from(Post as Post.p)
}.map(_.long(1)).single.apply().getOrElse(0L)
val publishedPosts = withSQL {
select(sqls.count).from(Post as Post.p).where.eq(Post.p.published, true)
}.map(_.long(1)).single.apply().getOrElse(0L)
PostStats(
totalPosts = totalPosts,
publishedPosts = publishedPosts,
unpublishedPosts = totalPosts - publishedPosts
)
}
// Monthly post counts
def getMonthlyPostCounts(months: Int = 12)(implicit session: DBSession = readOnlySession): List[MonthlyPostCount] = {
withSQL {
select(
sqls"EXTRACT(YEAR FROM created_at)".as("year"),
sqls"EXTRACT(MONTH FROM created_at)".as("month"),
sqls.count(Post.p.id).as("post_count")
)
.from(Post as Post.p)
.where.ge(Post.p.createdAt, sqls"NOW() - INTERVAL '$months months'")
.groupBy(sqls"EXTRACT(YEAR FROM created_at)", sqls"EXTRACT(MONTH FROM created_at)")
.orderBy(sqls"year".desc, sqls"month".desc)
}.map { rs =>
MonthlyPostCount(
year = rs.int("year"),
month = rs.int("month"),
count = rs.long("post_count")
)
}.list.apply()
}
// Get active users (with many recent posts)
def getActiveUsers(days: Int = 30, limit: Int = 10)(implicit session: DBSession = readOnlySession): List[UserPostCount] = {
withSQL {
select(
User.u.result.*,
sqls.count(Post.p.id).as("recent_post_count")
)
.from(User as User.u)
.innerJoin(Post as Post.p).on(User.u.id, Post.p.userId)
.where.ge(Post.p.createdAt, sqls"NOW() - INTERVAL '$days days'")
.and.eq(User.u.isActive, true)
.groupBy(User.u.id)
.having(sqls.gt(sqls.count(Post.p.id), 0))
.orderBy(sqls"recent_post_count".desc)
.limit(limit)
}.map { rs =>
UserPostCount(
user = User(User.u)(rs),
postCount = rs.long("recent_post_count")
)
}.list.apply()
}
}
Transaction Processing
// src/main/scala/services/UserService.scala
package services
import scalikejdbc._
import models.{User, Post}
import java.time.LocalDateTime
class UserService {
// User creation with welcome post in transaction
def createUserWithWelcomePost(
username: String,
email: String,
hashedPassword: String
): Either[String, (User, Post)] = {
DB localTx { implicit session =>
try {
// Check for user duplication
User.findByUsername(username) match {
case Some(_) => return Left(s"Username '$username' already exists")
case None =>
}
User.findByEmail(email) match {
case Some(_) => return Left(s"Email '$email' already exists")
case None =>
}
// Create user
val user = User.create(username, email, hashedPassword)
// Create welcome post
val welcomePost = Post.create(
userId = user.id.get,
title = s"Welcome ${user.username}!",
content = "Thank you for joining our platform. This is your first post!",
published = true
)
Right((user, welcomePost))
} catch {
case ex: Exception =>
session.rollback()
Left(s"Failed to create user: ${ex.getMessage}")
}
}
}
// Batch user operations
def batchUpdateUserStatus(userIds: List[Long], isActive: Boolean): Either[String, Int] = {
DB localTx { implicit session =>
try {
val updatedCount = userIds.map { userId =>
if (isActive) {
User.update(
User.findById(userId).getOrElse(return Left(s"User $userId not found"))
.copy(isActive = true, updatedAt = Some(LocalDateTime.now()))
)
} else {
User.deactivate(userId)
}
}.sum
Right(updatedCount)
} catch {
case ex: Exception =>
session.rollback()
Left(s"Batch update failed: ${ex.getMessage}")
}
}
}
// Complex business logic: User deletion and post archiving
def deactivateUserAndArchivePosts(userId: Long): Either[String, (User, Int)] = {
DB localTx { implicit session =>
try {
val user = User.findById(userId).getOrElse(return Left("User not found"))
if (!user.isActive) {
return Left("User is already inactive")
}
// Unpublish all user posts
val archivedPostsCount = withSQL {
scalikejdbc.update(Post).set(
Post.column.published -> false,
Post.column.updatedAt -> LocalDateTime.now()
).where.eq(Post.column.userId, userId)
}.update.apply()
// Deactivate user
User.deactivate(userId)
val updatedUser = User.findById(userId).get
Right((updatedUser, archivedPostsCount))
} catch {
case ex: Exception =>
session.rollback()
Left(s"User deactivation failed: ${ex.getMessage}")
}
}
}
}
Test Implementation
// src/test/scala/models/UserSpec.scala
package models
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
import scalikejdbc._
import scalikejdbc.scalatest.AutoRollback
class UserSpec extends AnyFlatSpec with Matchers with BeforeAndAfterEach with AutoRollback {
override def db(): DB = NamedDB("test").toDB()
override def beforeEach(): Unit = {
super.beforeEach()
// Test data setup
DB localTx { implicit session =>
sql"""
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
hashed_password VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL
)
""".execute.apply()
sql"""
CREATE TABLE IF NOT EXISTS posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
""".execute.apply()
}
}
"User.create" should "create a new user successfully" in { implicit session =>
val username = "testuser"
val email = "[email protected]"
val password = "hashedpassword123"
val user = User.create(username, email, password)
user.id should not be None
user.username should be(username)
user.email should be(email)
user.hashedPassword should be(password)
user.isActive should be(true)
user.createdAt should not be null
}
"User.findById" should "return user when exists" in { implicit session =>
val user = User.create("findtest", "[email protected]", "password")
val foundUser = User.findById(user.id.get)
foundUser should not be None
foundUser.get.username should be("findtest")
foundUser.get.email should be("[email protected]")
}
it should "return None when user does not exist" in { implicit session =>
val foundUser = User.findById(999999L)
foundUser should be(None)
}
"User.findByUsername" should "return user when username exists" in { implicit session =>
User.create("uniqueuser", "[email protected]", "password")
val foundUser = User.findByUsername("uniqueuser")
foundUser should not be None
foundUser.get.username should be("uniqueuser")
}
"User.update" should "update user information correctly" in { implicit session =>
val user = User.create("updatetest", "[email protected]", "password")
val updatedUser = user.copy(
username = "updatedname",
email = "[email protected]"
)
val updateCount = User.update(updatedUser)
updateCount should be(1)
val retrieved = User.findById(user.id.get).get
retrieved.username should be("updatedname")
retrieved.email should be("[email protected]")
retrieved.updatedAt should not be None
}
"User.search" should "find users by username and email" in { implicit session =>
User.create("alice", "[email protected]", "password")
User.create("bob", "[email protected]", "password")
User.create("charlie", "[email protected]", "password")
val searchResults = User.search("alice")
searchResults should have length 1
searchResults.head.username should be("alice")
val emailSearchResults = User.search("example.com")
emailSearchResults should have length 2
}
"User.countActiveUsers" should "return correct count" in { implicit session =>
User.create("active1", "[email protected]", "password")
User.create("active2", "[email protected]", "password")
val inactiveUser = User.create("inactive", "[email protected]", "password")
User.deactivate(inactiveUser.id.get)
val activeCount = User.countActiveUsers()
activeCount should be(2)
}
}
// src/test/scala/services/UserServiceSpec.scala
package services
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
import scalikejdbc._
import scalikejdbc.scalatest.AutoRollback
import models.{User, Post}
class UserServiceSpec extends AnyFlatSpec with Matchers with BeforeAndAfterEach with AutoRollback {
override def db(): DB = NamedDB("test").toDB()
val userService = new UserService()
override def beforeEach(): Unit = {
super.beforeEach()
// Table creation (same as User test)
}
"UserService.createUserWithWelcomePost" should "create user and welcome post in transaction" in { implicit session =>
val result = userService.createUserWithWelcomePost(
"newuser",
"[email protected]",
"hashedpassword"
)
result should be a 'right
val (user, post) = result.right.get
user.username should be("newuser")
user.email should be("[email protected]")
post.userId should be(user.id.get)
post.title should include("Welcome newuser!")
post.published should be(true)
}
it should "fail when username already exists" in { implicit session =>
User.create("existinguser", "[email protected]", "password")
val result = userService.createUserWithWelcomePost(
"existinguser",
"[email protected]",
"password"
)
result should be a 'left
result.left.get should include("Username 'existinguser' already exists")
}
"UserService.batchUpdateUserStatus" should "update multiple users" in { implicit session =>
val user1 = User.create("batch1", "[email protected]", "password")
val user2 = User.create("batch2", "[email protected]", "password")
val user3 = User.create("batch3", "[email protected]", "password")
User.deactivate(user1.id.get)
User.deactivate(user2.id.get)
val result = userService.batchUpdateUserStatus(
List(user1.id.get, user2.id.get),
isActive = true
)
result should be a 'right
result.right.get should be(2)
User.findById(user1.id.get).get.isActive should be(true)
User.findById(user2.id.get).get.isActive should be(true)
User.findById(user3.id.get).get.isActive should be(true)
}
}