Over the last few weeks I have been writing my first Scala application – a RESTful service backed by a SQL database for persistent storage. After some googling around the ‘Scala way’ to interact with a SQL database, I came across a modern database access library for Scala, called Slick.
Slick is a really clever library. Not only does it provide you with a way to perform operations on database tables by writing familiar Scala code – in the same way that you would perform operations on Scala collections – but, it also lets you do this in a type-safe way. Effectively, Slick is compiling Scala code to SQL. Consider a simple SQL query which selects a row by ID:
1 |
SELECT username FROM users WHERE userId = 10 |
With Slick, we can use the familiar filter and map methods to write this query – it’s just like we are using the Scala collections API:
1 |
val query = users.filter(_.userId === 10).map(_.username) |
There is a key difference – if users
were a normal Scala collection, then we would probably observe that _.userId
has type Int
, the literal 10
has type Int
and _.username
has type String
. But here, users
is not a Scala collection, it is an instance of a Slick table query. And we don’t actually have Int
s and String
s to use in our filters and maps, since we are building a query that has not yet been executed. So what types are we actually using?
To achieve type safety, behind the scenes Slick is ‘lifting’ types into its own parameterised type, called Rep
. From our table model, Slick knows that the userId column stores integers, so the type of _.userId
is Rep[Int]
. This can be read ‘representation of Int’. Since the ===
function on Rep[Int]
expects another Rep[Int]
, an implicit function is used to ‘lift’ the plain literal 10
into a Rep[Int]
. If we try to compare _.userId
with something that cannot be converted into a Rep[Int]
, then the compiler will complain. This is how Slick provides a type safe DSL for writing database queries. Similarly, the type of the username column has type Rep[String]
and so if we run this query we will get back a result of type String
.
This really demonstrates the power and extensibility of the Scala language, as it simply isn’t possible to achieve the same thing in other languages. Something similar has been done before by Microsoft, with LINQ for C#. However, Microsoft didn’t achieve LINQ by using the C# language itself; they had to build the functionality into the C# compiler.
While this feature of Slick really stands out, there are many other reasons to use the library:
- Full support for all popular SQL implementations
- Handling of sessions and transactions. You can plugin a connection pool implementation such as HikariCP
- Protection against SQL injection. When using plain SQL, all parameters are converted to SQL bind/substitution variables
- An asynchronous API for performing database operations
The asynchronous API has been introduced very recently in Slick 3 and has seen Slick move away from a blocking API and instead towards an API which leverages Scala futures. This was perfect for my application, a RESTful web service written on top of Spray – a toolkit for building services based on REST/HTTP. A Future[Response]
could be returned back to my request handling logic and Spray’s in-built marshaller for futures will delay marshalling until the response becomes available, at which point it can be marshalled by another thread.
As you might expect, with Slick you can build more complex queries or sequences of database operations using Scala’s for-yield construct. It turns out there are a few ways to do this – you can build queries by composing in either Query, DBIOAction or Future itself and at first I wasn’t sure when to use one over the other.
Composing in Query
When you compose in Query, Slick generates just one SQL statement. For this reason, this approach should be used wherever possible. In the example below, we perform a (implicit) join between a users and purchases table to find all purchases for a specific user.
1 2 3 4 |
val queryResult = (for { u <- users if u.userId === 10 p <- purchases if p.userId === u.userId } yield p).result |
Composing in DBIOAction
In Slick, a DBIOAction corresponds to a single database operation. Composing in DBIOAction results in multiple SQL statements, but just one database connection is used to execute them. This is particularly good if you have a sequence of dependent database operations that you want to execute transactionally. You can compose in DBIOAction and use Slick’s .transactionally
method to execute the queries in a single transaction. This means that if any one of the queries fails, all prior operations will be rolled back – leaving the database in a consistent state. In the example below, we insert a user and then use the ID returned by this insert operation to insert a sequence of purchases, all in a single transaction.
1 2 3 4 |
val queryResult = db.run((for { newId <- (users returning users.map(_.userId)) += user _ <- purchases ++= purchases.map(p => p.copy(userId = Some(newId))) } yield newId).transactionally) |
Composing in Future
With this approach, Slick obtains a connection from the connection pool for each statement, so potentially multiple connections are used. Therefore, composing in Future should only be done if absolutely necessary. In the example below, we perform the same inserts as above. Note that here, it’s not possible to execute the two operations transactionally and a different database connection may be used for each operation. The only real reason to compose in Future would be in the unlikely case that you need to perform some non-Slick related operation in-between the database operations.
1 2 3 4 |
val queryResult = for { userId <- db.run((users returning users.map(_.userId)) += user) _ <- db.run(purchases ++= purchases.map(p => p.copy(userId = Some(newId)))) } yield userId |
In conclusion, with Query resulting in a single SQL statement and DBIOAction resulting in a single connection, for optimal execution prefer Query over DBIOAction over Future.
Like all good things, Slick has some limitations – perhaps the biggest drawback is the lack of control you have over how Slick generates SQL queries from your Scala code. There are cases where Slick fails to generate the most optimal SQL and as a result the query performance will be worse than if you’d just written the SQL yourself. So when query performance is critical, there may be times where it is better to avoid Slick’s SQL generation functionality. Running Slick in debug mode enables you to see the SQL that is being executed, so if you hit a performance issue it is easy to find out whether Slick is the cause. Another limitation I came across is that some queries are currently not expressible with Slick’s Scala abstractions. For example, this fairly common SQL query cannot be expressed in the current release of Slick:
UPDATE table SET field = field + 123
This example is an exception, as the vast majority of common SQL queries can be expressed very easily. The library is still undergoing rapid development, so issues like these are becoming increasingly less common. In cases where it is not possible to correctly express the operation you want to perform using Slick’s abstractions, you can always use Slick’s plain SQL API.