Using Spring Data JPA and Scala… Seriously?

The combination of Scala and Spring Framework – you might say it’s not so common. While still working with both, having a few Scala projects in my track record, and having over 15 years of experience in using Spring in projects, I can testify that it’s not such a bad mixture.
For that reason, I have decided to write this blog post and to show how to combine Spring Data JPA and Scala. The aim is to use the powerful Spring Data JPA library for creating a model definition and respective repositories, yet still to continue to draw benefits from all the tricks and treats that Scala brings to the game.
Initializing the project
We will use Spring Initializr to initialize our project. You can select your favorite project setup (unfortunately, you can’t select Scala as your language – selecting Java and tweaking the project in the next section of setup will do the job).
For the sake of this example I have selected Gradle, Java, newest Spring Boot (fresh 2.5.0) and added Spring JPA Data as a dependency.
Download the project ZIP file, unzip it, init git if you like and we are ready for the next step.
Introduce Scala to your Spring Boot project
Of course, we want to have a Scala project so we will be tweaking our build.gradle
file.
What we want to add is add Scala and ScalaTest gradle plugins:
buildscript {
dependencies {
classpath "org.scala-lang:scala-library:$scalaLibraryVersion"
classpath "gradle.plugin.com.github.maiflai:gradle-scalatest:$scalaTestPluginVersion"
}
}
plugins {
id 'org.springframework.boot' version "$springBootVersion"
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'scala'
id 'com.github.maiflai.scalatest' version "$scalaTestPluginVersion"
}
Next, we want to use all the Scala, ScalaTest and ScalaTestPlus dependencies. ScalaTest Plus dependency provides us integration between other libraries in our case we use JUnit and Mockito which we added there as dependency. We remove transitive JUnit Jupiter dependency from the Spring Test starter not to have it on our classpath without a reason.
dependencies {
implementation "org.scala-lang:scala-library:$scalaLibraryVersion"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation "com.h2database:h2"
testImplementation ('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.jupiter', module: 'junit-jupiter'
}
testImplementation "junit:junit"
testImplementation "org.mockito:mockito-core"
testImplementation "org.scalacheck:scalacheck_$scalaVersion:$scalacheckVersion"
testImplementation "org.scalatest:scalatest_$scalaVersion:$scalatestVersion"
testImplementation "org.scalatestplus:scalatestplus-mockito_$scalaVersion:$mockitoScalaVersion"
testImplementation "org.scalatestplus:junit-4-13_$scalaVersion:$scalatestJUnitVersion"
testImplementation "com.h2database:h2"
testRuntimeOnly "com.vladsch.flexmark:flexmark-profile-pegdown:$flexmarkVersion"
}
After we did this we are ready to change our generated Spring Boot Application from Java code to Scala as follows:
@SpringBootApplication
class ScalaSpringJpaApplication
object ScalaSpringJpaApplication extends App {
SpringApplication.run(classOf[ScalaSpringJpaApplication], args: _*)
}
A simple application runner. Now, we are really ready to think of a simple model and few repositories.
Build up a model
We will build a simplified model for a Metering Service. Our service has Meters which have a name and a label. Meters can emit different Alert or Notifications. Alert has its label and severity, while Notification has again a label and a value.
For our model we create BaseEntity
abstract class, containing the UUID,
which is used to extend Meter
class and BaseEvent
abstract class. BaseEvent
class, containing reference to Meter, is used as parent
for both Alert
and Notification
classes.
Model
abstract classes are annotated with @MappedSupperclass
, and model classes are of course annotated with @Entity.
Following are two code snippets, for two mapped super classes BaseEntity
and BaseEvent
.
@MappedSuperclass
abstract class BaseEntity extends Serializable {
@Id
@GeneratedValue
@Column(name = "id", length = 36)
@Type(`type` = "uuid-char")
var id: UUID = _
}
@MappedSuperclass
abstract class BaseEvent extends BaseEntity {
@Type(`type` = "uuid-char")
@JoinColumn(name = "meter_id")
@ManyToOne(fetch = FetchType.LAZY)
var meter: Meter = _
}
Since our entity classes have to have a default constructor we are adding a companion object to each of the classes to define helper constructors for instantiating objects (from a Service layer for example).
The code snippet below shows an example of Meter
domain entity implementation.
@Entity
@Table(name = "meter")
class Meter extends BaseEntity {
@Column(name = "name", unique = true, nullable = false)
var name: String = _
@Column(name = "label")
var label: String = _
}
object Meter {
def apply(name: String, label: Option[String]): Meter = {
val meter = new Meter
meter.name = name
meter.label = label.orNull
meter
}
def apply(id: UUID, name: String, label: Option[String]): Meter = {
val meter = new Meter
meter.id = id
meter.name = name
meter.label = label.orNull
meter
}
}
Check out the repository for the Alert and Notification model definition.
Create repositories
For repositories, we create a root interface for each of the model classes containing simple prototypes on what we want to do on our model. For example, we show here MeterRepository
trait:
trait MeterRepository {
def add(meter: Meter): Meter
def update(meter: Meter): Meter
def list(pageable: Pageable): Page[Meter]
def get(name: String): Option[Meter]
def remove(meter: Meter): Unit
def removeByName(meterName: String): Unit
def filterByNameOrLabel(pattern: String, pageable: Pageable): Page[Meter]
}
Notice we use scala types for Option, or we would use scala.List in our trait if we had functions returning a list of objects. That is the main reason why we want to have separate trait that will be used by our code.
For the implementation we define another trait which will then extend our MeterRepository and Spring JPA Data JpaRepository. We override MeterRepository methods and use creation of either JPQL query or method names to create protected methods used with the implementation.
Our implementation also transforms, in our example Optional to Option, using scala implicit transformations to go from Java to pure Scala.
@Transactional(propagation = Propagation.MANDATORY)
trait MeterRepositoryImpl extends MeterRepository
with JpaRepository[Meter, UUID] {
override def add(meter: Meter): Meter = save(meter)
override def update(meter: Meter): Meter = save(meter)
override def list(pageable: Pageable): Page[Meter] = findAll(pageable)
override def remove(meter: Meter): Unit = delete(meter)
override def removeByName(meterName: String): Unit = deleteByName(meterName)
override def get(name: String): Option[Meter] = findByName(name).toScala
override def filterByNameOrLabel(pattern: String, pageable: Pageable): Page[Meter] = findByNameOrLabel(pattern, pageable)
protected def findByName(name: String): Optional[Meter]
protected def deleteByName(name: String): Unit
@Query("select m from Meter m where m.name like %:pattern% or m.label like %:pattern%")
protected def findByNameOrLabel(@Param("pattern") pattern: String, pageable: Pageable):
Page[Meter]
}
Checkout the rest of the implementation on the Github repository.
To be continued.
In this blog, we described how simple it is to set up the Scala project that uses Spring Data JPA. You might have noticed that we also introduced all the dependencies for the ScalaTests, that is because in the next blog we will talk more about the practice of writing integration tests for our repository implementations.