Simple Java persistence, part 2

September 28, 2020

Part 1 showed how to get persistence basics going for a simple Java Spring Boot service. Now let’s take the story a step further with related tables, transactions, and testing.

Real databases have foreign keys, collections, one-to-many, many-to-many, and lots more. Let’s see how Spring Data JDBC and Spring Data REST handle that.

We said owners have vehicles. Here are the classes we need for a vehicle.

Vehicle.java

package cloud.stephen.springdatajdbc;
import org.springframework.data.annotation.Id;
public class Vehicle {
@Id
private final Long id;
private final String make;
private final String model;
private final long mileage;
private final Long owner;
public Vehicle(final Long id, final String make, final String model, final long mileage, final Long owner) {
this.id = id;
this.make = make;
this.model = model;
this.mileage = mileage;
this.owner = owner;
}
static Vehicle of(final Long id, final String make, final String model, final long mileage, final Long owner) {
return new Vehicle(id, make, model, mileage, owner);
}
Vehicle withId(Long id) {
return of(id, make, model, mileage, owner);
}
public Long getId() {
return id;
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public long getMileage() {
return mileage;
}
public Long getOwner() {
return owner;
}
public int hashCode() {
return id == null ? 31 : id.hashCode();
}
public boolean equals(final Object that) {
if (this == that) {
return true;
}
if (!(that instanceof Vehicle)) {
return false;
}
return id != null && id.equals(((Vehicle) that).id);
}
}

This corresponds to the database table

CREATE TABLE IF NOT EXISTS VEHICLE (
ID INT AUTO_INCREMENT PRIMARY KEY,
MAKE VARCHAR(50) NOT NULL,
MODEL VARCHAR(50) NOT NULL ,
MILEAGE INT NOT NULL,
OWNER INT,
FOREIGN KEY (OWNER) REFERENCES OWNER(ID)
);

VehicleRepository.java

package cloud.stephen.springdatajdbc;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface VehicleRepository extends PagingAndSortingRepository<Vehicle, Long> {
}

Owner.java

One-to-many and foreign keys are Java Sets. So let’s update Owner.java and give that a proper go.

package cloud.stephen.springdatajdbc;
import java.util.Set;
import org.springframework.data.annotation.Id;
public class Owner {
@Id
private final Long id;
private final String firstName;
private final String lastName;
private final Set<Vehicle> vehicles;
public Owner(final Long id, final String firstName, final String lastName, final Set<Vehicle> vehicles) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.vehicles = vehicles;
}
static Owner of(final Long id, final String firstName, final String lastName, final Set<Vehicle> vehicles) {
return new Owner(id, firstName, lastName, vehicles);
}
Owner withId(Long id) {
return of(id, firstName, lastName, vehicles);
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Set<Vehicle> getVehicles() {
return vehicles;
}
...
}

Let’s change some data

He’s 18! So buy Alexander a car.

$ curl -i -X POST -H "Content-Type:application/json" -d '{ "make" : "VW", "model" : "Jetta", "mileage": 42000, "owner": 1 }' http://localhost:8080/vehicles
HTTP/1.1 201
{
"make" : "VW",
"model" : "Jetta",
"mileage" : 42000,
"owner" : 1,
"_links" : {
"self" : {
"href" : "http://localhost:8080/vehicles/1"
},
"vehicle" : {
"href" : "http://localhost:8080/vehicles/1"
}
}
}

Prove he owns it.

$ curl -i "http://localhost:8080/owners/1"
HTTP/1.1 200
{
"firstName" : "Alexander",
"lastName" : "Harrison",
"vehicles" : [ {
"make" : "VW",
"model" : "Jetta",
"mileage" : 42000,
"owner" : 1
} ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/owners/1"
},
"owner" : {
"href" : "http://localhost:8080/owners/1"
}
}
}

Cool. Now let’s buy Stephen a car. I’m thinking let’s get him a good one.

$ curl -i -X POST -H "Content-Type:application/json" -d '{ "make" : "Bugatti", "model" : "Veyron", "mileage": 10, "owner": 2 }' http://localhost:8080/vehicles
HTTP/1.1 201
{
"make" : "Bugatti",
"model" : "Veyron",
"mileage" : 10,
"owner" : 2,
"_links" : {
"self" : {
"href" : "http://localhost:8080/vehicles/3"
},
"vehicle" : {
"href" : "http://localhost:8080/vehicles/3"
}
}
}

That’s more like it.

Another custom finder

Now we have a one-to-many relation, let’s add a JOIN with a WHERE clause to OwnerRepository. We’ll add a fancier base interface PagingAndSortingRepository for some more useful database calls at the same time.

package cloud.stephen.springdatajdbc;
import java.util.List;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface OwnerRepository extends PagingAndSortingRepository<Owner, Long> {
List<Owner> findByFirstName(String firstName);
List<Owner> findByLastName(String lastName);
@Query("SELECT OWNER.ID, OWNER.FIRST_NAME, OWNER.LAST_NAME FROM OWNER JOIN VEHICLE ON OWNER.ID = VEHICLE.OWNER WHERE VEHICLE.MAKE = :make")
List<Owner> findByVehicleMake(String make);
}

We can get a handy list of the custom finders like this

$ curl -i "http://localhost:8080/owners/search"
HTTP/1.1 200
{
"_links" : {
"findByLastName" : {
"href" : "http://localhost:8080/owners/search/findByLastName{?lastName}",
"templated" : true
},
"findByVehicleMake" : {
"href" : "http://localhost:8080/owners/search/findByVehicleMake{?make}",
"templated" : true
},
"findByFirstName" : {
"href" : "http://localhost:8080/owners/search/findByFirstName{?firstName}",
"templated" : true
},
"self" : {
"href" : "http://localhost:8080/owners/search"
}
}
}

