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 UserAccountsince 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 BeforeSaveEventin 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”

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *