Skip to Content
Course content

79: Play with Slick for Database Access

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Slick can be a bit of a head-scratcher at first because it tries to bridge two completely different worlds: the relational world of SQL and the functional world of Scala. It's not exactly an ORM in the way Hibernate is; it's more of a functional-relational mapper. Today, let's build a simple inventory tracker for a small bookshop to see how this actually feels in practice.

Defining the Book Schema

First, we need to tell Slick what our data looks like. I prefer defining my tables as objects. In this case, we'll track books with an ID, a title, an author, and the current stock count. Note how the Table trait requires us to define the schema in the * column method.

import slick.jdbc.H2Profile.api._

case class Book(id: Long, title: String, author: String, stock: Int)

class Books(tag: Tag) extends Table[Book](tag, "BOOKS") {
  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def title = column[String]("TITLE")
  def author = column[String]("AUTHOR")
  def stock = column[Int]("STOCK")

  def * = (id, title, author, stock) <> (Book.tupled, Book.unapply)
}

val books = TableQuery[Books]

The <> operator is just a fancy way of telling Slick how to map the database columns to our case class and back again. It's a bit of "Slick magic" that saves us from writing tedious mapping code.

Connecting to the Database

For this example, I'm using H2 because it's lightweight and doesn't require you to spin up a separate server. I'll set up the database connection and make sure the table actually exists. If you've used Slick before, you know that defining the table object doesn't actually create the table in the DB—you have to do that explicitly.

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

val db = Database.forH2Pure("jdbc:h2:mem:bookshop;DB_CLOSE_DELAY=-1", user = "sa", password = "")

// Create the table
db.run(books.schema.create)

The "Nothing is Happening" Moment

Now, let's write a method to add a new book to our inventory. I'm going to make a mistake here—one that I've made countless times—and I want you to see it. I'll write a function that defines the action, but I'll forget the execution step.

def addBook(title: String, author: String, stock: Int) = {
  books += Book(0L, title, author, stock) 
}

// Calling it
addBook("The Scala Way", "Jane Doe", 10)
println("Book added!")

If you run this, it will print "Book added!", but if you check your database, it'll be empty. Why? Because in Slick, books += ... doesn't actually execute the query. It returns a DBIOAction—essentially a blueprint of the work to be done. It's a lazy description of a database operation.

To actually make it happen, we have to pass that action to db.run(). Here is the corrected version:

def addBook(title: String, author: String, stock: Int): Future[Int] = {
  val action = books += Book(0L, title, author, stock)
  db.run(action) // This is where the actual SQL is sent to the DB
}

// Now we handle the Future
addBook("The Scala Way", "Jane Doe", 10).map { _ =>
  println("Actually added to the database now.")
}

Finding Low-Stock Books

The real power of Slick comes when you start querying. Instead of writing raw SQL strings, you use Scala methods. I want to find all books where the stock is less than 5 so we know what to reorder. I'll use the filter method, which Slick translates into a WHERE clause.

def getLowStockBooks(): Future[Seq[Book]] = {
  val query = books.filter(_.stock < 5)
  db.run(query.result)
}

// Let's test it by adding some books and querying
val setup = for {
  _ <- addBook("Scala Basics", "Alice", 10)
  _ <- addBook("Advanced FP", "Bob", 3)
  _ <- addBook("Type Theory", "Charlie", 2)
  lowStock <- getLowStockBooks()
} yield lowStock

setup.onComplete {
  case scala.util.Success(results) => 
    results.foreach(b => println(s"Reorder: ${b.title} (Only ${b.stock} left!)"))
  case _ => println("Something went wrong")
}

Notice how query.result is used. books.filter(...) returns a Query object, but to actually get the rows, you need to call .result to turn it into a DBIOAction that can be executed by the database.




📋 Practical Task

Implement a Stock Update Method for the Bookshop

You have the basic inventory system, but you can't update the stock when a book is sold. Your task is to add a new method to the system called updateStock(bookId: Long, newStock: Int).

Requirements:

  • The method should use books.filter(_.id === bookId) to locate the specific book.
  • Use the .map method on the query to update the stock column.
  • The method must return a Future[Int] (the number of rows updated).
  • Ensure the action is actually executed using db.run().

Testing your fix: Add a book, retrieve its generated ID, and then call your updateStock method to change its quantity from 10 to 8. Verify the change by querying the book again.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.