1

So I'm going through various resources trying to learn Spring Boot and Rest API and I've encountered the same problem in several different tutorials and textbooks. It seems to stem from the CrudRepository interface and more specifically the JpaRepository.findById() method.

Every tutorial I've read has something to the effect of:

@GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable("id") final Long id){
        UserDTO user = userJpaRepository.findById(id);
        if (user == null) {
            return new ResponseEntity<UserDTO>(
                    new CustomErrorType("User with id " + id + " not found"),
                    HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<UserDTO>(user, HttpStatus.OK);

However, the UserDTO user = userJpaRepository.findById(id); won't compile.

I figured out if I change it to UserDTO user = userJpaRepository.findById(id).get(); it compiles, runs, and the GET is successful. The problem is if the user ID isn't found in the GET request it doesn't return NULL and I get a 500 internal server error.

The tooltips and suggestions from my IDE corrected the code to

@GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable("id") final Long id){
        Optional<UserDTO> user = userJpaRepository.findById(id);
        if (user == null) {
            return new ResponseEntity<UserDTO>(
                    new CustomErrorType("User with id " + id + " not found"),
                    HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<UserDTO>(HttpStatus.OK);
    }

Which works just as it should for the GET request and error handling. Why do all the tutorials have it listed the first way? Could someone explain to me what is going on here?

Jeff O.
  • 15
  • 1
  • 4
  • you need to be certain that some record in your database with the value of `id` exists, maybe that's why the tutorials do that. It's safer to have that `Optional` generic imo, writing a if condition and logging a `null` is far easier than finding it in production. – tsamridh86 Aug 08 '21 at 13:23
  • 3
    See https://stackoverflow.com/a/49317013/2612030. Apparently the API was changed. – ewramner Aug 08 '21 at 13:24
  • 1
    @MauricioGraciaGutierrez as to why it's presented differently, yes. As to what's actually going on and how I can get it to work, no. I'm trying to figure out the different HTTP request and how they correspond the CRUD interface. The HTTP methods work fine as long as the fields are valid. I've managed to get the `GET` request working and handling errors in a passable way. But none of the error handling for `POST` `PUT` or `DELETE` http methods are behaving the way this book I'm following indicate they should. – Jeff O. Aug 08 '21 at 16:22

2 Answers2

1

The JpaRepository.getById can retrieve a database record by id. This method is pre-defined, same as the findAll methods. The mentioned method CrudRepository.findById was inherited from ancestor class CrudRepository. It returns a Optional<T> since spring-data migrated to Java 8 (since Spring-Data-Jpa version 2.0).

See more: Spring Data JPA findOne() change to Optional how to use this?

Implementing JpaRepository's find-methods

However, if you implement the JpaRepository and add a new findByName method, this relies on JPA's Query Language (JPQL) and implicitly issues a prepared statement like SELECT * FROM table WHERE name = ?. Here the WHERE clause's predicates like name = ? are extracted and built from method-name, after By.

Rational behind Optional return

The default return of any find method is Optional<T> since Java 8 (which introduced the Optional type). This is because a search may either find some or not. If nothing is found, it is safer to return Optional instead of null or throw an exception. Main benefit and intention behind Optional returns are:

  • signal empty = optional results in the method signature (instead of implicit null before)
  • force the API user/developer to deal with empty returns
  • avoid incidental NullPointerExceptions (NPE)

Update to deal with empty results found:

Handling NOT FOUND in REST controllers

A RESTful way of responding in cases of empty or not-found results on CRUD resources is:

  • to return HTTP status 404 NOT FOUND with some customized/descriptive error-message.

This "unhappy path" can easily be achieved by throwing Spring's ResponseStatusException like described in Baeldung's REST exception-handling tutorial like this:

@GetMapping("/{id}")
public UserDTO getUserById(@PathVariable("id") final Long id) {
    Optional<UserDTO> user = userJpaRepository.findById(id);
    if (user.isEmpty()) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("User with id %d not found", id));
    }

    return user;
}

What was simplified:

  • throwing ResponseStatusException leads to an exceptional return and signals Spring to respond with specified status (404) and body (message).
  • controller methods can simply return the type UserDTO (without wrapping in ResponseEntity) because Spring will convert to response-representation
  • for @GetMapping the return always gets HttpStatus.OK assigned by default

Further, like Mauricio Gracia Gutierrez tutoring answer explains ideomatic Optional handling, the method-body can be simplified to a one-liner:

return userJpaRepository.findById(id)
       .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("User with id %d not found", id)));
hc_dev
  • 8,389
  • 1
  • 26
  • 38
  • This makes some sense to me. I was unaware that `Optional` was a type. Based on what you've said though if it's not returning `null` when the ID isn't found why is the `if` statement being executed in the second example? – Jeff O. Aug 08 '21 at 13:49
  • @JeffO. Well, `Optional` is, in a practical sense, more a type-wrapper, to be specific a _generic type_. `if (user == null)` most-likely will always be `false`. The usual way to handle `Optional` returns would suggest `if (user.isEmpty())`. – hc_dev Aug 08 '21 at 13:52
  • 1
    @hc_dev thanks `user.isEmpty()` makes more sense to me than `user == null` however they both work if the GET request is for a bad Id. But, I see in the doc that `Optional ==` shouldn't be used. – Jeff O. Aug 08 '21 at 14:46
  • 1
    @JeffO. I updated my answer after the horizontal ruler to advise improved NOT FOUND handling in Spring REST-controllers ️See how your code can be simplified incorporating the practical hints from [Mauricio's answer](https://stackoverflow.com/a/68701499/5730279). – hc_dev Aug 08 '21 at 18:33
  • 1
    very helpful thanks. I've been trying to get started with REST api for a week through several different resources. They all have presented error handling the way my first code block is formatted. Very frustrating. – Jeff O. Aug 08 '21 at 19:45
1

Some hints on how to use Optional with Optional<T> findById(ID id).

Generally, as you look for an entity by id, you want to return it or make a particular processing if that is not retrieved.

Here are three classical usage examples.

  1. Suppose that if the entity is found you want to get it otherwise you want to get a default value.

You could write :

Foo foo = repository.findById(id)
                    .orElse(new Foo());

or get a null default value if it makes sense (same behavior as before the API change) :

Foo foo = repository.findById(id)
                    .orElse(null);
  1. Suppose that if the entity is found you want to return it, else you want to throw an exception.

You could write :

return repository.findById(id)
        .orElseThrow(() -> new EntityNotFoundException(id));
  1. Suppose you want to apply a different processing according to if the entity is found or not (without necessarily throwing an exception).

You could write :

Optional<Foo> fooOptional = fooRepository.findById(id);
if (fooOptional.isPresent()) {
    Foo foo = fooOptional.get();
    // processing with foo ...
} else {
    // alternative processing....
}

Exctract taken from Spring Data JPA findOne() change to Optional how to use this?

Mauricio Gracia Gutierrez
  • 10,288
  • 6
  • 68
  • 99
  • 1
    These hints on Optional usage for conditional behavior (to branch execution) are useful Java patterns - not only for JPA-related find results. – hc_dev Aug 08 '21 at 16:32