Scala and Slick - An Introduction

Published:

In this article, I am going to explain working with Scala and slick, this is a tutorial to help beginners get a hang of slick as well as introduce to the concept of Scala Programming.

Why I love scala is because of its expressiveness, it allows developers to program with utmost ease.

Slick and it’s properties that make it awesome:

Slick allows you to do database query as though you are working with Scala collections i.e it gives you a bonus point of being able to run map, filter, flatmap etc. on your stored data.

Apart from this, you get compilation Type Checks, hence you don’t have to go into running your application with some hidden bugs.

Also, you get a non-blocking request as part of the bonus for using slick, as slick uses a DBIO Monadic API, you are able to chain operations that you can later flatten out

Let’s put this into an example, say you have a list and you can apply map to generate a list of list as shown below.

The above List[Int] produces a List[List[Int]] as per our example, flatmap flattens out the inner List, hence you don’t have to do map then flatten. So, let’s say you do a DB query that can return with map List[List[SomeType]], because of the monadic API in slick you get a flatmap over many of your queries.

Now that we have established what benefits we can get with Slick let’s do a quick rundown of an example. We will need a db for this, you can use anyone be it MYSQL, PostGres, MariaDB. I am biased towards MariaDB hence our example will be using MariaDB. To set up Mariadb, you can refer to this article.

Now that we have MariaDB installed we can start writing our scala application to interact with MariaDB.

In this article, I intend to introduce how to build something close to what a real-life software engineer get’s to build in practice, so, I like to use one of scala seed project to kickstart my scala application.

sbt -Dsbt.version=0.13.15 new akka/akka-http-quickstart-scala.g8

Once you fill out the necessary information, including package name, version of Akka and name of the project, you can import the project into Intelij.

This is as simple as open Intelij => Files => Open and navigate to your newly created file.

This seed project gives you a lot free of charge which we will just basically extend for our case since our main aim is to use slick with this minimal web service.

Slick and Scala

To use Slick in your project, you need to add Slick package as dependency and also add the driver for the database you are using, in our case we are using MariaDB hence MYSQL driver it is, for PostGres you can get the driver from Maven Repository. Finally, our build.sbt should look like this:

libraryDependencies ++= Seq(

      //Akka packages
      "com.typesafe.akka" %% "akka-http"            % "10.1.5",
      "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.5",
      "com.typesafe.akka" %% "akka-http-xml"        % "10.1.5",
      "com.typesafe.akka" %% "akka-stream"          % "2.5.18",

      //Database packages
      "com.typesafe.slick" %% "slick"               % "3.2.1",
      "com.typesafe.slick" %% "slick-hikaricp"      % "3.2.1",
      "mysql"              % "mysql-connector-java" % "5.1.34",

      //Migration tool
      "org.flywaydb"       %  "flyway-core"         % "5.0.7",

      //Test packages
      "com.typesafe.akka" %% "akka-http-testkit"    % "10.1.5" % Test,
      "com.typesafe.akka" %% "akka-testkit"         % "2.5.18"     % Test,
      "com.typesafe.akka" %% "akka-stream-testkit"  % "2.5.18"     % Test,
      "org.scalatest"     %% "scalatest"            % "3.0.5"         % Test,
)

Next we will like to create our own Registry Actors and Routes that integrates with slick, hence delete the files UserRegistoryActor.scala and UserRoutes.scala in your seed project.

Side Notes: The Registry actors are basically actors that help you manage your requests so that you have a non-blocking and concurrent web-service, while Routes are basically you web service address

Once we have this, let’s do a quick configuration for the db to connect to our application. You keep your configuration files in application.conf in resources folder: Our sample configuration is as shown below:

database {
  url = "jdbc:mysql://localhost:3306/sampledb?useUnicode=true&characterEncoding=UTF-8"
  driver = "com.mysql.jdbc.Driver"
  user = "root"
  password = "usedbsetupduringinstallatonofdb"
  numThreads = 4
  maxConnections = 8
  minConnections = 2
  registerMbeans = true

  numberOfThreads = 10
}

Also, it’s good to use a migration package since you still need to have a mapping of the representation of your DB columns when slick wants to create a new table in your db

We are using flyway package to handle Database migration. For anyone with experience with python, flyway is just like alembic. Once you had the package into your sbt file we should be fine.

Next will be to create an sql for your table, you can add every new version of your sql update in your resource folder. Also, there is an important way in naming your sql file this is to preserve versioning, the syntax for naming is VX__Information.sql where VX is the latest version of your migration and its changes, and Information is basically what the version is adding. A concrete naming example is V1__Create_user_table.sql, a sample of such file is as shown below:


-- -----------------------------------------------------
-- Table `users`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `users` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(200) NOT NULL,
`password` VARCHAR(300) NOT NULL,
`location` VARCHAR(200) NOT NULL,
`gender` INT(4) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `username_UNIQUE` (`username` ASC))
ENGINE = InnoDB;

After this we can go ahead and write our flyway and database configuration in our scala app, so that we can write our first migration.

It will be good to wrap our general db migration in a Trait since both flyway and database configuration shares attributes from it


//Load Configuration files

import com.typesafe.config.ConfigFactory

trait Config {
      
      //set's up ConfigFactory to read from application.conf
      private val config = ConfigFactory.load() 
      
      //Get configurations key vales for database
      private val databaseConfig = config.getConfig("database")

      val databaseUrl = databaseConfig.getString("url")
      val databaseUser = databaseConfig.getString("user")
      val databasePassword = databaseConfig.getString("password")
}

In another file for Migration.scala, we then have the flyway migration, also can be wrapped in a Trait since its methods and properties will be used in a later phase.


import org.flywaydb.core.Flyway

trait MigrationConfig extends Config {

      private val flyway = new Flyway()

      flyway.setDataSource(databaseUrl, databaseUser, databasePassowrd)

      //I am adding this in case you make a mistake in your migration script, 
      //do a flyway.repair() before you migrate again
      //flyway.repair()

      def migrate() = flyway.migrate()
      def reloadSchema() = {
            flyway.clean()
            flyway.migrate()
      }
}

Now we should also create this configuration for our database

trait DatabaseConfig extends Config {

      val driver = slick.jdbc.MySQLProfile

      import driver.api._

      def db = Database.forConfig("database")
      //We make database session implicits so that it will
      //be always be available for any class that extends Database Config.
      implicit val session: Session = db.createSession()
}

Now that we have all this set up we can now create our TableQuery for our user SQL file and also implement the various the operations we will be applying on the users table. TableQuery class allows you to map your Table pattern in your code to your database table so that we continue to write db queries as scala syntax and we get the promise of writing queries as scala collection which slick promised us.

Therefore, our UserTable.scala looks like this:

import com.akkaactors.db.models.User
import slick.jdbc.MySQLProfile.api._

//We can define a type alias for userID  
package object definition {
      type UserId = Long
}

//Also we need a case class that act as a place holder for this column table
case class User(id: Option[UserId], username: String, password: String, location: String, gender: Int)



class UsersTable(tag: Tag) extends Table[User](tag, "users") {

  def id = column[UserId]("id", O.PrimaryKey, O.AutoInc)
  def username = column[String]("username")
  def password = column[String]("password")
  def location = column[String]("location")
  def gender = column[Int]("gender")

  //Add id to *
  def * = (id.?, username, password, location, gender) <> ((User.apply _).tupled, User.unapply)
}

The case class is important since it’s what connects other parts of our application with the table name in db i.e your User case class now embodies the columns of your user table in db hence you can apply your scala functions on User as though they are columns in your db.

To define the operations on your user table we create another file UsersDao.scala. Also, usually for every query to db with slick, you need to do a db.run() to execute the query, you can abstract away that portion by providing it as an implicit parameter such that when you just call your operations, you app picks it up as an action and implement db.run(operation) on it without you having to always call db.run().

import slick.jdbc.MySQLProfile.api._

import scala.concurrent.Future

trait BaseDao extends DatabaseConfig {

      val usersTable = TableQuery[UsersTable]


      protected implicit def executeFromDb[A](action: SqlAction[A, NoStream, _ <: slick.dbio.Effect]): Future[A] = {
    db.run(action)
  }

}

object UsersDao extends BaseDao {

  def findAll: Future[Seq[User]] = usersTable.result
  def create(user: User): Future[UserId] = usersTable.returning(usersTable.map(_.id)) += user
  def findById(userId: UserId): Future[User] = usersTable.filter(_.id === userId).result.head

  def delete(userId: UserId): Future[Int] = usersTable.filter(_.id === userId).delete
}

With this, we are almost done, we can now make requests that will interact with db based on whatever operation we intend to carry out.

So our toy example will be implemented in such a way that requests to my services are sent to an actor that will perform non-blocking operations on the request.

The remaining code is based more on implementing a web service with akka-http, you can get a glimpse into the very fundamental of akka-http in one of my article here. I basically extended the approach in the article to leverage the use of Actor, so that we move towards a concurrent approach to developing web services.

Let’s create our own UserRegistryActor.scala

object UserRegistryActor{
  final case class ActionPerformed(description: String)
  final case object GetUsers
  final case class CreateUser(user: User)
  final case class GetUser(name: UserId)
  final case class DeleteUser(name: UserId)

  //This is you registering the Actor
  def props: Props = Props[UserRegistryActor]
}

class UserRegistryActor extends JsonSupport with Actor with ActorLogging {
  import UserRegistryActor._
  import context.dispatcher
  //DB Implementation here

  def receive: Receive = {

   //Get all users
    case GetUsers =>
      val mysender = sender
      val allUsers = UsersDao.findAll
      allUsers.onComplete {
        case Success(usr) => mysender ! Users(usr)
        case Failure(failureUsr) => println("Data not found to find all Users in Database")
      }

   //Create User
    case CreateUser(user) =>
      UsersDao.create(user)
      sender() ! ActionPerformed(s"User ${user.username} created.")

    //Get a particular User
    case GetUser(id) =>
      val user = UsersDao.findById(id)
      val userSender = sender
      user.onComplete {
        case Success(usr) => userSender ! usr
        case Failure(failureUsr) => println(s"$id user not found")
      }

   //Delete a user based on an id
    case DeleteUser(id) =>
      val user = UsersDao.delete(id)
      val delSender = sender
      user.onComplete {
        case Success(del) => delSender ! ActionPerformed(s"User $id deleted")
        case Failure(delUser) => println(s"Unable to Delete user $id")
      }
  }
}

Finally, the entity that connects your client to your backend service is the routes. So we will implement the routes in Routes.scala


  //Json Support is important for marshalling your requests either json, xml into a case class with akk-http
   trait UserRoutes extends JsonSupport{

         implicit def system: ActorSystem

         def userRegistryActor: ActorRef

         //Actor ask needs this to ever function
         implicit lazy val timeout = Timeout(5.seconds)


         def userRoutes: Route = pathPrefix("users") {
            pathPrefix("enroll-user") {pathEnd {
                  concat(
                  get {
                           val users: Future[Users] =
                           (userRegistryActor ? GetUsers).mapTo[Users]
                           complete(users) },
                  post { entity(as[User]) { user =>
                            val userCreated = (userRegistryActor ? CreateUser(user)).mapTo[ActionPerformed]
                            onSuccess(userCreated) { performed =>
                                   log.info(s"Created User [${user.username}]: ${performed.description}")
                            complete(StatusCodes.Created, performed.description)}}})
                     } ~ 
                  path(IntNumber) { id =>
                  get {
                        val maybeUser: Future[User] = (userRegistryActor ? GetUser(id)).mapTo[User]
                        rejectEmptyResponse {complete(maybeUser)}
                  } ~
                  delete {
                        val userDeleted: Future[ActionPerformed] = (userRegistryActor ? DeleteUser(id)).mapTo[ActionPerformed]
                        onSuccess(userDeleted) { performed =>
                        log.info(s"Deleted user $id", performed.description)
                        complete(StatusCodes.OK, performed)
                        }
                     }
                  }
               }
         }
      }

With this we have built a minimal backend. We can scale this by adding more operations based on request and what we intend to achieve, also we can interact with more tables on our DB by following the same approach of creating TableQuery.

You can play around with the codes for this article by cloning the github repository

Leave a Comment