简体   繁体   中英

JPA: N+1 query even after FetchType.LAZY

I've read a few articles that shows how to solve n+1 query problem in JPA, but none of it worked for me.

When I try to fetch the data, JPA makes n+1 queries.

select owner0_.id as id1_1_, owner0_.created_at as created_2_1_, owner0_.updated_at as updated_3_1_, owner0_.name as name4_1_, owner0_.version as version5_1_ from owner owner0_

select cars0_.owner_id as owner_id6_0_0_, cars0_.id as id1_0_0_, cars0_.id as id1_0_1_, cars0_.created_at as created_2_0_1_, cars0_.updated_at as updated_3_0_1_, cars0_.license_no as license_4_0_1_, cars0_.owner_id as owner_id6_0_1_, cars0_.version as version5_0_1_ from car cars0_ where cars0_.owner_id=? [1]

select cars0_.owner_id as owner_id6_0_0_, cars0_.id as id1_0_0_, cars0_.id as id1_0_1_, cars0_.created_at as created_2_0_1_, cars0_.updated_at as updated_3_0_1_, cars0_.license_no as license_4_0_1_, cars0_.owner_id as owner_id6_0_1_, cars0_.version as version5_0_1_ from car cars0_ where cars0_.owner_id=? [2]

Below is the code snippet:

@Entity
public class Owner extends BaseEntity implements EntityTransformer<OwnerDto> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Version
    private Long version;

    @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
    private Set<Car> cars;

    @Override
    public OwnerDto convertToDto() {
        OwnerDto ownerDto = new OwnerDto();
        ownerDto.setId(this.getId());
        ownerDto.setName(this.getName());
        ownerDto.setVersion(this.getVersion());
        if (this.getCars() != null) ownerDto.setCars(this.getCars().stream().map(Car::convertToDto).collect(Collectors.toSet()));
        return ownerDto;
    }
}

and my Car class is as follow:

@Entity
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String licenseNo;

    @Version
    private Integer version;

    @JoinColumn( name = "owner_id" )
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Owner owner;
    @Override
    public CarDto convertToDto() {
        CarDto carDto = new CarDto();
        carDto.setId(this.getId());
        carDto.setLicenseNo(this.getLicenseNo());
        carDto.setVersion(this.getVersion());
        return carDto;
    }
}

OwnerService:

@Service
public class OwnerServiceImpl implements OwnerService {

    @Autowired
    OwnerRepository ownerRepository;

    @Override
    public List<Owner> findAll() {
        return ownerRepository.findAll();
    }
}

OwnerController:

@RestController
public class OwnerController {
    @Autowired
    private OwnerService ownerService;

    @GetMapping(value = "/owners", produces = "application/vnd.demo.api.v1+json")
    public ResponseEntity<List<OwnerDto>> findAll() {
        return ResponseEntity.ok(ownerService.findAll().stream().map(Owner::convertToDto).collect(Collectors.toList()));
    }
}

cURL:

curl -X POST \
  http://localhost:8080/owner \
  -H 'Accept: application/vnd.demo.api.v1+json' \
  -H 'Content-Type: application/json' \
  -H 'Host: localhost:8080' \
  -d '{
    "name": "pranay5"
}'

curl -X POST \
  http://localhost:8080/owner/5/car \
  -H 'Accept: application/vnd.demo.api.v1+json' \
  -H 'Content-Type: application/json' \
  -H 'Host: localhost:8080' \
  -d '{
    "licenseNo": "MSH-5555"
}'

Is there something wrong with the code?

On a side note: @BatchSize(size = 5) JPA makes only two queries When I set @BatchSize(size = 5) without making any other changes, it makes only two queries to the database.

select o_.id , o_.created_at, o_.updated_at, o_.name from owner o_ 

select c_.owner_id, c_.id, c_.created_at, c_.updated_at, c_.license_no, c_.owner_id, from car c_ where c_.owner_id in (?, ?, ?, ?, ?) [1,2,3,4,5] 

But my doubt is why FetchType.LAZY making N+1 queries?

Code: https://github.com/pranayhere/exception-demo-mvn

Actually the problem is that you use the default findAll of the OwnerRepository and you put FetchType.LAZY .

Because you get the cars in the Owner::convertToDto , Hibernate has to fetch the cars it did not fetched because of the lazy fetching.

To avoid the additional queries, create a new JPA method in the OwnerRepository getAllBy with an EntityGraph to eagerly fetch the cars in the query:

public class OwnerRepository extend JpaRepository<Owner, Long> {

    @EntityGraph(attributePaths = {
            "cars",
    })
    List<Owners> getAllBy();

}

Then use it in your service instead of the findAll.

I just checked your code and I think the problem is in OwnerController :

@GetMapping(value = "/owners", produces = "application/vnd.demo.api.v1+json")
public ResponseEntity<List<OwnerDto>> findAll() {
    return ResponseEntity.ok(ownerService.findAll().stream().map(Owner::convertToDto).collect(Collectors.toList()));
}

The Owner.convertToDto() method iterates over Owner.cars field.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM