Interface-based projections

Interface-based projections are a great place to start. They are simple to create and commonly used when you want to trim down an entity for user views or retrieval.

Take the Movie entity as an example. There are sixteen fields on the entity, which is a lot to work with!

java
@Node
public class Movie {
    @Id
    private String movieId;

    private String title;
    private String plot;
    private String poster;
    private String url;
    private String imdbId;
    private String tmdbId;
    private String released;

    private Long year;
    private Long runtime;
    private Long budget;
    private Long revenue;
    private Long imdbVotes;

    private Double imdbRating;

    private String[] languages;
    private String[] countries;

    //constructor, getters, and setters
}

When you saved a new movie back in Module 4, you only sent a few values, making the return results show a lot of null values. What if more of the data contained empty values or you wanted to provide a page for users to scroll through all movies without displaying every field?

You could write a projection to only include a few key fields. Next, you will create a projection that only contains the title, released, and poster properties for each movie.

MovieProjection interface

To create a projection, create an interface called MovieProjection.java in the src/main/java/com/example/appspringdata folder. Then call the getter methods of the desired properties for title, released, and poster.

java
public interface MovieProjection {
    String getTitle();
    String getReleased();
    String getPoster();
}

Now add a new method to each of the MovieRepository and MovieController files to return a list of MovieProjection instead of Movie with an endpoint mapping of /movielist.

java
interface MovieRepository extends Neo4jRepository<Movie, String> {
    //other methods

    Iterable<MovieProjection> findAllMovieProjectionsBy();
}
@RestController
@RequestMapping("/movies")
public class MovieController {
    //other methods

    @GetMapping("/movielist")
    Iterable<MovieProjection> findAllMovieProjections() { 
        return movieRepo.findAllMovieProjectionsBy(); 
    }
}

Completed repository and controller code is available in the dropdown below.

Click to reveal the completed code
java
interface MovieRepository extends Neo4jRepository<Movie, String> {

    @Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person)" +
            "RETURN m, collect(r), collect(p) LIMIT 20;")
    Iterable<Movie> findMoviesSubset();

    @Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person {name: $name})" +
            "RETURN m, collect(r), collect(p);")
    Iterable<Movie> findMoviesByPerson(String name);

    @Query("MATCH (m:Movie {movieId: $movieId}) " +
            "SET m.imdbVotes = coalesce(m.imdbVotes+1, 1) " +
            "RETURN m;")
    Movie incrementImdbVotes(String movieId);

    Iterable<MovieProjection> findAllMovieProjectionsBy();
}
@RestController
@RequestMapping("/movies")
public class MovieController {
    private final MovieRepository movieRepo;

    public MovieController(MovieRepository movieRepo) {
        this.movieRepo = movieRepo;
    }

    @GetMapping()
    Iterable<Movie> findAllMovies() {
        return movieRepo.findMoviesSubset();
    }

    @GetMapping("/{movieId}")
    Optional<Movie> findMovieById(@PathVariable String movieId) {
        return movieRepo.findById(movieId);
    }

    @PostMapping("/save")
    Movie save(@RequestBody Movie movie) {
        return movieRepo.save(movie);
    }

    @DeleteMapping("/delete")
    void delete(@RequestParam String movieId) {
        movieRepo.deleteById(movieId);
        System.out.println("Deleted movie with movieId: " + movieId);
    }

    @GetMapping("/person")
    Iterable<Movie> findMoviesByPerson(@RequestParam String name) {
        return movieRepo.findMoviesByPerson(name);
    }

    @PutMapping("/updateVotes")
    Movie updateVotes(@RequestParam String movieId) {
        return movieRepo.incrementImdbVotes(movieId);
    }

    @GetMapping("/movielist")
    Iterable<MovieProjection> findAllMovieProjections() { return movieRepo.findAllMovieProjectionsBy(); }
}

Returning a projection from a repository method

As mentioned in the documentation, the return type of the method (MovieProjection) is different from the repository’s domain type (Movie), and therefore must use properties defined in the domain type. The method’s suffix By is needed to make SDN not look for a property called MovieProjections in the Movie class.

Now, when you test the application and call the /movies/movielist endpoint, you will only get the three properties specified in the projection.

shell
curl 'localhost:8080/movies/movielist'
FindMovieProjections sample results
{
    "title":"The Beatles: Eight Days a Week - The Touring Years",
    "poster":"https://image.tmdb.org/t/p/w440_and_h660_face/A6q7Jy4vXgXXoCoHX4lpCaKvcMV.jpg",
    "released":null
}

Lesson Summary

In this lesson, you learned how to create an interface projection to return a subset of properties from an entity.

In the next optional lesson, you will learn how to create a DTO-based projection for the Movie entity.