What You'll Learn

With JDBC, we have to write the SQL ourselves and we need to write code to convert the returned data into objects. We have to manage the primary and foreign keys and the object graphs ourselves. This provides a deep level of control over the database interaction and enables us to make full use of the facilities of the database, tuning queries and data sizes to the needs of the request.

However, there is another point of view that would say that there are a number of common patterns in the generation of the SQL and that those patterns can be abstracted into a framework that can generate the SQL, handling key generation, joins and offer concepts such as eager vs lazy loading (lazy => only getting data from the database when required).

This latter concern has been addressed for many years. The dominate ORM (object-relational mapping) framework in Java is Hibernate which is used by Spring Data JPA (Java Persistence Architecture).

In this tutorial, we are going to introduce Spring Data JPA.
It is only intended to provide an overview.
JPA is a large framework with many features and options.

In Spring Data JPA, we need to do the following:

As always, we need to add the Spring Data JPA dependency to our project. We do this by adding this line to build.gradle in the dependencies block.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

To illustrate JPA without making change to the existing code, we will create a completely separate package structure. Under the data package, we create a new package jpa, and in that package, we create three packages; entities, repositories and adaptors. We won't need this last one for this tutorial, but think about what might be heading into this package! Under the entities package, create a package called converters.

Annotate the class

In the entities package, create a class called Charity. We may rename this in future to avoid confusion with the domain class, but for now, we'll let the package distinguish the classes in our code.

This new class will look a lot like the domain class (HINT: you can copy/paste), but we need to add some annotations to it.

In entities/Charity.java.

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Charity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @Column(name="registration_id")
    private String registration;
    private String acronym;
    private String logoFileName;
    @Column(name="mission")
    private String missionStatement;

    @Convert(converter = BooleanToStringYNConverter.class)
    private Boolean isActive;
    @Transient private Set<Trustee> trustees;
    //as per the domain class...

Let's examine these new annotations in turn.

The @Entity annotation - this registers this class as a JPA entity. If we take the DDD (Domain-Driven Design) definition of "Entity", we can see that it is "An object that is not defined by its attributes, but rather by a thread of continuity and its identity." (see Evans, E. ,2004, Domain-Driven Design: Tackling Complexity in the Heart of Software).

This is typically a business domain concept that represents some element of state in the domain.

By default, Spring Data JPA will map the class to a table of the same name (though do check the manual on case sensitivity!) So, in this case, we're in a good place because we have a "charity" table.

The @Id annotation - this is an annotation on a field and it identifies the identifier of the entities in the class. Think of it like the primary key of the table.

The @GeneratedValue annotation - this annotation goes with the @Id annotation. It is used to determine the strategy to be used for creating new primary keys in the database. In this case, we're using IDENTITY which means that the database will auto-increment. Other options are documented on Baeldung.

Essentially, with these two annotations, we've told JPA that when we create a new instance of a Charity entity, this will require an insert and that the primary key auto-increment will be used.

The @Column annotation - This annotation is used twice. If you examine the field names and the column names in the table, then you should be able to work out why these two fields need annotating and the others don't.

Again, it comes down to "Convention over Configuration". If the field can be mapped to a column name using a standard algorithm, Spring will do that. The standard algorithm is to replace camel case with underscores to separate words. So, "imageFileName" would map to "image_file_name".

The two annotated columns don't follow this pattern, so the @Column annotation allows us to make this explicit.

The @Convert annotation - Java is an OO language and we can represent data using data types. For example, boolean values can be held in boolean or Boolean fields. Some databases don't offer a datatype for boolean values, so they are mapped to other types, be that 0 and 1 in a numerical field or "Y" or "N" in a character field. In our case, we're converting to "Y" or "N". The code to do this is shown later in the tutorial.

The @Transient annotation - For now, we're going to keep the mapping simple and only bring back the charity fields. We'll leave the trustees for later as that starts to introduce some interesting design considerations that determine the SQL sent to the database and the way in which objects can be accessed.

We now have an annotated entity. More about what this means will be revealed later. Now we need a way of getting these entities "inflated" into our application from the database.

As with JDBC, we are going to use a Repository to provide the means to get objects from a database.

Let's code a new repository in the repositories package. Call it CharityRepositoryJPA.

In CharityRepositoryJPA.java.

public interface CharityRepositoryJPA extends JpaRepository<Charity, Long> {

    List<Charity> findAll();
    Optional<Charity> findByAcronym(String acronym);
    Charity save(Charity aCharity);
}

Did you write a class? JPA doesn't require you to do this. Rather an interface will suffice. Our interface extends JpaRepository. There are alternatives such as a CrudRepository. The differences are left as an exercise.

Note that the methods are almost the same as our hand-crafted JDBC repository. The one method that has changed is save(). This is because save() is one of the methods in the super-interface.

The others use a naming convention. findAll is, as you would expect standard, but findByAcronym is only valid because the Charity entity has an acronym field.

Spring Data JPA will parse the method name and, by picking out keywords, will try to match parts of the method name to fields. From that, it can work out the column names and from all that logic, it can work out the SQL with a where clause.

The save() method works just like our JDBC implementation in that entities with a null id field will be inserted, and those with an existing id field will be updated.

We can test JPA repositories in the same way that we tested JDBC repositories - we just use a different test annotation.

In CharityDataJPATests.java (new class)

@DataJpaTest
@Sql(scripts={"/schema-test-h2.sql","/data-test-h2.sql"})
@DirtiesContext
public class CharityDataJPATests {

 @Autowired
 CharityRepositoryJPA charityRepository;
}

Again - we set the SQL and we tell Spring that the tests will "dirty the context", but this time we use @DataJpaTest.

We can autowire the repository for testing.

We can then write tests against the repository.

    @Test
    public void shouldGet11Charities() throws Exception{
        List<Charity> charityList = charityRepository.findAll();
        System.out.println("charity list = " + charityList);
        assertEquals(11, charityList.size());
    }

We can also write tests that update (see repo) and insert...

@Test
    public void shouldGet12CharitiesAfterInsert() throws Exception{
        Charity nspccWales = new Charity("nspcc-wales", "56565656","nspcc-wales", "helping kids in Wales");
        charityRepository.save(nspccWales);
        List<Charity> charityList = charityRepository.findAll();
        assertEquals(12, charityList.size());
        Charity nspccWales2 = charityRepository.findByAcronym("nspcc-wales").get();
        assertEquals("56565656", nspccWales2.getRegistration());
 }

In this test, we create a new Charity entity and add it to the repository. We then get all and check that we have 12 charities.

We then search for the one one by acronym and check a field.

JPA allows us to quickly write a database persistence layer that maps to objects without writing any SQL. This can seem like magic and a "must-use" feature, but it does come with its own issues and can need careful configuration, especially when dealing with more complicated object graphs. The question to always bear in mind is "how much data do I want to ‘inflate' in the object tier?"