javaspringspring-bootspring-data-jpa

Spring Boot @OneToOne bidirectional relationship lazy loading does not work


i have three entities USER ,RefreshToken, and ForgotPassword

public class User {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;
    private String email;
    private String password;

    @OneToOne(mappedBy = "user",fetch = FetchType.LAZY)
    private RefreshToken refreshToken;
    @Enumerated(EnumType.STRING)
    private Role role;

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true ,fetch = FetchType.LAZY)
    private ForgotPassword forgotPassword;

      
    // getter setters



    }
    
@Entity
public   class RefreshToken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer tokenId;

    @Column(nullable = false, length = 500)
    private String refreshToken;

    @Column(nullable = false)
    private Instant expirationTime;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private User user;

    //getter setters
@Entity
public class ForgotPassword {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private Long otp;
    private Date expirationTime;
    @OneToOne(fetch = FetchType.LAZY)
    private User user;
    
    // getter setters
}

and i want to use this method to generate otp and send email to user when he forgets his password.

@PostMapping("/verifyMail/{email}")
    public ResponseEntity<String> verifyEmail(@PathVariable String email) {
        long otp = forgotPasswordService.generateAndStoreOtp(email);
        emailService.sendOtpEmail(email, otp);
        return ResponseEntity.ok("Email sent for verification");
    }
public void sendOtpEmail(String email, long otp) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(email);
        message.setFrom("hasanorentr@gmail.com");
        message.setSubject("OTP for Forgot Password Request");
        message.setText("This is the OTP for your Forgot Password request: " + otp);

        mailSender.send(message);
    }
public long generateAndStoreOtp(String email) {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("Please provide a valid email!"));

        int otp = otpGenerator(); // Generates a 6-digit OTP

        String redisKey = "otp:" + email; // Store OTP with user's email as key

        // Store OTP with 70 seconds expiration
        redisTemplate.opsForValue().set(redisKey, otp, Duration.ofSeconds(70));

        return otp;
    }


But the problem is when i execute userRepository.findByEmail() method, it hit the db three times instead of only one. Because i annotated FetchType.LAZY in *@OneToOne annotations. So i expect this method hit the db only once. Can anyone help me out about this issue. And im not master in spring data jpa.

Hibernate: select u1_0.id,u1_0.email,u1_0.password,u1_0.role from users u1_0 where u1_0.email=?
Hibernate: select fp1_0.id,fp1_0.expiration_time,fp1_0.otp,fp1_0.user_id from forgot_password fp1_0 where fp1_0.user_id=?
Hibernate: select rt1_0.token_id,rt1_0.expiration_time,rt1_0.refresh_token,rt1_0.user_id from refresh_token rt1_0 where rt1_0.user_id=?

Solution

  • According to Hibernate documentation.

    Bidirectional @OneToOne lazy association

    Although you might annotate the parent-side association to be fetched lazily, Hibernate cannot honor this request since it cannot know whether the association is null or not.

    The only way to figure out whether there is an associated record on the child side is to fetch the child association using a secondary query. Because this can lead to N+1 query issues, it’s much more efficient to use unidirectional @OneToOne associations with the @MapsId annotation in place.

    However, if you really need to use a bidirectional association and want to make sure that this is always going to be fetched lazily, then you need to enable lazy state initialization bytecode enhancement.

    According to the documentation this is the expected behavior and if you want to have bidirectional relationship of OneToOne, you will have to try and fine tune it through bytecode enhancement.

    So you can try adding the property hibernate.enhancer.enableLazyInitialization: true but as mentioned in the documentation, this setting is deprecated without an expected replacement.