spring-projects / spring-projects/spring-data-jpa
Unnecessary JOIN for @JoinColumn(referencedColumnName) with where condition by FK
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 3.3k
- Forks
- 1.6k
- PR merge metrics
- No merged PRs in 30d
Description
Derived query method suddenly generates a JOIN instead of filtering on the FK column directly
Repro environment
- Spring Data JPA 3.2.1 – 4.x (observed on 3.2.1 and 4.1.0); regression introduced in 3.2.1
(Spring Boot 3.2.1), NOT in 4.0.x. - Spring Boot 3.1.x (Spring Data JPA 3.1.x) and Spring Data JPA 3.2.0 do NOT show this.
- H2 used for the repro; the behaviour is provider-independent (generated by Spring Data itself).
Summary
When a repository method name derives a query that navigates an association whose
@JoinColumn uses a non-default referencedColumnName, Spring Data JPA >= 3.2.1 generates
a SQL JOIN to evaluate the predicate. Versions up to 3.2.0 generated a plain
WHERE <fk_column> = ? with no JOIN.
The FK column physically stores a copy of the referenced column's value, so the JOIN
is redundant and can hurt performance on hot paths.
Minimal reproducer
Entities
@Entity
@Table(name = "abonents")
public class Abonent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "code", unique = true)
String code;
}
@Entity
@Table(name = "contracts")
public class Contract {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "abonent", referencedColumnName = "code")
Abonent abonent;
}
Repository
public interface ContractRepository extends JpaRepository<Contract, Long> {
List<Contract> findByAbonentCode(String abonentCode);
}
The derived path is abonent.code. In Spring Data JPA <= 3.2.0 the comparison is made
against the FK column contracts.abonent directly. From 3.2.1 the derived-query engine
emits an explicit JOIN (verified on 3.2.1 / 3.2.5 / 4.x).
Generated SQL, Spring Data JPA 3.1.2 (old behaviour)
select c1_0.id, c1_0.abonent, c1_0.number
from contracts c1_0
where c1_0.abonent = ? -- no JOIN
Generated SQL, Spring Data JPA 3.2.1+ (current behaviour; same in 4.1.0)
select c1_0.id, c1_0.abonent, c1_0.number
from contracts c1_0
left join abonents a1_0 on a1_0.code = c1_0.abonent
where a1_0.code = ?
Why it is a regression
- The regression point is Spring Data JPA 3.2.1 (Spring Boot 3.2.1): 3.1.x and 3.2.0 are
join-free. In the same 3.2.1 an explicit@Query("... where c.abonent.code = :code")
is rendered join-free by Hibernate, confirming the JOIN is introduced by the derived-query
(query-by-name) engine, not by Hibernate (nor by the Hibernate 6.3 -> 6.4 upgrade that
landed in the same release). contracts.abonentphysically stores thecodevalue (it is declared via
referencedColumnName = "code"). Thuswhere c1_0.abonent = ?and
where a1_0.code = ?are semantically identical, but the latter forces a join.- We have many such lookups on hot endpoints; the extra join on every request is
measurable overhead and not removable without rewriting queries. - No configuration flag exists in 3.2.x/4.x to restore the old behaviour (we searched
JpqlQueryBuilder,JpaQueryCreator,PartTreeJpaQueryand the properties docs).
Requested behaviour
Restore the previous join-free behaviour, made configurable so that the cases that
deliberately rely on explicit joins (composite keys, #3349/#3588) are preserved by default.
A PR implementing this as a default-off option exists:
- Property
${spring.data.jpa.derived-query.prefer-fk-column}(viaEntityManagerFactoryproperties),
truerenders the implicit dotted path (no explicit JOIN), letting the persistence provider collapse
the predicate onto the foreign-key column — restoring the pre-3.2.1 SQL (WHERE contracts.abonent = ?). - Default is
false(explicit joins kept), preserving #3349/#3588.
Independent check (reference)
Cross-checked on the same model:
- Spring Data JPA 3.1.2 ->
where contracts.abonent = ? - Spring Data JPA 3.2.0 ->
where contracts.abonent = ?(still join-free) - Spring Data JPA 3.2.1 -> explicit
left joinin the derived-query JPQL (regression point) - Spring Data JPA 4.1.0 -> explicit
left joinin the derived-query JPQL - Spring Data JPA 4.2.0-SNAPSHOT +
prefer-fk-column=true->where contracts.abonent = ?
Both statements were verified against a real H2 database. An explicit @Query("... where c.abonent.code = :code")
is rendered join-free by Hibernate in all versions, confirming the join is introduced by the
derived-query (query-by-name) engine, not by Hibernate.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with JpqlQueryBuilder, JpaQueryCreator, and PartTreeJpaQuery, which the issue identifies as the derived-query engine involved in the regression. Reproduce the entity and repository example, compare generated SQL across the cited versions, and verify that the requested configurable behavior preserves the default handling for #3349/#3588.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- backend, database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100