mv89
mv89

Reputation: 33

How to use JPA projections with one-to-many attributes in Criteria API / JPQL

I have difficulties creating a query with Criteria API that projects attributes of the queried entity and instantiates a DTO. One of the projected attributes maps a one-to-many relationship with another entity, so it is a set of dependent entities. I am using fetch join to retrieve the set. But I am getting the following error:

org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list

I have already tried using a regular join, but in this case the set of dependent entities won't get populated. Removing the join and / or fetch completely didn't help either.

I am using JPA specification 2.0, Hibernate 4.2.21.Final, Spring Data JPA 1.10.11.RELEASE.

Could anybody advise me on this? I would be happy for a working JPQL also.

This is my implementation of the query:

@Override
public List<EntityADto> findByPartialKey1OrderByPartialKey2(String partialKey1) {
    // Create query
    final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    final CriteriaQuery<EntityADto> criteriaQuery = criteriaBuilder.createQuery(EntityADto.class);

    // Define FROM clause
    final Root<EntityA> root = criteriaQuery.from(EntityA.class);
    root.fetch(EntityA_.oneToManyAttribute);

    // Define DTO projection
    criteriaQuery
            .select(criteriaBuilder.construct(
                    EntityADto.class,
                    root.get(EntityA_.id).get(EntityAId_.partialKey1),
                    root.get(EntityA_.id).get(EntityAId_.partialKey2),
                    root.get(EntityA_.stringAttribute1),
                    root.get(EntityA_.stringAttribute2),
                    root.get(EntityA_.oneToManyAttribute)))
            .orderBy(criteriaBuilder.asc(root.get(EntityA_.id).get(EntityAId_.partialKey2)))
            .distinct(true);

    // Define WHERE clause
    final ParameterExpression<String> parameterPartialKey1 = criteriaBuilder.parameter(String.class);
    criteriaQuery.where(criteriaBuilder.equal(root.get(EntityA_.id).get(EntityAId_.partialKey1), parameterPartialKey1));

    // Execute query
    final TypedQuery<EntityADto> typedQuery = entityManager.createQuery(criteriaQuery);
    typedQuery.setParameter(parameterPartialKey1, partialKey1);

    return typedQuery.getResultList();
}

The entities look as follows:

@Entity
@Table(name = "TABLE_A", uniqueConstraints = 
@UniqueConstraint(columnNames = {
    "PARTIAL_KEY_1", "STRING_ATTR_1", "STRING_ATTR_2" }))
public class EntityA {
    @EmbeddedId
    @AttributeOverrides({
        @AttributeOverride(name = "partialKey1", column = @Column(name = "PARTIAL_KEY_1", nullable = false)),
        @AttributeOverride(name = "partialKey2", column = @Column(name = "PARTIAL_KEY_2", nullable = false))})
    private EntityAId id;
    @Column(name = "STRING_ATTR_1", nullable = false)
    private String stringAttribute1;
    @Column(name = "STRING_ATTR_2", nullable = false)
    private String stringAttribute2;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "entityA")
    private Set<EntityB> entityBs;

    // getters and setters omitted for brevity.
}

@Entity
@Table(name = "TABLE_2")
public class EntityB {
    @EmbeddedId
    @AttributeOverrides({
        @AttributeOverride(name = "partialKey3", column = @Column(name = "PARTIAL_KEY_3", nullable = false)),
        @AttributeOverride(name = "partialKey1", column = @Column(name = "PARTIAL_KEY_1", nullable = false)),
        @AttributeOverride(name = "partialKey2", column = @Column(name = "PARTIAL_KEY_2", nullable = false))})
    private EntityBId id;
    @Column(name = "VALUE")
    private String value;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name = "PARTIAL_KEY_1", referencedColumnName = "PARTIAL_KEY_1", nullable = false, insertable = false, updatable = false),
        @JoinColumn(name = "PARTIAL_KEY_2", referencedColumnName = "PARTIAL_KEY_2", nullable = false, insertable = false, updatable = false)})
    private EntityA entityA;

    // getters and setters omitted for brevity.

}

And finally the DTO:

public class EntityADto implements Serializable {
    private static final long serialVersionUID = -5343329086697620178L;

    private String partialKey1;
    private Integer partialKey2;
    private String stringAttribute1;
    private String stringAttribute2;
    private Map<String, String> additionalAttributes;

    public ProzessdatStandardDto() { }

    public ProzessdatStandardDto(String partialKey1,
                                 Integer partialKey2,
                                 String stringAttribute1,
                                 String stringAttribute2,
                                 Set<EntityB> entityBs) {
        this.partialKey1 = partialKey1;
        this.partialKey2 = partialKey2;
        this.stringAttribute1 = stringAttribute1;
        this.stringAttribute2 = stringAttribute2;

        final Map<String, String> entityBsConverted = new HashMap<>();
        if (!CollectionUtils.isEmpty(entityBs)) {
            for (EntityB entityB : entityBs) {
                entityBsConverted.put(entityB.getPartialKey3(), entityB.getValue());
            }
        }
        this.additionalAttributes = prozessdatExpansionsConverted;
    }

    // getters and setters omitted for brevity.
}

Upvotes: 0

Views: 2524

Answers (1)

K.Nicholas
K.Nicholas

Reputation: 11551

A join gives you a collection of rows result in sql:

Parent    Child
p1        c1
p1        c2
p1        c3

and so on. There is no mechanism for passing the resulting collection into a constructor.

JPA Spec 4.14

constructor_expression ::=
NEW constructor_name ( constructor_item {, constructor_item}* )

constructor_item ::=
single_valued_path_expression |
scalar_expression |
aggregate_expression |
identification_variable

Also, another issue is that your query might return more than one parent or child.

Parent    Child    Child2
p1        c111     c121
p1        c121     
p1        c131     c122
p2        c211     c211
p2        c221     c212
p2        c231     

I'm guessing that the reason is that becomes too complicated for the underlying JPA provider to know where to split this up or which values to use to pass to a child constructor or perhaps more subtle reasons I'm not familiar with. Bottom line it requires that you provide code for parsing this matrix and if you're going to do that you might as well just parse the result without JPA.

Upvotes: 1

Related Questions