2013-12-10

Yet another guide to Spring Data JPA

I've been learning Spring Data JPA recently and I am really impressed by its features and ease of use. It allows to create JPA based data access layer very quickly. In a basic usage scenario you only need to create interfaces which extend specific Spring interfaces and which have methods named according to appropriate convention - and that's it. Spring Data JPA will automatically provide implementations of those interfaces.

I created a simple project, which demonstrates basic features of Spring Data JPA. The source code can be downloaded here and the most interesting parts are described below.

First of all this is my Spring config:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
public class SpringConfig {

  @Bean
  public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder().addScript("/schema.sql").addScript("/data.sql").build();
  }

  @Bean
  public AbstractEntityManagerFactoryBean entityManagerFactory() {
    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setShowSql(true);
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setPackagesToScan("com.github.mateuszwenus.entity");
    factory.setDataSource(dataSource());
    factory.afterPropertiesSet();
    return factory;
  }

  @Bean
  public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager(entityManagerFactory().getObject());
  }
}
There is no XML configuration, only Java code. Even the database is created and prepopulated with test data from code. The DB stores some basic data about the Solar System - planets and their satellites:
create table planets
(
  id bigint,
  name varchar(255),
  mass_rel_to_earth decimal(10, 3)
);

create table satellites
(
  id bigint,
  planet_id bigint,
  name varchar(255)
);
It contains the following data:
insert into planets(id, name, mass_rel_to_earth) values(1, 'Mercury', 0.055);
insert into planets(id, name, mass_rel_to_earth) values(2, 'Venus', 0.815);
insert into planets(id, name, mass_rel_to_earth) values(3, 'Earth', 1);
insert into planets(id, name, mass_rel_to_earth) values(4, 'Mars', 0.107);

insert into satellites(id, planet_id, name) values(1, 3, 'Moon');
insert into satellites(id, planet_id, name) values(2, 4, 'Phobos');
insert into satellites(id, planet_id, name) values(3, 4, 'Deimos');
The tables are mapped by simple entites:
@Entity
@Table(name = "planets")
public class Planet {
  @Id
  private Long id;
  private String name;
  @Column(name = "mass_rel_to_earth")
  private BigDecimal massRelativeToEarth;
  @OneToMany(fetch = FetchType.LAZY, mappedBy = "planet")
  private Set satellites;
  ...
@Entity
@Table(name = "satellites")
public class Satellite {
  @Id
  private Long id;
  @ManyToOne
  @JoinColumn(name = "planet_id")
  private Planet planet;
  private String name;
  ...
}
Okay, that's enough about project config. Let's see Spring Data JPA in action. This is the most basic usage example:
@Autowired
private PlanetRepository planetRepository;

@Test
public void shouldFindPlanetById() {
  // when
  Planet planet = planetRepository.findOne(1L);
  // then
  assertThat(planet, is(notNullValue()));
  assertThat(planet.getName(), is("Mercury"));
}
The test succeeds. But what actually is PlanetRepository?
public interface PlanetRepository extends JpaRepository<Planet, Long> {
}
It is just an interface (there's no implementation in my project) which extends Spring's JpaRepository. The method findOne() is defined in CrudRepository, which is a superinterface of JpaRepository. Notice the "Crud" in the name - PlanetRepository gets CRUD features for free. This means that all of below tests pass as well:
@Test
public void shouldSaveNewPlanet() {
  // given
  Planet planet = new Planet(JUPITER_ID, JUPITER);
  // when
  planetRepository.save(planet);
  planetRepository.flush();
  // then
  assertThat(numberOfPlanetsWithName(JUPITER), is(1));
}

@Test
public void shouldUpdateExistingPlanet() {
  // given
  Planet planet = new Planet(MERCURY_ID, MERCURY);
  String oldName = planet.getName();
  String newName = oldName + oldName;
  planet.setName(newName);
  // when
  planetRepository.save(planet);
  planetRepository.flush();
  // then
  assertThat(numberOfPlanetsWithName(oldName), is(0));
  assertThat(numberOfPlanetsWithName(newName), is(1));
}

@Test
public void shouldDeleteExistingPlanet() {
  // when
  planetRepository.delete(MERCURY_ID);
  planetRepository.flush();
  // then
  assertThat(numberOfPlanetsWithName(MERCURY), is(0));
}
Spring Data JPA gives you much more than simple CRUD operations. It is possible to declare query methods in your repository interface and Spring will provide the implementation based on the method name. Consider the following test:
@Autowired
private SatelliteRepository satelliteRepository;

@Test
public void shouldFindSatelliteByPropertyEq() {
  // when
  Satellite satellite = satelliteRepository.findByName(MOON);
  // then
  assertThat(satellite, is(notNullValue()));
  assertThat(satellite.getName(), is(MOON));
}
SatelliteRepository is:
public interface SatelliteRepository extends JpaRepository {
  Satellite findByName(String name);
}
Spring Data JPA parses the method name and builds the query based on the parts that come after "By". It knows that "Name" in method name refers to field Satellite.name and that we expect the query to return single result (based on method return type).

It is also possible to compare entity fields using other operators than equals and even to walk over entity relationships and compare fields of related entites. The following two are completely valid and working finder methods. The second is especially interesting - it returns Satellites based on condition checked on their Planet.
public interface SatelliteRepository extends JpaRepository {
  Collection<Satellite> findByNameEndingWith(String suffix);
  Collection<Satellite> findByPlanetName(String name);
}
What's more, with Spring Data JPA it is very easy to implement paging and sorting - you just need to pass a Pageable as an argument of finder method. Below is an example for builtin findAll() method, but Pageable also works with user defined methods - for example it could be used with findByPlanetName() from previous example:
@Autowired
private PlanetRepository planetRepository;

@Test
public void shouldReturnSecondPage() {
  // given
  Pageable pageable = new PageRequest(1, 2, new Sort(Direction.DESC, "massRelativeToEarth"));
  // when
  Page<Planet> planets = planetRepository.findAll(pageable);
  // then
  assertThat(planets.getContent(), contains(planet(MARS), planet(MERCURY)));
}
It is also possible to create finder methods based on user defined queries. Consider the following test:
@Test
public void shouldFindByQueryAnnotatedMethod() {
  // when
  PlanetDto planetDto = planetRepository.findPlanetDtoByName(MARS);
  // then
  assertThat(planetDto, is(notNullValue()));
  assertThat(planetDto.getName(), is(MARS));
  assertThat(planetDto.getNumberOfSatellites(), is(2));
}
PlanetRepository contains the following method:
public interface PlanetRepository extends JpaRepository<Planet, Long> {
  @Query("select new com.github.mateuszwenus.dto.PlanetDto(p.name, p.satellites.size) from Planet p where p.name = :name")
  PlanetDto findPlanetDtoByName(@Param("name") String name);
}
The query to run can also be defined like standard JPA Named Query, for example:
@Entity
@Table(name = "planets")
@NamedQuery(name = "Planet.findLighterThanEarth", query = "select p from Planet p where p.massRelativeToEarth < 1")
public class Planet {
...
public interface PlanetRepository extends JpaRepository<Planet, Long> {
  List<Planet> findLighterThanEarth();
}
@Test
public void shouldFindByNamedQuery() {
  // when
  List<Planet> planets = planetRepository.findLighterThanEarth();
  // then
  assertThat(planets, is(notNullValue()));
  assertThat(planets, containsInAnyOrder(planet(MERCURY), planet(VENUS), planet(MARS)));
}
I simplified some of my examples to make them fit a blog post. You can check the full source code in my github repository. You also can check the official docs and examples for more thorough description of Spring Data JPA.

To sum it up, what I really like in Spring Data JPA is the "write less, do more" approach to creating data access layer. My project consists of 2 entites, 1 DTO, 1 Spring @Configuration class and 2 interfaces. However, it has CRUD, paging, sorting and various finders. It would require a lot of code to achieve this using only JPA or Hibernate. This makes Spring Data JPA a good candidate to use in the next project.