1

I'm trying to create a @NamedNativeQuery with a @ConstructorResult for a class that has a field with a Set of enum values.

VeterinarianJPA.java:

@Entity
@Table(name = "veterinarians")
@Setter
@Getter
@NoArgsConstructor
@NamedNativeQueries({
        @NamedNativeQuery(
                name = VeterinarianJPA.FIND_ALL_VETS,
                query = "SELECT v.id, v.name, vs.specialisations " +
                        "FROM veterinarians v " +
                        "JOIN veterinarian_specialisations vs ON v.id = vs.vet_id",
                resultSetMapping = VeterinarianJPA.VETERINARIAN_RESULT_MAPPER
        )})
@SqlResultSetMappings({
        @SqlResultSetMapping(
                name = VeterinarianJPA.VETERINARIAN_RESULT_MAPPER,
                classes = @ConstructorResult(
                        targetClass = Veterinarian.class,
                        columns = {
                                @ColumnResult(name = "id", type = Long.class),
                                @ColumnResult(name = "name"),
                                @ColumnResult(name = "specialisations", type = Set.class)
                        }
                )
        )})
class VeterinarianJPA {

    static final String FIND_ALL_VETS = "net.kemitix.naolo.gateway.data.jpa.findAllVets";
    static final String VETERINARIAN_RESULT_MAPPER = "net.kemitix.naolo.gateway.data.jpa.Veterinarian";

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ElementCollection
    @Enumerated(EnumType.STRING)
    @CollectionTable(
            name = "veterinarian_specialisations",
            joinColumns = @JoinColumn(name = "vet_id")
    )
    private final Set<VetSpecialisation> specialisations = new HashSet<>();
}

Veterinarian.java:

public final class Veterinarian {

    private Long id;
    private String name;
    private Set<VetSpecialisation> specialisations;

    public Veterinarian() {
    }

    public Veterinarian(final long id,
                        final String name,
                        final Set<VetSpecialisation> specialisations) {
        this.id = id;
        this.name = name;
        this.specialisations = new HashSet<>(specialisations);
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Set<VetSpecialisation> getSpecialisations() {
        return new HashSet<>(specialisations);
    }

}

VetSpecialisation.java:

public enum VetSpecialisation {

    RADIOLOGY,
    DENTISTRY,
    SURGERY

}

When I attempt to execute the named query:

entityManager.createNamedQuery(VeterinarianJPA.FIND_ALL_VETS, Veterinarian.class)
             .getResultStream()

I get the following exception:

java.lang.IllegalArgumentException: Could not locate appropriate constructor on class : net.kemitix.naolo.entities.Veterinarian

    at org.hibernate.loader.custom.ConstructorResultColumnProcessor.resolveConstructor(ConstructorResultColumnProcessor.java:92)
    at org.hibernate.loader.custom.ConstructorResultColumnProcessor.performDiscovery(ConstructorResultColumnProcessor.java:45)
    at org.hibernate.loader.custom.CustomLoader.autoDiscoverTypes(CustomLoader.java:494)
    at org.hibernate.loader.Loader.processResultSet(Loader.java:2213)
    at org.hibernate.loader.Loader.getResultSet(Loader.java:2169)
    at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1930)
    at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1892)
    at org.hibernate.loader.Loader.scroll(Loader.java:2765)
    at org.hibernate.loader.custom.CustomLoader.scroll(CustomLoader.java:383)
    at org.hibernate.internal.SessionImpl.scrollCustomQuery(SessionImpl.java:2198)
    at org.hibernate.internal.AbstractSharedSessionContract.scroll(AbstractSharedSessionContract.java:1058)
    at org.hibernate.query.internal.NativeQueryImpl.doScroll(NativeQueryImpl.java:217)
    at org.hibernate.query.internal.AbstractProducedQuery.scroll(AbstractProducedQuery.java:1462)
    at org.hibernate.query.internal.AbstractProducedQuery.stream(AbstractProducedQuery.java:1486)
    at org.hibernate.query.Query.getResultStream(Query.java:1110)

I expect that the SQL is returning multiple rows for a multi-valued Set rather than a single value, which is causing the constructor not to match. How do I change the SQL to produce the correct input to the constructor, or is there another configuration change I need to make?

1 Answers1

0

Well, I'm not sure if that's even possible in the way you want to to this. But you can use LISTAGG function on specialisations table to inline the specialisations with veterinarians by using some kind of separator.

So the query should look like this:

SELECT v.id, v.name 
(SELECT LISTAGG(vs.type, ';') 
  WITHIN GROUP (ORDER BY vs.type) 
  FROM veterinarian_specialisations vs 
  WHERE vs.vet_id = v.id) specialisations
FROM veterinarians v;

The query will return veterinarian and his semicolon separated specialisations:

1   NAME   DENTISTRY;RADIOLOGY

And then in your Veterinarian class constructor you must remap String result back to Set of VetSpecialisation. I used Java 8 stream api just for convenience.

public final class Veterinarian {

private Long id;
private String name;
private Set<VetSpecialisation> specialisations;

public Veterinarian() {
}

public Veterinarian(final long id,
                    final String name,
                    final String specialisations) {
    this.id = id;
    this.name = name;
    this.specialisations = Arrays.asList(specialisations.split(";"))
        .stream()
        .map(VetSpecialisation::valueOf) //Map string to VetSpecialisation enum.
        .collect(Collectors.toSet());
}
Michał Stochmal
  • 3,268
  • 1
  • 22
  • 34
  • Thanks. I'm using the H2 database which doesn't support the `LISTAGG()` function. I was able to use the equivalent: `GROUP_CONCAT(vs.specialisations SEPARATOR ';') specialisations`. I was hoping that JPA would have had better support for `Set`s. – Paul Campbell Aug 09 '18 at 10:43
  • Having a set in a database field of varchar is an anti-pattern. That's why it's not supported by JPA. – Simon Martinelli Aug 09 '18 at 10:49
  • @SimonMartinelli Do you have a link for this? I've not used JPA enough to see much discussion on any anti-patterns. I'd like to see the reasoning and any alternatives that were suggested. Thanks. – Paul Campbell Aug 09 '18 at 10:54
  • It's not JPA related. But SQL. Read this book: https://pragprog.com/book/bksqla/sql-antipatterns – Simon Martinelli Aug 09 '18 at 11:34