And test it out

$ curl -i "http://localhost:8080/owners/search/findByVehicleMake?make=VW"
HTTP/1.1 200
{
"_embedded" : {
"owners" : [ {
"firstName" : "Alexander",
"lastName" : "Harrison",
"vehicles" : [ {
"make" : "VW",
"model" : "Jetta",
"mileage" : 42000,
"owner" : 1
} ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/owners/1"
},
"owner" : {
"href" : "http://localhost:8080/owners/1"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/owners/search/findByVehicleMake?make=VW"
}
}
}

What happened to transactions?

Sorry, you can’t do them.

Just kidding.

First let’s get everything ready.

Create the class OwnerService with the following that includes the @Transactional annotation. Now with the default transaction Propagation type of REQUIRED, all public service methods use an existing or create a new transaction.

package cloud.stephen.springdatajdbc;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class OwnerService {
private final OwnerRepository ownerRepository;
public OwnerService(final OwnerRepository ownerRepository, final Utils utils) {
this.ownerRepository = ownerRepository;
}
public Optional<Owner> findById(final Long ownerId) {
return ownerRepository.findById(ownerId);
}
public Owner save(final Owner owner) {
return ownerRepository.save(owner);
}
public Iterable<Owner> findAll() {
return ownerRepository.findAll();
}
public void deleteAll() {
ownerRepository.deleteAll();
}
}

The pattern to follow is that all the service methods are generally pass-through to the underlying repository. They simply decorate the repository calls with transactions. The service is the only place we should ever put transaction demarcation. Here’s why.

If we put a transaction annotation on repositories, we couldn’t necessary coordinate multiple repository calls from a service, something they do all the time. If we put transactions on the controller, then service calls couldn’t coordinate other service calls: Controllers should only be concerned with data marshalling and conversion. Transactions on the service tier is simply the correct place.

VehicleService follows the same pattern.

Let’s test all this

Tests in Spring Boot tend to look like this: A class annotation @SpringBootTest, a @BeforeEach hook for test-level initialization, and test methods annotated with @Test. The class annotation executes all the Spring Boot initialization.

Testing a repository

@SpringBootTest
class OwnerRepositoryTest {
@Autowired
private OwnerRepository ownerRepository;
@Autowired
private VehicleRepository vehicleRepository;
@BeforeEach
void spickAndSpanDatabase() {
ownerRepository.deleteAll();
vehicleRepository.deleteAll();
}
@Test
void whenDatabaseEmpty_ThenReturnEmpty() {
final Iterable<Owner> actual = ownerRepository.findAll();
final Iterable<Owner> expected = emptyList();
assertThat(actual).isEqualTo(expected);
}
...

And so on.

assertThat() comes from Assert4j’s fluent methods.

Here’s another test.

@Test
void whenSaveTwoOwnersAndDeleteOne_ThenReturnOne() {
final Owner stephen = ownerRepository.save(Owner.of(null, "Stephen", "Harrison", emptySet()));
final Owner alexander = ownerRepository.save(Owner.of(null, "Alexander", "Harrison", emptySet()));
ownerRepository.delete(stephen);
final Iterable<Owner> actual = ownerRepository.findAll();
final List<Owner> expected = Arrays.asList(alexander);
assertThat(actual).isEqualTo(expected);
}

Pretty straightforward. You get the idea. In fact, good tests tend to be really simple because they test just one thing. This makes things “provably correct by inspection,” something we value and strive for at Venmo.

Testing a service

This is bit subtler because transactions are in play: We have to test not only the service methods but transactions too. And by subtler we mean super interesting.

The test class contains a field of type TransactionTemplate, an interface with a method execute that takes a callback for the code we want to run inside a transaction context. Handy.

@SpringBootTest
class OwnerServiceTest {
@Autowired
private OwnerService ownerService;
@Autowired
private Utils utils;
@Autowired
private PlatformTransactionManager transactionManager;
private TransactionTemplate transactionTemplate;
@BeforeEach
void spickAndSpanDatabase() {
ownerService.deleteAll();
transactionTemplate = new TransactionTemplate(transactionManager);
}
@Test
void whenDatabaseEmpty_ThenReturnEmpty() {
final Iterable<Owner> owners = ownerService.findAll();
assertThat(owners).isEmpty();
}
...

Now we can write a test that uses the transaction context like this

@Test
void whenServiceMethodFails_ThenTransactionIsRolledBack() {
assertThat(ownerService.findAll()).isEmpty();
final Owner existing = ownerService.save(utils.randomOwner());
assertThat(ownerService.findAll()).isEqualTo(asList(existing));
transactionTemplate.execute(status -> {
try {
// Succeeds
//
ownerService.save(utils.randomOwner());
// Fails
//
ownerService.save(Owner.of(null, null, null, null));
} catch (final Exception e) {
status.setRollbackOnly();
}
return "ok";
});
assertThat(ownerService.findAll()).isEqualTo(asList(existing));
}

Notice that both save()s are inside a transaction context. And the whole thing works because each method in OwnerService either takes an existing or creates a new transaction if there’s not one in the context: That’s the semantics of @Transactional with the default propagation.

The upshot

We looked at whether we could take a simpler route to persistence in Java that might be more robust and easier to get right than alternatives.

There’s a lot to the Spring Boot/Spring Data ecosystem and we only looked at a small corner. Judge for yourselves whether Spring Data JDBC is for you.

In any case, please let us know what you think.

Stephen Harrison, Venmo SRE