Introduction to Spring Data JDBC Baeldung
For future students of the course “Java Developer. Professional” prepared a translation of useful material.
We also invite you to take part in an open lesson on the topic “Introducing Spring Data jdbc”
Spring Data JDBC was announced in 2018. The goal was to provide developers with a simpler alternative to JPA while continuing to follow the Spring Data principles. You can learn more about the motives behind the project in documentation…
In this article, I will show some examples of using Spring Data JDBC. There won’t be a detailed tutorial here, but hopefully the information provided is enough to give you a try yourself. Very good if you are already familiar with Spring Data JPA. You can find the source code at github…
For a quick start, I used this template…
Preliminary preparation
From dependencies we need data-jdbc
– starter, flyway
to control the circuit and driver postgres
to connect to the database.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.flywaydb:flyway-core'
runtimeOnly 'org.postgresql:postgresql'
}
Next, we configure the application to connect to the database:
# application.yml
spring:
application:
name: template-app
datasource:
url: jdbc:postgresql://localhost:5432/demo_app?currentSchema=app
username: app_user
password: change_me
driver-class-name: org.postgresql.Driver
Entity mapping
For this example, we will use the following table:
create table book (
id varchar(32) not null,
title varchar(255) not null,
author varchar(255),
isbn varchar(15),
published_date date,
page_count integer,
primary key (id)
);
And the corresponding java class (note that @Id
imported from org.springframework.data.annotation.Id
):
// Book.java
public class Book {
@Id
private String id;
private String title;
private String author;
private String isbn;
private Instant publishedDate;
private Integer pageCount;
}
However, if we run test:
// BookRepositoryTest.java
@Test
void canSaveBook() {
var book = Book.builder().author("Steven Erikson").title("Gardens of the Moon").build();
var savedBook = bookRepository.save(book);
assertThat(savedBook.getId()).isNotBlank();
assertThat(savedBook.getAuthor()).isEqualTo(book.getAuthor());
assertThat(savedBook.getTitle()).isEqualTo(book.getTitle());
assertThat(savedBook).isEqualTo(bookRepository.findById(savedBook.getId()).get());
}
Then we will see an error – ERROR: null value in column “id” violates not-null constraint. This is because we have not defined a way to generate id or a default value. Spring Data JDBC Identity Behavior slightly different from Spring Data JPA. In our example, we need to define ApplicationListener
for BeforeSaveEvent
:
// PersistenceConfig.java
@Bean
public ApplicationListener<BeforeSaveEvent> idGenerator() {
return event -> {
var entity = event.getEntity();
if (entity instanceof Book) {
((Book) entity).setId(UUID.randomUUID().toString());
}
};
}
The test will now pass because the Id field is being filled. For a complete list of supported lifecycle events, see documentation…
Query methods
One of the features of Spring Data projects is the ability to define query methods in repositories. Spring Data JDBC takes a slightly different approach here. For demonstration, let’s define a request method in BookRepository
:
Optional<Book> findByTitle(String title);
And if we run the corresponding test:
@Test
void canFindBookByTitle() {
var title = "Gardens of the Moon";
var book = Book.builder().author("Steven Erikson").title(title).build();
var savedBook = bookRepository.save(book);
assertThat(bookRepository.findByTitle(title).get()).isEqualTo(savedBook);
}
We get an error – Caused by: java.lang.IllegalStateException: No query specified on findByTitle
… Currently Spring Data JDBC supports only explicit queries via @Query. Let’s write a sql query for our method:
@Query("select * from Book b where b.title = :title")
Optional<Book> findByTitle(@Param("title") String title);
Test passed! Keep this in mind when creating your repositories.
Translator’s note: in Spring Data JDBC 2.0 support for generating queries by method names…
Connections
Spring Data JDBC also takes a different approach to work with bindings. The main difference is that there is no lazy loading. So if you don’t need a relationship in the entity, then just don’t add it there. This approach is based on one of the concepts of Domain Driven Design, according to which the entities that we load are the roots of the aggregates, so we need to design so that the roots of the aggregates pull the loading of other classes with them.
One to one
For one-to-one and one-to-many relationships, the annotation is used @MappedCollection
… Let’s look at one-to-one first. Class UserAccount
will refer to Address
… Here is the relevant sql:
create table address
(
id varchar(36) not null,
city varchar(255),
state varchar(255),
street varchar(255),
zipcode varchar(255),
primary key (id)
);
create table user_account
(
id varchar(36) not null,
name varchar(255) not null,
email varchar(255) not null,
address_id varchar(36),
primary key (id),
constraint fk_user_account_address_id foreign key (address_id) references address (id)
);
Class UserAccount
looks something like this:
// UserAccount.java
public class UserAccount implements GeneratedId {
// ...other fields
@MappedCollection(idColumn = "id")
private Address address;
}
Other fields have been omitted here to show the mapping. address
… Value in idColumn
Is the name of the class id field Address
… Note that in the class Address
no class reference UserAccount
since the aggregate is UserAccount
… This is demonstrated in the test:
//UserAccountRepositoryTest.java
@Test
void canSaveUserWithAddress() {
var address = stubAddress();
var newUser = stubUser(address);
var savedUser = userAccountRepository.save(newUser);
assertThat(savedUser.getId()).isNotBlank();
assertThat(savedUser.getAddress().getId()).isNotBlank();
var foundUser = userAccountRepository.findById(savedUser.getId()).orElseThrow(IllegalStateException::new);
var foundAddress = addressRepository.findById(foundUser.getAddress().getId()).orElseThrow(IllegalStateException::new);
assertThat(foundUser).isEqualTo(savedUser);
assertThat(foundAddress).isEqualTo(savedUser.getAddress());
}
One-to-many
Here’s the sql we’ll use to demonstrate a one-to-many relationship:
create table warehouse
(
id varchar(36) not null,
location varchar(255),
primary key (id)
);
create table inventory_item
(
id varchar(36) not null,
name varchar(255),
count integer,
warehouse varchar(36),
primary key (id),
constraint fk_inventory_item_warehouse_id foreign key (warehouse) references warehouse (id)
);
In this example, there are many inventoryitems in the warehouse. Therefore in the class Warehouse
we will also use @MappedCollection
for InventoryItem
:
public class Warehouse {
// ...other fields
@MappedCollection
Set<InventoryItem> inventoryItems = new HashSet<>();
public void addInventoryItem(InventoryItem inventoryItem) {
var itemWithId = inventoryItem.toBuilder().id(UUID.randomUUID().toString()).build();
this.inventoryItems.add(itemWithId);
}
}
public class InventoryItem {
@Id
private String id;
private String name;
private int count;
}
In this example, we are setting the field id
in a helper method addInventoryItem
… You can also define ApplicationListener
for class Warehouse
with processing BeforeSaveEvent
in which to set the field id
for all InventoryItem
… You don’t have to do exactly what I did. Take a look tests demonstrating some of the behavior of a one-to-many relationship. The main thing is that saving or deleting an instance Warehouse
affects relevant InventoryItem
…
In our case InventoryItem
shouldn’t know about Warehouse
… Thus, this class only has fields that describe it. It is customary in JPA to do two-way communications, but it can be cumbersome and error prone if you forget to maintain both sides of the relationship. Spring Data JDBC contributes to creating only the relationships you need, so many-to-one feedback is not used here.
Many-to-one and many-to-many
For the purposes of this tutorial, I will not go into details about many-to-one or many-to-many relationships. My advice is to avoid many-to-many relationships and only use them as a last resort. Although sometimes they can be inevitable. Both of these types of relationships are implemented in Spring Data JDBC through links to Id
related entities. So keep in mind that you have a little more work to do here.
Conclusion
If you’ve used Spring Data JPA, then most of what I’ve just described should be familiar to you. I mentioned earlier that Spring Data JDBC tends to be simpler and therefore there is no lazy loading. In addition, there is no caching, dirty tracking and session. If you load an object in Spring Data JDBC, then it is fully loaded (including the bindings) and saved when you save it to the repository. The examples I’ve shown are very similar to their JPA counterparts, but remember that many Spring Data JPA concepts are missing from Spring Data JDBC.
Overall I like Spring Data JDBC. I admit that this may not be the best choice for all applications, however I would recommend giving it a try. As someone who has struggled with lazy loading and dirty tracking in the past, I appreciate its simplicity. I think this is a good choice for simple domains that don’t require a lot of custom queries.
That’s all for now, thanks for reading! I hope you found this tutorial helpful and provides a starting point for using Spring Data JDBC.
More about the course “Java Developer. Professional”.
Sign up for an open lesson “Introducing Spring Data jdbc”…