Affected versions
Reproduced on 7.1.16.Final and 7.4.1.Final (identical code paths). Any Hibernate ORM 6.2+ where applyToLoadByKey exists is likely affected.
Environment
- JPA/HQL query (SQM path). Also reproducible via Criteria root.fetch(…).
- Any dialect; observed on Oracle (ORA-00904), but the generated SQL is malformed on all dialects.
Description
When a @FilterDef has applyToLoadByKey = true and is active, an HQL/Criteria JOIN FETCH of a @ManyToOne whose target entity uses @Inheritance(InheritanceType.JOINED) generates SQL where the subclass/secondary table joins are missing from the FROM clause, even though those subclass columns are still listed in the SELECT
clause. The database rejects the query because the SELECT references table aliases that are never joined.
entityManager.find(id) and lazy by-id loads of the same entity work correctly (they use the loader path, not the SQM association-fetch path), so this only manifests for query-driven eager association fetches.
Mapping to reproduce
@MappedSuperclass
@FilterDef(
name = "tenantFilter",
applyToLoadByKey = true, // <-- required to trigger
defaultCondition = "tenant_id = :tenantId",
parameters = @ParamDef(name = "tenantId", type = Long.class))
public abstract class TenantAware {
@Column(name = "tenant_id")
Long tenantId;
}
// JOINED inheritance root
@Entity
@Table(name = "payment")
@Inheritance(strategy = InheritanceType.JOINED)
@Filter(name = "tenantFilter")
public abstract class Payment extends TenantAware {
@Id @GeneratedValue Long id;
}
@Entity @Table(name = "card_payment")
@Filter(name = "tenantFilter")
public class CardPayment extends Payment {
@Column(name = "card_number") String cardNumber;
}
@Entity @Table(name = "wire_payment")
@Filter(name = "tenantFilter")
public class WirePayment extends Payment {
@Column(name = "iban") String iban;
}
// Owning entity with a LAZY to-one to the JOINED hierarchy
@Entity @Table(name = "wallet")
@Filter(name = "tenantFilter")
public class Wallet extends TenantAware {
@Id @GeneratedValue Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "payment_id")
Payment payment;
}
Steps to reproduce
session.enableFilter("tenantFilter").setParameter("tenantId", 1L);
// Fails:
session.createQuery(
"SELECT w FROM Wallet w LEFT JOIN FETCH w.payment", Wallet.class)
.getResultList();
// Equivalent Criteria form also fails:
// root.fetch("payment", JoinType.LEFT);
Expected SQL (what you get WITHOUT applyToLoadByKey)
The subclass tables are joined so their columns can be selected:
select w.id, ...,
p.id, cp.card_number, wp.iban
from wallet w
left join payment p
on p.id = w.payment_id
left join card_payment cp on cp.id = p.id -- subclass join present
left join wire_payment wp on wp.id = p.id -- subclass join present
where w.tenant_id = ?
Actual SQL (WITH applyToLoadByKey = true)
The tenant predicate is correctly added to the join, but the subclass table joins are gone while their columns remain selected:
select w.id, ...,
p.id, cp.card_number, wp.iban -- cp., wp. still selected
from wallet w
left join payment p
on p.id = w.payment_id
and p.tenant_id = ? -- filter applied here (correct)
-- MISSING: left join card_payment cp ...
-- MISSING: left join wire_payment wp ...
where w.tenant_id = ?
→ e.g. Oracle: ORA-00904: "CP"."CARD_NUMBER": invalid identifier.
Root-cause analysis
- applyToLoadByKey = true makes AbstractEntityPersister.hasFilterForLoadByKey() return true.
- In ToOneAttributeMapping.createTableGroupJoin(…), the block guarded by hasFilterForLoadByKey() calls applyBaseRestrictions(…), appending the filter predicate to the association’s join ON-clause. The join predicate becomes a compound predicate instead of a bare col = col.
- SimpleForeignKeyDescriptor.isSimpleJoinPredicate(…) now returns false, so in ToOneAttributeMapping.determineTableGroupForFetch(…) the LEFT-join table-group reuse guard (“only reuse if the predicate has no user-defined bits”) rejects the join.
- As a result the fetch’s LazyTableGroup is not initialized/realized in the rendered FROM (LazyTableGroup.getTableReferenceJoins() returns empty), so the JOINED subclass table joins are never emitted — but the entity fetch still selects all subclass columns.
Additional notes
- Works fine when the JOINED entity is the query root (SELECT p FROM Payment p) or loaded by id (em.find(Payment.class, id)) — those go through the loader/root-table-group path, not the association-fetch reuse path.
- Single-table (SINGLE_TABLE) targets are unaffected (no subclass tables to drop).
- Reproducible with both HQL JOIN FETCH and Criteria Root.fetch(…).
- Minimal trigger set: applyToLoadByKey = true + @Inheritance(JOINED) target + eager association fetch in a query.
Thank you for your response.