Blazebit / blaze-persistence

Rich Criteria API for JPA providers
https://persistence.blazebit.com
Apache License 2.0
741 stars 90 forks source link

Entity view inheritance and FetchType.SUBSELECT with @EmbeddedId throws UnsuportedOperationException #1945

Open retroandchill opened 4 weeks ago

retroandchill commented 4 weeks ago

Description

I'm currently trying to translate a fairly deep inheritance hierarchy of entities to use Blaze persistence and I'm running into some issues with the query of the view when using Hibernate.

My structure looks something like this

@Entity
@Getter
@Setter
public class BaseEntity {
    @EmbeddedId
    private CompositeId compositeId;

    ...
}

@Entity
@Getter
@Setter
public class DerivedEntity extends BaseEntity{
    @OneToMany
    private Set<OtherEntity> entityType;
}

I then try to create views with a similar structure and the issue I seem to be running into is that the collection is mapped using FetchType.SUBSELECT which then causes issues when the query is generated. It seems to stem from the usage of the TREAT keyword which seems to expand to create a case statement, which returns the compositeId of the entity.

When it tries to execute the query, it ends up throwing an UnsupportedOperationException with the message of Resolution of embedded-valued SqmExpressible nodes not yet implemented causing the query to fail to run. It seems that it is unable to resolve the mapping inside of that case statement because it is both an Embeddable and the ID of the source entity.

Expected behavior

The query goes off and the entities are subselected

Actual behavior

An exception is thrown and the query method fails

Steps to reproduce

  1. Create an entity hierarchy using Hibernate that follows the above schema.
  2. Create an identical entity view hierarchy with the collections mapped using FetchType.SUBSELECT
  3. Perform an entity view query using the criteria builder
  4. Observe the exception

Environment

Version: 1.6.12
JPA-Provider: Hibernate
DBMS: Postgres/HSQL
Application Server: Spring Boot

beikov commented 3 weeks ago

Hi, the error comes from Hibernate ORM. Can you please share the full stack trace and also the HQL query that is generated which fails?

retroandchill commented 3 weeks ago

I can provide a similar version that strips out any information pertinent to the project, but I can at least provide the general idea as to what is happening.

This is the full stack trace from where getResultList() is called in our code.

java.lang.UnsupportedOperationException: Resolution of embedded-valued SqmExpressible nodes not yet implemented

    at org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl.resolveMappingExpressible(MappingMetamodelImpl.java:801)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.determineCurrentExpressible(BaseSqmToSqlAstConverter.java:7017)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitSearchedCaseExpression(BaseSqmToSqlAstConverter.java:6983)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitSearchedCaseExpression(BaseSqmToSqlAstConverter.java:445)
    at org.hibernate.query.sqm.tree.expression.SqmCaseSearched.accept(SqmCaseSearched.java:118)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitSelection(BaseSqmToSqlAstConverter.java:2264)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitSelectClause(BaseSqmToSqlAstConverter.java:2206)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitQuerySpec(BaseSqmToSqlAstConverter.java:2077)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitQuerySpec(BaseSqmToSqlAstConverter.java:445)
    at org.hibernate.query.sqm.tree.select.SqmQuerySpec.accept(SqmQuerySpec.java:124)
    at org.hibernate.query.sqm.spi.BaseSemanticQueryWalker.visitQueryPart(BaseSemanticQueryWalker.java:244)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitQueryPart(BaseSqmToSqlAstConverter.java:1935)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitSelectStatement(BaseSqmToSqlAstConverter.java:1620)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.visitSelectStatement(BaseSqmToSqlAstConverter.java:445)
    at org.hibernate.query.sqm.tree.select.SqmSelectStatement.accept(SqmSelectStatement.java:234)
    at org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter.translate(BaseSqmToSqlAstConverter.java:785)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.buildCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:422)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:328)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:302)
    at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:526)
    at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:423)
    at org.hibernate.query.Query.getResultList(Query.java:120)
    at com.blazebit.persistence.impl.query.TypedQueryWrapper.getResultList(TypedQueryWrapper.java:49)
    at com.blazebit.persistence.impl.query.ObjectBuilderTypedQuery.getResultList(ObjectBuilderTypedQuery.java:63)
    at com.blazebit.persistence.impl.AbstractQueryBuilder.getResultList(AbstractQueryBuilder.java:58)

As for the HQL query, this is the rough outline of what's going on in the query based on the minimal example provided above:

SELECT CASE
           WHEN TYPE(entity) = DerivedEntity THEN 1
           WHEN TYPE(entity) = OtherDerived THEN 2
           ELSE 0 END                                                                                                 AS BaseEntityView_class,
       entity.compositeId                                                                                               AS BaseEntityView_compositeId,                                                                                                AS NodeView_template_rows,
       CASE
           WHEN TYPE(entity) = DerivedEntity
               THEN entity.compositeId END                                                                              AS BaseEntityView_otherEntities,
       FROM BaseEntity entity
WHERE entity.compositeId = :param_0;
beikov commented 3 weeks ago

Ah, I think I see where this is going. Do you use the CompositeId type in the entity view? Try creating an entity view for that embeddable too, then this will work. This is an unfortunate limitation, but I wouldn't recommend using JPA types in entity views anyway, so it's IMO not that big of a deal.

retroandchill commented 3 weeks ago

@beikov I just tried that but I got the same result in the same location. It seems like the issue is coming from the generated HQL and since it sees the field being queries as the ID it ends up failing still.

It looks as if the HQL code has not changed at all from that change and is still using the composite field.

beikov commented 3 weeks ago

Please share the entity views you're using.

retroandchill commented 3 weeks ago

Gladly: This is the view for the base class:

@EntityView(BaseEntity.class)
@EntityViewInheritance
public interface BaseEntityView {
    @IdMapping
    CompositeIdView getCompositeId();

    int getDataField();
}

Here is the ID

@EntityView(CompositeId.class)
public interface CompositeIdView {
    UUID getId();

    int getRevisionId();
}

This is the one for the derived view:

@EntityView(DerivedEntity.class)
public interface DerivedEntityView extends BaseEntityView {
    @Mapping(fetch = FetchStrategy.SUBSELECT)
    Set<OtherEntityView> getOtherEntities();
}

And this is the one for the related entity:

@EntityView(OtherEntity.class)
public interface OtherEntityView {
    @IdMapping
    UUID getId();

    int getMyDataField();
}

When run this is the query that gets generated:

SELECT CASE WHEN TYPE(baseEntity) = DerivedEntity THEN 1 ELSE 0 END                  AS BaseEntityView_class,
       baseEntity.compositeId.id                                                     AS BaseEntityView_compositeId_id,
       baseEntity.compositeId.revisionId                                             AS BaseEntityView_compositeId_revisionId,
       baseEntity.dataField                                                          AS BaseEntityView_dataField,
       CASE WHEN TYPE(baseEntity) IN (DerivedEntity) THEN baseEntity.compositeId END AS BaseEntityView_otherEntities
FROM BaseEntity baseEntity;
beikov commented 3 weeks ago

Thanks, I can imagine where this goes wrong. I hope though, that you can use a different fetch type in the meantime.