0

Please note that I have looked at similar questions and I have explained why they haven't worked for me

I have a simple Spring boot JPA-Hibernate application with one to one mapping between User and Address. (Please note that I do not have this issue with one to many mapping)

User Entity

@Entity
    @Table(name = "users")
    public class User implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;

        @Column
        private String name;

        @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "user")
        private Address address;

        @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
        private Set<Note> notes;

    }

Address Entity

   @Entity
    @Table(name = "addresses")
    public class Address implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;

        @Column
        private String street;

        @Column
        private String city;

        @JsonIgnore
        @OneToOne
        @JoinColumn(name = "user_id")
        private User user;

    }

Note Entity

@Entity
    @Table(name = "notes")
    public class Note implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;

        @Column
        private String date;

        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id", nullable = false)
        private User user;

    }

My problem is that whenever I call the controller mapped to get all users I was getting the address and all the associated notes with it as well. But I would expect FetchType.LAZY to take care of that.

I read a lot of questions on StackOverflow mentioning that Jackson might be the culprit here:

Post 1

I also read that spring.jpa.open-in-view defualt value might be the culprit:

Post 2

Post 3

So i tried the following options:

I disabled default open in view property by adding spring.jpa.open-in-view=false to my application.properties which started giving me

Could not write JSON: failed to lazily initialize a collection of role error

I am assuming its because Jackson is calling the getters on my lazily loaded objects so I followed the instructions from another post and added the following for Jackson to leave the lazily loaded collections alone:

pom.xml

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate5</artifactId>
    <version>2.9.9</version>
</dependency>

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (HttpMessageConverter converter : converters) {
            if (converter instanceof org.springframework.http.converter.json.MappingJackson2HttpMessageConverter) {
                ObjectMapper mapper = ((MappingJackson2HttpMessageConverter) converter).getObjectMapper();
                mapper.registerModule(new Hibernate5Module());
            }
        }
    }

}

This solution above fixed the issue with the One to Many mapping but still has the Address associated in the response.

I am not sure what can I do here. The User Entity on the default landing page does not need any address details so I do not want to load it on the landing page. When the record is clicked then it navigates to another page and that's where I would like all the lazy loaded objects to be returned in the response.

I have tried everything I could find online but still nothing has worked so far. I would really appreciate some help with this.

As mentioned by one of the users that it might a duplicate of another question on SO: Suggested Possible duplicate I would like to mention that I got the Lazy loading working by disabling spring.jpa.open-in-view property but adding

mapper.registerModule(new Hibernate5Module());

brings back the address associated to the User in the response.

Nick Div
  • 4,261
  • 8
  • 55
  • 108
  • Have you considered serializing some kind of DTO (dedicated per use case rather than single entity)? – Piotr Podraza Jul 23 '19 at 17:39
  • I have but I would also like to figure out a solution to this issue. I dont want to develop further without exploring all the options. – Nick Div Jul 23 '19 at 17:59
  • Possible duplicate of [Jackson triggering JPA Lazy Fetching on serialization](https://stackoverflow.com/questions/47989857/jackson-triggering-jpa-lazy-fetching-on-serialization) – K.Nicholas Jul 23 '19 at 18:54
  • Are you asking (1) how to prevent Jackson from mapping `User.address`, or (2) how to enable lazy loading on `User.adress`? Those are very different questions. You already got two answers, each answering a different one out of the two I listed. Note that lazy loading means *load when requested*, **not** *do not load at all* – crizzis Jul 23 '19 at 19:09
  • @crizzis I am asking how Jackson can respect lazy loading. Even when I get the lazy loading working, Jackson does not respect it and loads the associations. – Nick Div Jul 23 '19 at 19:16
  • @K.Nicholas You are right, it is almost identical but I am trying to understand even when I got the entities lazy loaded how adding Hibernate5Module() brings back the associations. How is it doing exactly the opposite of what its supposed to do – Nick Div Jul 23 '19 at 19:17
  • ...which is exactly why I said 'lazy loading means load when requested, not do not load at all', it seems you are confused about this part. I'm not sure why you expected Jackson not to try to serialize a lazy-loaded property, but without `@JsonIgnore`, it simply accesses it and triggers the lazy loading (in other words, Jackson *requests* for `User.adress` to be loaded) – crizzis Jul 23 '19 at 19:19
  • @crizzis I was expecting the added Hibernate5Module to recognize that its a lazy loaded entity but doesnt that mean that adding Hibernate5Module() basically added no value to the situation? – Nick Div Jul 23 '19 at 19:25
  • It might be the case that your `FetchType.LAZY` hint is simply [not being respected by Hibernate](https://stackoverflow.com/questions/1444227/making-a-onetoone-relation-lazy). In any case, I'd strongly advise against relying on LAZY/EAGER hints for controlling the JSON schema of the serialized entity. Either your API clients depend on the property or they don't. If you happen to trigger lazy loading for some other reason in the future, your API consumers will see surprising results – crizzis Jul 23 '19 at 19:38
  • @crizzis The issue is it happens only with the One to One mapping. I should have emphasized that more in the question. But seems like using DTO or the JSON View is the way to go. – Nick Div Jul 23 '19 at 19:44
  • @NickDiv - Perhaps https://stackoverflow.com/questions/38273640/spring-hibernate-jackson-hibernate5module. However, IMHO, the real question is why not use DTOs? Generally a much better solution. The whole `open-in-view` thing is about moving the transactions to the controller level instead of leaving them in the service level where they belong. You could simply solve the problem by setting the association to null before returning it to Jackson. Not to sound harsh, but the bottom line is you should be considering the architecture of your application instead applying hacks willy-nilly. – K.Nicholas Jul 23 '19 at 19:44
  • @NickDiv Then it sure looks a lot more as though the hint weren't respected by Hibernate. You can try adding `optional = false` to see if the problem goes away. Still, unless you want multiple JSON representations for a single entity, a simple `@JsonIgnore` should work in your scenario – crizzis Jul 23 '19 at 19:47
  • @K.Nicholas Yeah, I think I will go with DTOs. I just wanted to understand the cycle and figure out why its not working. – Nick Div Jul 23 '19 at 19:52
  • @crizzis I am going to try setting optional attribute too. If that doesnt work too then I will simply use a DTO as JSONIgnore will not render it on other other calls as well. – Nick Div Jul 23 '19 at 19:53
  • @crizzis Setting the optional attribute did not work but there was another workaround i found in a blog which was a hacky way of doing it so chose to stick with DTO. Appreciate all the suggestions and help :) – Nick Div Jul 24 '19 at 06:10
  • @K.Nicholas I finally chose to go with the DTO. I directly cast the Entity to DTO in the repository itself so that avoids the association call and also has only the information needed by the UI. Thanks for the help and all the suggestions. – Nick Div Jul 24 '19 at 06:11

3 Answers3

1

You may take a look at Jackson Serialization Views.

I´ve taken a look into the Hibernate5 module you tried and it has some interesting features... but none should fix this issue out of the box for you.

By the way, I normally fix this issue by not returning the Entity as the response but DTOs instead.

Bohemian
  • 365,064
  • 84
  • 522
  • 658
Emanuel Ramirez
  • 352
  • 2
  • 8
  • JsonView might fix the issue that I am having but I am just wondering it would Lazy load calls. Let me try – Nick Div Jul 23 '19 at 19:04
1

It's working as in the JPA spec:-

Refer the below URL https://javaee.github.io/javaee-spec/javadocs/javax/persistence/FetchType.html

LAZY fetching strategy is only a hint (as the javadoc says the data can be lazily fetched).. not a mandatory action.

Eager is mandatory (as the javadoc says the data must be eagerly fetched).

1

The problem is jackson triggering initialization when he writes the JSON, so just don't write the current field (address). But you should not use @jsonIgnore so at other places you could return an Eager obj.

You can use the @jsonView annotation that can provide different JSON for the same obj at different requests. You can look this example :

Create view class:

public class ViewFetchType {
    static class lazy{ }
    static class Eager extends lazy{ }
}

Annotate your Entity

@Entity
public class User {

    @Id
    @JsonView(ViewFetchType.Lazy.class)
    private String id;

    @JsonView(ViewFetchType.Eager.class)
    @OneToOne( fetch = FetchType.LAZY)
    private Address address ;
}

Specify the FetchType class in your controller:

public class UserController {

    private final UserRepository userRepository;

    @Autowired
    UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @RequestMapping("get-user-details")
    @JsonView(ViewFetchType.Eager.class)
    public @ResponseBody Optional<User> get(@PathVariable String email) {
        return userRepository.findByEmail(email);
    {

    @RequestMapping("get-all-users")
    @JsonView(ViewFetchType.Lazy.class)
    public @ResponseBody List<User> getUsers() {
        return userRepository.findAll();
    }
}

Here is the answer that i took the idea from... https://stackoverflow.com/a/49207551/10162200

Paul Roub
  • 35,100
  • 27
  • 72
  • 83
Itay wazana
  • 150
  • 5