Fetch join to attribute from another object using Spring Specification

Viewed 1030

I have the following entities:

@Entity
@Table(name = "user_data")
public class UserData {
    ...
    @ManyToOne
    private User user;
    ...
}

@Entity
@Table(name = "user_cars")
public class UserCar {
    ...
    private Integer userId;
    ...
}

@Entity
@Table(name = "users")
public class User {
    ...
    @OneToMany(mappedBy = "userId", cascade = CascadeType.ALL)
    private List<UserCar> userCars;
    ...
}

As you can see, userCars are loaded lazily (and I am not going to change it). And now I use Specifications in order to fetch UserData:

public Page<UserData> getUserData(final SpecificationParameters parameters) {
    return userDataRepository.findAll(createSpecification(parameters), parameters.pageable);
}

private Specification<UserData> createSpecification(final SpecificationParameters parameters) {
    final var clazz = UserData.class;
    return Specification.where(buildUser(parameters.getUserId()));
}

private Specification<UserData> buildUser(final Integer userId) {
    return (root, criteriaQuery, criteriaBuilder) -> {
        if (Objects.nonNull(userId)) {
            final Join<UserData, User> joinParent = root.join("user");
            return criteriaBuilder.equal(joinParent.get("id"), userId);
        } else {
            return criteriaBuilder.isTrue(criteriaBuilder.literal(true));
        }
    };
}

But I have no idea how to add there a fetch join clause in order to fetch user cars. I tried to add it in different place and I got either LazyInitializationException (so it didn't work) or some other exceptions...

2 Answers

In addition of the suggestion @crizzis provided in the question comments, please, try to join fetch the user relationship as well; in the error you reported:

org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list

Hibernate is complaining because it it unable to find the owner, user in this case, of the userCars relationship.

It is strange in a certain way because the @ManyToOne relationship will fetch eagerly the user entity and it will be projected as well while obtaining userData but probably Hibernate is performing the query analysis prior to the actual fetch phase. It would be great if somebody could provide some additional insight about this point.

Having said that, please, consider to set the fetch strategy explicitly to FetchType.LAZY in your @ManyToOne relationship:

@Entity
@Table(name = "user_data")
public class UserData {
    ...
    @ManyToOne(fetch= FetchType.LAZY)
    private User user;
    ...
}

Your Specification code can look like the following:

public Page<UserData> getUserData(final SpecificationParameters parameters) {
    return userDataRepository.findAll(createSpecification(parameters), parameters.pageable);
}

private Specification<UserData> createSpecification(final SpecificationParameters parameters) {
    final var clazz = UserData.class;
    return Specification.where(buildUser(parameters.getUserId()));
}

private Specification<UserData> buildUser(final Integer userId) {
    return (root, criteriaQuery, criteriaBuilder) -> {
        // Fetch user and associated userCars
        final Join<UserData, User> joinParent = (Join<UserData, User>)root.fetch("user");
        joinParent.fetch("userCars");

        // Apply filter, when provided
        if (Objects.nonNull(userId)) {
            return criteriaBuilder.equal(joinParent.get("id"), userId);
        } else {
            return criteriaBuilder.isTrue(criteriaBuilder.literal(true));
        }
    };
}

I did not pay attention to the entity relations themself previously, but Atmas give you a good advice indeed, it will be the more performant way to handle the data in that relationship.

At least, it would be appropriate to define the relationship between User and UserCars using a @JoinColumn annotation instead of mapping through a non entity field in order to prevent errors or an incorrect behavior of your entities. Consider for instance:

@Entity
@Table(name = "users")
public class User {
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "user_id")
    private List<UserCar> userCars;
    ...
}

Slightly different approach from the prior answer, but I think the idea jcc mentioned is on point, i.e. "Hibernate is complaining because it it unable to find the owner, user in this case, of the userCars relationship."

To that end, I'm wondering if the Object-Relational engine is getting confused because you have linked directly to a userId (a primitive) instead of a User (the entity). I'm not sure if it can assume that "userId" the primitive necessarily implies a connection to the User entity.

Can you try to re-arrange the mapping so that it's not using an integer UserId in the join table and instead using the object itself, and then see if it allows the entity manager to understand your query better?

So the mapping might look something like this:

@Entity
@Table(name = "user_cars")
public class UserCar {
    ...

    @ManyToOne
    @JoinColumn(name="user_id", nullable=false) // Assuming it's called user_id in this table
    private User user;
    ...
}

@Entity
@Table(name = "users")
public class User {
    ...
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<UserCar> userCars;
    ...
}

It would also be more in line with https://www.baeldung.com/hibernate-one-to-many

Related