Spring Data Projections

Janani Subbiah
Javarevisited
Published in
5 min readJun 22, 2021

--

A quick introduction to Spring data Projections!

Photo by ian dooley on Unsplash

Spring Data supports custom queries in Repositories where developers just need to follow a set way of writing repository methods for the functionality they are looking for. Just defining the method in the interface does the job, for most types of searches at least. Developers pick the functionality and the fields to search by, create the method and pass in the required parameters and the method does what it is supposed to do.

This is pretty straightforward and easy. But say you don’t (for whatever reason) want the whole entity or object returned as a part of your query. Say you only want a subset of fields from the original entity: Spring data projections to the rescue!

Spring supports different types of projections: interface-based, class-based, and dynamic projections. For the sake of this article, I am going to stick to interface-based projections. The Spring Data documentation has excellent information on the other types!

Before we jump in, I am using the following versions of tools and languages:

On that note, let us talk about how to use spring data projections step by step, using books as the resource we want to manage:

Step 1: Defining the Entity

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
class BookEntity(
val name: String,
val author: String,
val isSeries: Boolean,
val yearPublished: Int,
val publisherName: String,
val genre: String
) {

@Id
@GeneratedValue
var id: Long = 0

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as BookEntity

if (name != other.name) return false
if (author != other.author) return false
if (isSeries != other.isSeries) return false
if (yearPublished != other.yearPublished) return false
if (publisherName != other.publisherName) return false
if (genre != other.genre) return false

return true
}

override fun hashCode(): Int {
var result = name?.hashCode() ?: 0
result = 31 * result + (author?.hashCode() ?: 0)
result = 31 * result + isSeries.hashCode()
result = 31 * result + (yearPublished ?: 0)
result = 31 * result + (publisherName?.hashCode() ?: 0)
result = 31 * result + (genre?.hashCode() ?: 0)
return result
}

override fun toString(): String {
return "BookEntity(" +
"name='$name', " +
"author='$author', " +
"isSeries=$isSeries, " +
"yearPublished=$yearPublished, " +
"publisherName='$publisherName', " +
"genre='$genre', " +
"id=$id" +
")"
}
}

We define a very simple class to manage books. We let the ID field be auto-generated (so that the user does not have to set a value for it) and all other fields are required!

Step 2: Defining the Repository

import org.springframework.data.repository.CrudRepository

interface BookRepository : CrudRepository<BookEntity, Long> {

fun findByAuthor(author: String): List<BookEntity>
}

Next, we define a simple repository to manage books. For the sake of simplicity, we assume we don’t want any pagination or sorting capabilities. So we just extend from Spring Data’s CrudRepository.

Step 3: Using the Repository to Save and Retrieve data

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application : CommandLineRunner {

@Autowired
private lateinit var bookRepository: BookRepository

override fun run(vararg args: String?) {
bookRepository.saveAll(
listOf(
BookEntity(
name = "Harry Potter and the Chamber of Secrets",
author = "J K Rowling",
isSeries = true,
yearPublished = 1998,
publisherName = "Bloomsbury Publishing",
genre = "Fantasy"
),
BookEntity(
name = "Harry Potter and the Goblet of Fire",
author = "J K Rowling",
isSeries = true,
yearPublished = 2000,
publisherName = "Bloomsbury Publishing",
genre = "Fantasy"
),
BookEntity(
name = "Harry Potter and the Deathly Hallows",
author = "J K Rowling",
isSeries = true,
yearPublished = 2007,
publisherName = "Bloomsbury Publishing",
genre = "Fantasy"
),
BookEntity(
name = "Lord of the Rings: The Fellowship of the Ring",
author = "J.R.R. Tolkien",
isSeries = true,
yearPublished = 1954,
publisherName = "Allen & Unwin",
genre = "Adventure"
),
BookEntity(
name = "The Lion, the Witch, and the Wardrobe",
author = "C. S. Lewis",
isSeries = true,
yearPublished = 1950,
publisherName = "Geoffrey Bles",
genre = "Children's Fantasy"
),
BookEntity(
name = "Gone Girl",
author = "Gillian Flynnp",
isSeries = false,
yearPublished = 2012,
publisherName = "Crown Publishing Group",
genre = "Thriller"
)
)
)

bookRepository.findByAuthor("J K Rowling")
.forEach { book -> println(book) }
}
}

fun main(args: Array<String>) {
runApplication<Application>(*args)
}

Our DB currently has no data. So we first load it up with some data. We add some books and then we fetch them all by the author using the custom query that we created in our Repository.

If we were to run this app at this point we would see the following in the logs:

BookEntity(name='Harry Potter and the Chamber of Secrets', author='J K Rowling', isSeries=true, yearPublished=1998, publisherName='Bloomsbury Publishing', genre='Fantasy', id=1)BookEntity(name='Harry Potter and the Goblet of Fire', author='J K Rowling', isSeries=true, yearPublished=2000, publisherName='Bloomsbury Publishing', genre='Fantasy', id=2)BookEntity(name='Harry Potter and the Deathly Hallows', author='J K Rowling', isSeries=true, yearPublished=2007, publisherName='Bloomsbury Publishing', genre='Fantasy', id=3)

Step 4: Introducing Projections!

Say we only want to get the book names that were written by a certain author vs getting all the information about books. So we create an interface with a single method that starts with get followed by the name of the field we are looking to retrieve (which in our case is name).

import org.springframework.beans.factory.annotation.Value

interface BookName {

fun getName(): String
}

Step 5: Updating the Repository

import org.springframework.data.repository.CrudRepository

interface BookRepository : CrudRepository<BookEntity, Long> {

fun findByAuthor(author: String): List<BookName>
}

We update the return type of the findByAuthor method to return a list of BookName vs BookEntity. As a final step, we also update our print statement to print the projection method

bookRepository.findByAuthor("J K Rowling")
.forEach { bookName -> println(bookName.getName()) }

Step 6: Projections in action

Now when we run our app, we should see the following in the console:

Harry Potter and the Chamber of Secrets
Harry Potter and the Goblet of Fire
Harry Potter and the Deathly Hallows

Step 7: Another Projection Method

If we wanted to combine multiple fields from the BookEntity into a single field in our projection interface, we can update our getName() method to the following:

import org.springframework.beans.factory.annotation.Value

interface BookName {

@Value("#{target.name + ', ' + target.yearPublished}")
fun getName(): String
}

And when we run the app we should see the following in the logs:

Harry Potter and the Chamber of Secrets, 1998
Harry Potter and the Goblet of Fire, 2000
Harry Potter and the Deathly Hallows, 2007

Spring data projections are that easy and convenient! For further reading please refer to Spring Data JPA’s documentation on projections!

--

--

Janani Subbiah
Javarevisited

Product Architect | Ice cream lover | Newbie gardener