Modelé una relación de base de datos en JPA así:
@Entity @Data @NoArgsConstructor(access = AccessLevel.PROTECTED) // Required for JPA @AllArgsConstructor public class OuterEntity { @Id private String outerId; @JoinColumn(nullable = true) @ManyToOne(fetch = FetchType.LAZY, optional = true) private InnerEntity inner; } @Entity @Data @NoArgsConstructor(access = AccessLevel.PROTECTED) // Required for JPA @AllArgsConstructor public class InnerEntity { @Id private String innerId; @Column private boolean deleted; }Y persistió algunos datos de prueba de la siguiente manera:
InnerEntity inner = new InnerEntity("inner", false); OuterEntity outerWithInner = new OuterEntity("outerWithInner", inner); OuterEntity outerWithoutInner = new OuterEntity("outerWithoutInner", null); em.persist(inner); em.persist(outerWithInner); em.persist(outerWithoutInner);Realizar una consulta muy simple utilizando la API de criterios con éxito da dos resultados:
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<OuterEntity> query = cb.createQuery(OuterEntity.class); query.from(OuterEntity.class); System.out.println(em.createQuery(query).getResultList()); // [OuterEntity(outerId=outerWithInner, inner=InnerEntity(innerId=inner, deleted=false)), OuterEntity(outerId=outerWithoutInner, inner=null)] Pero una vez que agrego un filtro en la entidad inner como esta:
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<OuterEntity> query = cb.createQuery(OuterEntity.class); Root<OuterEntity> root = query.from(OuterEntity.class); query.where(cb.or(cb.isNull(root.get("inner")), cb.isFalse(root.get("inner").get("deleted")))); hibernate genera una cross join con where outerentit0_.innerId_innerId=innerentit1_.innerId en el SQL resultante para acceder a la tabla de la entidad inner :
select outerentit0_.outerId as outerid1_1_, outerentit0_.inner_innerId as inner_in2_1_ from OuterEntity outerentit0_ cross join InnerEntity innerentit1_ where outerentit0_.inner_innerId=innerentit1_.innerId and ( outerentit0_.inner_innerId is null or innerentit1_.deleted=0 )Esto ahora solo devuelve una entidad:
[OuterEntity(outerId=outerWithInner, inner=InnerEntity(innerId=inner, deleted=false))]Que no es el resultado que deseo. Esperaba que todavía hubiera dos resultados.
Creo que esto sucede porque el filtro outerentit0_.inner_innerId=innerentit1_.innerId elimina la entidad externa que tiene null para su inner , porque usar = en nulos siempre se evalúa como falso. Tenga en cuenta que configuré explícitamente la columna de combinación en anulable y la relación de muchos a uno en opcional en las anotaciones JPA, por lo que esperaba que JPA manejara correctamente el caso nulo.
Si agrego root.join("inner", JoinType.LEFT); o root.join("inner", JoinType.LEFT); , obtengo el resultado correcto porque la consulta generada usa una left outer join :
select outerentit0_.outerId as outerid1_1_, outerentit0_.inner_innerId as inner_in2_1_ from OuterEntity outerentit0_ left outer join InnerEntity innerentit1_ on outerentit0_.inner_innerId=innerentit1_.innerId where outerentit0_.inner_innerId is null or innerentit1_.deleted=0 CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<OuterEntity> query = cb.createQuery(OuterEntity.class); Root<OuterEntity> root = query.from(OuterEntity.class); root.join("inner", JoinType.LEFT); cb.isFalse(root.get("inner").get("deleted")))); List<OuterEntity> result = em.createQuery(query).getResultList(); System.out.println(result); // [OuterEntity(outerId=outerWithInner, inner=InnerEntity(innerId=inner, deleted=false)), OuterEntity(outerId=outerWithoutInner, inner=null)] Supongo que es una solución viable, pero estoy muy confundido por qué hibernate emitiría una cross join , lo cual es sutilmente incorrecto para mis expectativas, el peor tipo de error. ¿Qué hice para causar este comportamiento? ¿Estoy obligado a realizar siempre explícitamente una combinación externa izquierda cuando se trata de relaciones anulables como esta?