FetchType.LAZY not working for @OneToOne relation

Viewed 738

I'm using JPA2 and Springboot2X. Trying to define a simple one-to-one relationship in User and Account tables. User is the parent while Account is the child. The relationship is from parent to child. Followed this.

User table has a column account_no to hold the account number which is the primary key of the Account table. So User holds the foreign key.

@Entity
public class Account extends AbstractEntity implements Serializable{

    // fields like account#, balance etc goes here..

    @OneToOne (fetch = FetchType.LAZY)
    private User user;

}

@Entity
public class User extends AbstractEntity implements Serializable{

    // Other fields related to user entity go here ..

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "account_no", referencedColumnName = "account_number")
    private Account account;
}

Saving is working properly. But when I fetch a user ( User user = userRp.findByUserID(userid);), I get data from both tables. My question is when I define fetch = FetchType.LAZY why the account table data is fetching? I only want the user data to be in the output as when I run the select query in the User table.

Output :

    {
    "id": 1,
    "userID": "xxxxx",
    "firstName": "Dsd",
    "llastName": "cccc",
    "email": "danqsaa@brown",
    "creation_date": "2021-12-19 19:41",
    "modified_date": "2021-12-19 19:41",
    "account": {
        "id": 2,           
        "accountNo": 6848982326,
        "balance": 4000.0,
        "creationUser": "xxx",
        "creation_date": "2021-12-19 19:41",
        "modified_date": "2021-12-19 19:41",
        "transactions": []
    }
}

DB output enter image description here

enter image description here

Added below RestController, Service, and Repo details.

@RestController
public class UserController {

    @GetMapping("/users/{userID}")
        public ResponseEntity<Object> getUser(@PathVariable("userID") String userID){
                   return new ResponseEntity<>(userServiceImpl.getUserByID(userID), HttpStatus.OK)); 
        }
}

@Service
public class UserService {

 // injected user repo
public User getUserByID(String userID){
        return  userRepo.findByUserID(userID);      
    }
}

public interface UsersRepository extends CrudRepository<User, String> {
     User findByUserID(String userID);
}
2 Answers

I thought I had the answer about this question, thinking it was just Jackson calling the getter during serialization for response object and that had as a result the fetch of the lazy property but it seems that this is not the case!!

This question was a good reason to dig and learn something strange from documentation.

The most common ORM vendor is hibernate. So if you read the documentation on hibernate it clearly states that EAGER type is a must conform for Hibernate, but the LAZY fetch type is just a hint that hibernate may or may not follow !!

From the hibernate doc

fetch - FetchType

(defaults to EAGER) Defines whether this attribute should be fetched eagerly or lazily. JPA says that EAGER is a requirement to the provider (Hibernate) that the value should be fetched when the owner is fetched, while LAZY is merely a hint that the value is fetched when the attribute is accessed. Hibernate ignores this setting for basic types unless you are using bytecode enhancement. See the Bytecode Enhancement for additional information on fetching and on bytecode enhancement.

Also according to the doc, you have to follow at least for now the bytecode enhancment in order for lazy loading to work

As a hopefully temporary legacy hold-over, it is currently required that all lazy singular associations (many-to-one and one-to-one) also include @LazyToOne(LazyToOneOption.NO_PROXY). The plan is to relax that requirement later.

Also according to hibernate performance enhancement doc you also need a configuration property for hibernate, so in a Spring Boot App, please also include the following in your application.yml or properties file.

hibernate.enhancer.enableLazyInitialization = true

After you include this property, your entity must be updated to

@Entity
public class User extends AbstractEntity implements Serializable{

    // Other fields related to user entity go here ..

    @LazyToOne(LazyToOneOption.NO_PROXY)  <------
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "account_no", referencedColumnName = "account_number")
    private Account account;
}

@Entity
public class Account extends AbstractEntity implements Serializable{

    // fields like account#, balance etc goes here..

    @LazyToOne(LazyToOneOption.NO_PROXY) <-----
    @OneToOne (fetch = FetchType.LAZY)
    private User user;

}

All these are needed just for hibernate to enable the lazy loading functionality.

Then you would still see it in your response, because Jackson would call the getter method while serializing so it will force hibernate to fetch the value. So you also need to inform jackson to not use this getter during serialization.

So you must also update

  @Entity
  public class User extends AbstractEntity implements Serializable{

    @JsonIgnore <--------
    @JsonProperty(access =  JsonProperty.Access.WRITE_ONLY) <-------
    @LazyToOne(LazyToOneOption.NO_PROXY) 
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "account_no", referencedColumnName = "account_number")
    private Account account; 

Hopefully after all this should you be able to not see the account response because of lazy loading.

It's unfortunate that the question is misleading and it takes many comments to find out what you're asking. Finally I understand - you see the field in JSON and you don't want to. This is a purely Jackson-related question and Hibernate has nothing to do with it.

There are several ways you can take:

Use DTO

There's a good reason we don't use our Entities as part of JSON serialization - exactly because it's rare that they satisfy our JSON requirements.

So instead we create a special DTO class which maps exactly how we need it for a particular endpoint. We convert our Entity to the DTO (and it will not include account field) and then serialize DTO.

Using Jackson Views

Jackson has a notion of a JsonView - it lets you map the same object differently depending on the view that you request. This way you can make account field invisible to that particular endpoint.

Ignore properties

As was already hinted in the other response - you can @JsonIgnore the property or rid of its visibility or making it deserialize-only with WRITE_ONLY.

Jackson Hibernate module

Jackson has "plugins" specific for Hibernate: jackson-datatype-hibernate. They allow to be configured to ignore lazy fields and not initialize them. In such case Jackson will serialize null (HibernateXModule.Feature).

This is probably the worst option of all because you really don't want to tie how you optimize your work with DB and your presentation layer.

Related