scala Tutorial
Building an Expense Tracker with Scala 3 - Part 2: Database & Docker
In this series
- •Building an Expense Tracker with Scala 3 - Part 1: Setup
- •Building an Expense Tracker with Scala 3 - Part 2: Database & Docker
- •Building an Expense Tracker with Scala 3 - Part 3: Authentication
- •Building an Expense Tracker with Scala 3 - Part 4: Expenses & Categories
- •Building an Expense Tracker with Scala 3 - Part 5: Internationalization & Testing
In Part 1, we set up our Scala 3 project. Now, we will configure our database infrastructure using Docker Compose and connect our application to MySQL.
A robust backend needs a reliable place to store data. We chose MySQL for its ubiquity and reliability, and Docker Compose to make our development environment reproducible. No more "it works on my machine" issues!
Docker Compose Setup
We'll use Docker Compose to spin up a MySQL instance easily. This saves us from installing MySQL directly on our host machine and keeps our project self-contained.
Create a docker-compose.yml file in the project root:
version: '3.8'
services:
db:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: expense_tracker
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
Key points:
- mysql:8.0: We're using a specific version to ensure consistency.
- volumes: We map a volume
db_datato persist our data even if we destroy the container. - ports: We expose port 3306 so our Play application (running on the host) can talk to the database.
To start the database, run:
docker-compose up -d
Configuring Play Framework
Now we need to configure Play to connect to this database. Play uses a configuration file conf/application.conf based on HOCON (Human-Optimized Config Object Notation).
First, ensure you have the MySQL driver in your build.sbt (we added this in Part 1).
Next, update conf/application.conf with the database credentials:
# Default database configuration using MySQL database engine
db.default.driver = "com.mysql.cj.jdbc.Driver"
db.default.url = "jdbc:mysql://localhost:3306/expense_tracker?useSSL=false"
db.default.username = "user"
db.default.password = "password"
# Connection pool settings (optional but recommended for production)
play.db.pool = "hikari"
play.db.prototype.hikaricp.minimumIdle = 2
play.db.prototype.hikaricp.maximumPoolSize = 10
Database Service
We'll create a simple DatabaseService to manage our connections. While Play provides a DB API, wrapping it in a service allows us to abstract away the specific implementation details and makes testing easier.
Since we are using raw JDBC (as requested for this tutorial to understand the basics), we'll create a helper to execute blocks of code with a connection.
// app/services/DatabaseService.scala
package services
import java.sql.{Connection, DriverManager}
import javax.inject.{Inject, Singleton}
import play.api.db.Database
@Singleton
class DatabaseService(db: Database) {
// This helper function manages the lifecycle of the connection.
// It borrows a connection from the pool, gives it to your block of code,
// and ensures it's closed (returned to the pool) afterwards.
def withConnection[A](block: Connection => A): A = {
db.withConnection { conn =>
block(conn)
}
}
}
Why withConnection?
Manually opening and closing connections is error-prone. If you forget to close a connection, you'll leak resources and eventually crash your database. Play's db.withConnection handles this automatically, even if an exception is thrown.
Wiring the Service
Finally, we need to wire this service into our application using Macwire in AppLoader.scala. This makes the DatabaseService available for injection into our controllers.
// app/AppLoader.scala
// ... imports
class AppComponents(context: Context) extends BuiltInComponentsFromContext(context)
with HttpFiltersComponents
with AssetsComponents {
lazy val homeController: HomeController = wire[HomeController]
// We wire the DatabaseService here. Macwire finds the `Database` dependency
// from `BuiltInComponentsFromContext` (which includes DB components)
// and injects it into our DatabaseService constructor.
lazy val databaseService: services.DatabaseService = wire[services.DatabaseService]
// ...
}
Testing the Connection
You can now inject DatabaseService into your controllers to perform database operations.
In the next part, we will implement Authentication to secure our application.
Continue to Part 3: Authentication
