Search code examples
javahibernatejpaspring-data-jpaspring-data

Interface-based projections, native queries and Enums


After upgrading to Hibernate 6 and Spring Boot 3 the following throws me an error (Cannot project java.lang.Short to com.cubetrek.cubetrekplayground.database.TrackData$Sharing; Target type is not an interface and no matching Converter found).

The @Entity class is as follows:

@Entity(name = "trackdata")
@Table(name = "trackdata")
public class TrackData implements Serializable {

    public enum Sharing {
        PRIVATE, FRIENDS, PUBLIC;
    }

    @Getter
    @Setter
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Getter
    @Setter
    @Column(name = "title")
    private String title;

    @Getter
    @Setter
    @Enumerated(EnumType.ORDINAL)
    @Column(name = "sharing")
    private Sharing sharing;
    
    // loads of other fields and getters and setters...

    //and here's the interface
    
    public interface TrackMetadata {
        Long getId();
        @JsonSerialize(using = TitleSerializer.class, as=String.class)
        String getTitle();

        @Enumerated(EnumType.ORDINAL)
        Sharing getSharing();
    }
}

In the TrackDataRepository:

public interface TrackDataRepository extends JpaRepository<TrackData, Long>, JpaSpecificationExecutor<TrackData> {
    Optional<TrackData> findById(Long id);
    
    // and here's the query in question:
    @Query(value = "SELECT trackdata.id, trackdata.title, trackdata.sharing FROM trackdata " +
            "WHERE trackdata.id = :trackid", nativeQuery = true)
    TrackData.TrackMetadata getMetadata(long trackid);
}

The Entity works as intented:

    TrackData trackData = trackDataRepository.getReferenceById(21682L);
    System.out.println(trackData.getSharing()); //works fine

but the interface is screwed up:

TrackData.TrackMetadata td2 = trackDataRepository.getMetadata(21682L);
System.out.println(td2.getTitle()); //works
System.out.println(td2.getSharing()); //ERROR!!

And the error seems to be because of the projection from short to enum:

Cannot project java.lang.Short to com.cubetrek.cubetrekplayground.database.TrackData$Sharing; Target type is not an interface and no matching Converter found


Also a custom Converter doesn't seem to do the trick:

    @Converter
    public static class SharingConverter implements AttributeConverter<Sharing, Short> {
        @Override
        public Short convertToDatabaseColumn(Sharing sharing) {
            return (short)sharing.ordinal();
        }

        @Override
        public Sharing convertToEntityAttribute(Short dbData) {
            return Sharing.values()[dbData];
        }
    }
    public interface TrackMetadata {
        Long getId();
        @JsonSerialize(using = TitleSerializer.class, as=String.class)
        String getTitle();

        @Convert(converter = SharingConverter.class)
        Sharing getSharing();
    }

Solution

  • There is a workaround solution to solve this. Mark your converter class as a bean with @Component.

    @Component
    @Converter(autoApply = true)
    public class SharingConverter implements AttributeConverter<Sharing, Short> {
      @Override
      public Short convertToDatabaseColumn(Sharing sharing) {
        return (short)sharing.ordinal();
      }
    
      @Override
      public Sharing convertToEntityAttribute(Short dbData) {
        return Sharing.values()[dbData];
      }
    }
    

    Then use SpEL through @Value to convert Short to enum.

    target is an identifier that will be bound to the "aggregate root backing the projection" (Spring Data Jpa docs, section "An Open Projection").

      public interface TrackMetadata {
        Long getId();
    
        String getTitle();
    
        @Value("#{@sharingConverter.convertToEntityAttribute(target.sharing)}")
        Sharing getSharing();
      }