Как сформировать в jpa specification join запрос, если существует промежуточная таблица?

Есть 2 энтити:

@Entity
@Table
public class Complex {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(fetch = FetchType.EAGER)
    private List<UserTable> users;
}
@Entity
@Table
public class UserTable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "users", fetch = FetchType.EAGER)
    private List<Complex> complexes;
}

Для этой ситуации jpa создает промежуточную таблицу complex_users.

И вот вопрос, как правильно составить спецификацию, чтобы получить пользователей по энтити Complex?

Я пробую такой вариант:

public class UsersSpec {
    public static Specification<User> find(UsersFilter usersFilter) {
        return (root, query, cb) -> {
            final Collection<Predicate> predicates = new ArrayList<>();

            if(usersFilter.getComplex() != null) {
                predicates.add(root.join("complexes", JoinType.LEFT).in(Collections.singletonList(usersFilter.getComplex())));
            }
            else {
                predicates.add(root.join("complexes", JoinType.LEFT).isNull());
            }

            return cb.and(predicates.toArray(new Predicate[predicates.size()]));
        };
    }
}

Но jpa генерирует совсем не тот запрос, как будто он забывает о третьей таблице:

select ... from
    user_table user0_ 
    left outer join
    complex complexes1_ 
        on user0_.id=complexes1_.users_id 
    where
    (
        complexes1_.id in (55)
    )

Хотя должно быть так:

select ... from
    user_table user0_ 
    left join
    complex_users complexes1_ 
        on user0_.id=complexes1_.users_id 
    where
    (
        complexes1_.complex_id in (55)
    ) 

Как правильно составить join?


Ответы (2 шт):

Автор решения: alex valuiskyi

При использовании спецификаций подразумевается, что запрос динамический и набор спецификаций может быть различным. В таком случае, лучше делать не одну спецификацию на все условия сразу, а по спецификации на каждое условие, которые не зависят друг от друга. Join в данном случае не очень подходит, тут лучше использовать Subquery.

public class UserSpecifications {
    public static Specification<User> containsComplex(Complex complex) {
            return (root, query, builder) -> {
                    Subquery<Complex> subquery = query.subquery(Complex.class);
                    Root<Complex> subqueryRoot = subquery.from(Complex.class);
                    Predicate predicateToJoin = builder.isMember(root,
                                                    subqueryRoot.get("users"));
                    if(complex != null) {
                       Predicate predicateByComplex = builder.equal(subqueryRoot, complex);
    
                       subquery.select(operation).where(predicateToJoin, predicateByComplex);
                       return builder.exists(subquery);
                    }

                    subquery.select(operation).where(predicateToJoin);              
                    return builder.exists(subquery).not();
            };
     }
}

Использование в проекте:

Specification spec = UserSpecifications.containsComplex(usersFilter.getComplex())
     .and(UserSpecifications.secondSpecification(usersFilter.secondParameter()))
     .and(UserSpecifications.thirdSpecification(usersFilter.thirdParameter()));

repository.findAll(spec);

Все спецификации должны возвращать предикат, а не null. Если спецификацию нужно проигнорировать исходя из значения параметра, то для этого есть builder.conjunction(), который вернет условия вида 1=1. Есть также builder.disjunction(), который вернет условие 1=0. Используют их следующим образом:

public static Specification<User> containsComplex(Complex complex) {
            return (root, query, builder) -> {
                    if(complex != null) {
                       return builder.conjunction(); //всегда истина, т.е. игнорируется
                    } else {
                       return builder.disjunction(); //всегда ложь
                    }
                    
            };
}
→ Ссылка
Автор решения: denis Krivorutchko

Судя по вашим сущноснять вам нужна, @ManyToMany (неважно двунаправленая или одно решайте сами)

@Entity
@Table
public class Complex {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany(fetch = FetchType.Lazy)
    private List<UserTable> users;
}


@Entity
@Table
public class UserTable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany(mappedBy = "users", fetch = FetchType.Lazy)
    private List<Complex> complexes;
}

и запрос с джоином, плюс к кому же поменять fetch = FetchType.EAGER на fetch = FetchType.Lazy что бы избежать N+1

@Query(value = "select u from User u left join fetch u.complexes c where c = :complex")
    User findByComplex (@Param(value = "complex") Complex complex);

или же вы можете сделать так как вы хотели через промежуточную таблицу если у вас будет не List сылка, а просто сущность, иначе отношение @ManyToMany

→ Ссылка