8
votes

I'm new here, I hope you can help me guys.

I have a problem with the lazy fetch type.

I don't like to fetch all relationship, just if I need that. When I use CommandLineRunner.run method, it's okay, it's fetching Lazily, but if I call that method from RestController, it's always fetching eagerly but I don't want that.

I tried:

  • with DTO and without DTO object.
  • upgrade all dependencies to the newest version.
  • Change @RestController annotation to @Controller
  • @Query annotation with LEFT JOIN FETCH on a custom method in the Repository
  • @Lazy(value=true)
  • @Basic(fetch = FetchType.LAZY)
  • @LazyCollection(value = LazyCollectionOption.TRUE)
  • @LazyToOne(value = LazyToOneOption.NO_PROXY)
  • @ElementCollection(fetch = FetchType.LAZY)
  • And sure, I tried to put fetch = FetchType.LAZY and into the @ManyToMany, @ManyToOne... annotation and with cascade things too.
  • Using @PersistenceContext private EntityManager manager with createQuery();

and finally, I'm using spring security with CustomUserDetailsService. When I log in it returns with the User object. If the @Transactional annotation is on the ServiceImpl class it's fetching eagerly, but if I remove that annotation, it's fetch Lazily, but this is only working on login.

Do you have any idea?

Repository:

import hu.pte.clms.model.domain.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor{
}

ServiceImpl:

import hu.pte.clms.model.domain.User;
import hu.pte.clms.repository.UserRepository;
import hu.pte.clms.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional
public class UserServiceImpl implements UserService{

    @Autowired
    private UserRepository userRepository;

    @Override
    public List<User> listAll(){
        return userRepository.findAll();
    }

    /* And another methods with this scheme */

Controller:

import hu.pte.clms.model.domain.User;
import hu.pte.clms.model.dto.UserDTO;
import hu.pte.clms.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api")
public class UserController{

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/user/all", method = RequestMethod.GET)
    public ResponseEntity<List<UserDTO>> listAll(){
        return new ResponseEntity<>(userService.listAll().stream().map(user ->
                new UserDTO(user.getId(), user.getFirstName(), user.getLastName(), user.getCity(), user.getCountry(), user.getBio(), user.getPictureUrl())).collect(Collectors.toList()), HttpStatus.OK);
        }

    @RequestMapping(value = "/auth/user")
    public LoginResult get(){
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if(!auth.getName().equals("anonymousUser")){
            User user = userService.findByUsername(auth.getName());
            return new LoginResult(auth.getName(), auth.getAuthorities(), user);
        }
        return null;
    }
}

LoginResult:

import hu.pte.clms.model.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class LoginResult implements UserDetails{
    private String password;
    private String name;
    private User user;
    private Collection<? extends GrantedAuthority> authorities;

    public LoginResult(String name, Collection<? extends GrantedAuthority> authorities, User user){
        this.name = name;
        this.authorities = authorities;
        this.user = user;
    }

    public LoginResult(String username, String s, boolean b, boolean userNonExpired, boolean credentialsNonExpired, boolean userNonLocked, Collection<? extends GrantedAuthority> authorities){}

    public LoginResult(String username, String password, List<GrantedAuthority> grantedAuthorities){
        this.name = username;
        this.password = password;
        this.authorities = grantedAuthorities;
    }

User:

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import hu.pte.clms.model.domain.relationship.UserSkill;

import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "USER")
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class User implements Serializable{

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "PASSWORD")
    private String password;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "AGE")
    private Short age;

    @Column(name = "SEX")
    private String sex;

    @Column(name = "PHONE")
    private String phone;

    @Column(name = "SKYPE")
    private String skype;

    @Column(name = "PRIMARY_EMAIL")
    private String primaryEmail;

    @Column(name = "SECONDARY_EMAIL")
    private String secondaryEmail;

    @Column(name = "CITY")
    private String city;

    @Column(name = "COUNTRY")
    private String country;

    @Column(name = "BIO")
    private String bio;

    @Column(name = "PICTURE_URL")
    private String pictureUrl;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "CONFIG_ID")
    private Config config;

    @JsonIgnore
    @ManyToMany
    @JoinTable(name = "REL_USER_ROLE", joinColumns = {@JoinColumn(name = "USER_ID")}, inverseJoinColumns = {@JoinColumn(name = "ROLE_ID")})
    private List<Role> roles = new ArrayList<>();

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "REL_USER_SECURITY_ROLE", joinColumns = {@JoinColumn(name = "USER_ID")}, inverseJoinColumns = {@JoinColumn(name = "SECURITY_ROLE_ID")})
    private List<SecurityRole> securityRoles = new ArrayList<>();

    @ManyToMany(mappedBy = "user")
    private List<UserSkill> skills = new ArrayList<>();

    @JsonIgnore
    @ManyToMany
    @JoinTable(name = "REL_USER_PROJECT", joinColumns = {@JoinColumn(name = "USER_ID")}, inverseJoinColumns = {@JoinColumn(name = "PROJECT_ID")})
    private List<Project> projects = new ArrayList<>();

    @JsonIgnore
    @OneToMany(mappedBy = "reviewed", cascade = CascadeType.ALL)
    private List<Review> reviews = new ArrayList<>();

 /*Getters & setters*/

UserDTO:

import com.fasterxml.jackson.annotation.JsonInclude;
import hu.pte.clms.model.domain.*;
import hu.pte.clms.model.domain.relationship.UserSkill;
import java.util.ArrayList;
import java.util.List;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class UserDTO{

    private Long id;
    private String username;
    private String password;
    private String firstName;
    private String lastName;
    private Short age;
    private String sex;
    private String phone;
    private String skype;
    private String primaryEmail;
    private String secondaryEmail;
    private String city;
    private String country;
    private String bio;
    private String pictureUrl;
    private Config config;
    private List<Role> roles = new ArrayList<>();
    private List<SecurityRole> securityRoles = new ArrayList<>();
    private List<UserSkill> skills = new ArrayList<>();
    private List<Project> projects = new ArrayList<>();
    private List<Review> reviews = new ArrayList<>();

    public UserDTO(){
    }

    public UserDTO(User user){
        this.id = user.getId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.firstName = user.getFirstName();
        this.lastName = user.getLastName();
        this.age = user.getAge();
        this.sex = user.getSex();
        this.phone = user.getPhone();
        this.skype = user.getSkype();
        this.primaryEmail = user.getPrimaryEmail();
        this.secondaryEmail = user.getSecondaryEmail();
        this.city = user.getCity();
        this.country = user.getCountry();
        this.bio = user.getBio();
        this.pictureUrl = user.getPictureUrl();
        this.config = user.getConfig();
        this.roles = user.getRoles();
        this.securityRoles = user.getSecurityRoles();
        this.skills = user.getSkills();
        this.projects = user.getProjects();
        this.reviews = user.getReviews();
    }

    public UserDTO(Long id, String firstName, String lastName, String city, String country, String bio, String pictureUrl){
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.city = city;
        this.country = country;
        this.bio = bio;
        this.pictureUrl = pictureUrl;
    }
    /*Getters & setters*/
}

Application.java:

import hu.pte.clms.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner{

    @Autowired
    private UserService userService;

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.setShowBanner(false);
        app.setRegisterShutdownHook(true);  
    }

    @Override
    public void run(String... strings) throws Exception{
        userService.listAll();
    }
}

pom.xml:

    ...

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>4.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
    </dependencies>

Application.yml:

spring.datasource:
  url: jdbc:mysql://localhost:3306/clms?autoReconnect=true
  username: clms
  password: clms
  testOnBorrow: true
  validationQuery: SELECT 1
  driverClassName: com.mysql.jdbc.Driver
2
Could you please try to narrow down your problem, in order to not post your entire application code?aribeiro
I shared it because maybe I went wrong in something and someone notices. I wrote my problem at the beginning, it is not required to go through the code.zse014
As @aribeiro said, better try to provide a simplified example of what's going on, so people can test it. I don't see all the code useful to solve the case. Have a look at this.Xtreme Biker
Okey guys, sorry about that. Next time I will use your suggestions.zse014
What exactly is being eagerly fetched which shouldn't be?Alan Hay

2 Answers

0
votes

**Sorry I cannot really write a comment so I am writing here.

The only thing I could see here is the @Transactional annotation in service class and default propagation is required.

One another thing, in Jackson @JsonIgnoreProperties and @JsonIgnore does not work along. Better put the properties to ignore in the @JsonIgnoreProperties(value = {"projects", "reviews"})

I hope this will help.

0
votes

You should perform mapping in DB, not in your code. i.e.

SELECT NEW package.UserDTO(user.id, user.firstName, user.lastName, user.city, user.country, user.bio, user.pictureUrl) FROM User user

It's guaranteed that only fields from constructor will be fetched when you're using JPQL query like this.