How to get entities with relationships in Spring Rest controller

Let’s imagine that you are developing the most common REST service on the Spring stack, for example, using these dependencies (build.gradle):

...
dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	...
}

Everything is going as usual. You write a repository for the User entity:

public interface UserRepository extends CrudRepository<User, UUID> {
}

Then write the most common service for working with a user, which has a method for getting a user by his Id:

public class UserService {
    ...
    public User getUser(UUID id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }
    ...
}

And finally, use this method in your REST controller to get user data via REST API from outside:

@RestController
@RequestMapping("/users")
public class UserController {
    ...
    @GetMapping(path = "/{id}")
    public User getUser(@PathVariable UUID id) {
        return userService.getUser(id);
    }
    ...
}

I deliberately do not dwell on the settings for connecting Spring to the database in application.properties or in some other way, as well as other details of setting up and creating a service, assuming that you already know how, you have a database and are available, users in it created, the User entity class is also created in the application.

The next step is to test the application. Let’s use Postman for this. First of all, we launch our application (let’s assume we are working in Intellij Idea) and see that it started successfully:

Now we go to Postman and try to make a GET request to the REST API method to get the user, which we probably know is in the database, by its Id:

Horrible! We see that we cannot get any response to our request at all, not even a 500 Interal error! What’s the matter? After all, this, it would seem, is the most common REST service. Further, as a more or less experienced junior developer, you go back to the Intellij Idea console and see the following error:

So, let’s see what’s the matter. From the log, we see that we have some kind of Contact and Profile entities (and why are they here, because we worked with the User entity?), And Spring is not able to generate a JSON response for our REST request, because it falls into an endless recursion . Why? Now the fun begins.

Let’s now see in detail what is happening to our user. First of all, let’s look at the User entity itself:

...
@Entity
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public class User {
    @Id
    @GeneratedValue
    private UUID id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "profile_id", referencedColumnName = "id")
    private Profile profile;
    ...
}

How interesting! It turns out that we store user data not only in the User entity, but also in the Profile entity bound to it. You made such a one-to-one relationship, but forgot about it.

Let’s take a look at the Profile entity:

...
@Entity
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public class Profile {
    @Id
    @GeneratedValue
    private UUID id;

    private String name;

    private String surname;

    @Column(name = "second_name")
    private String secondName;

    @Column(name = "birth_date")
    private Instant birthDate;

    @Column(name = "avatar_link")
    private String avatarLink;

    private String information;

    @Column(name = "city_id")
    private Integer cityId;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "contact_id", referencedColumnName = "id")

    private Contact contact;

    @Column(name = "gender_id")
    private UUID gender_id;

    @OneToOne(mappedBy = "profile")
    User user;

}
...

Oops! In addition to the “reverse end” of the one-to-one profile relationship with the user

    @OneToOne(mappedBy = "profile")
    User user;

we see that the profile itself is connected in a one-to-one type with one more entity – Contact. That is, you selected the user’s contacts as a separate entity as well. We can take a look at it:

@Entity
@Getter
@Setter
@RequiredArgsConstructor
@ToString
public class Contact {
    @Id
    @GeneratedValue
    private UUID id;

    private String email;

    private String phone;

    @OneToOne(mappedBy = "contact")
    Profile profile;
}

Well, it seems that this is the end of the chain of related entities – the contact is not connected to anything else.

So, we have three interrelated entities – user, profile and contact. We will not dwell on this in detail, but it is obvious that you have imposed the appropriate constraint on them in the database, something like this:

alter table users_scheme.user add constraint user_profile_fk foreign key (profile_id) references users_scheme.profile(ID) on delete cascade on update cascade;
alter table users_scheme.profile add constraint profile_contact_fk foreign key (contact_id) references users_scheme.contact(ID) on delete cascade on update cascade;

That’s why you’ve added an implementation of these restrictions to your entity classes so that Hibernate can handle them properly. But it doesn’t work.

You probably already guessed the reason if you remembered that our one-to-one relationship is two-way. So when trying to serialize an instance of the fasterxml user class, jackson gets caught in a loop and can’t do it. The conclusion is that this cycle needs to be somehow processed, making it not infinite, or interrupted.

This situation is new for you, so I will immediately give the most interesting, in my opinion, solution that allows you to get the most complete information in JSON format in the response to the request, including information from related entities.

There are other ways to do the same thing, but they work differently and may not work at all or give the same complete information. Therefore, I will stop here to show you only this one. Perhaps I will consider other methods in other articles.

So what needs to be done. We mark the following fields in each of the entities with the following annotation:

//пользователь
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id", referencedColumnName = "id")
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
private Profile profile;

//профиль
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "contact_id", referencedColumnName = "id")
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
private Contact contact;

//контакт
@OneToOne(mappedBy = "contact")
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
    Profile profile;

Then we restart the application and repeat the request in Postman:

Now everything is in perfect order, we received a normal JSON response with user data from all three related entities. There are no more errors in the IDE console either.


OTUS will host an open class soon “Aspects in Java and in Spring”, on which we will consider aspects – what is it and why is it needed; how to create aspects in Java using different technologies and how they are used in Spring. Registration for everyone – link.

Similar Posts

Leave a Reply

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