Search code examples
javaspringpostgresqlhibernateprojection

Polymorphic Projections in Spring JPA


I need polymorphic projections that map differently for each subtype. I found this thread and tried to copy it, but it does not work. The type variable is set correctly, but the projection-sub-classes are not used. I could not find any news on this topic, last posts on this are 5 years old.

This is my Baseclass Material

public class Material implements FileOwner {
    protected Long id;
    String name;
    int grade;
    boolean favorite = false;
}

Subclass LinkMaterial

public class LinkMaterial extends Material {
    String url;
}

Now my projections:

@Projection(name = "material", types = Material.class)
public interface DTOMaterial {
    @Value("#{target.getClass().getSimpleName()}")
    String getType();
    Long getId();
    String getName();
    int getGrade();
    boolean getFavorite();
}

And the inheriting projection

@Projection(name = "material", types = LinkMaterial.class)
public interface DTOLinkMaterial extends DTOMaterial{
    String getURL();
}

This is my repository method to get the DTOs:

@Query(value = "SELECT m from Material m JOIN FETCH m.submissionTasks stm WHERE stm.id.submissionTaskId=:id")
<T> List<T> getForSubmissionTask(Long id, Class<T> type);

I use the method like this:

public List<DTOMaterial> getMaterialDTOsForSubmissionTask(Long id) {
    return materialRepository.getForSubmissionTask(id, DTOMaterial.class);
}

If I check if my DTOMaterial instanceof DTOLinkMaterial it is always false. However, Hibernate definetely understands that the Material itself is a LinkMaterial as you can see in the json I get on my client.

{name: "test LinkMaterial", id: 62, type: "LinkMaterial", grade: 8, favorite: false}

So type is correct. I tried to change the name of the projection, but this did also not succeed.

Hope, that anybody can help me, thanks in advance!


Solution

  • Spring Data Projections has many limitations and I think this is a perfect use case for Blaze-Persistence Entity Views.

    I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

    A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

    @EntityView(Material.class)
    @EntityViewInheritance
    public interface DTOMaterial {
        @IdMapping
        Long getId();
        default getType() {
            return "Material";
        }
        String getName();
        int getGrade();
        boolean getFavorite();
    }
    
    @EntityView(LinkMaterial.class)
    public interface DTOLinkMaterial extends DTOMaterial {
        String getURL();
    }
    

    Notice how you can enable the use of inheritance by simply annotating the root type with @EntityViewInheritance. It will automatically work based on the entity subtype used in subtypes of the main entity view. Note though that you can also do some ad-hoc inheritance based on some predicate if you want.

    Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

    DTOMaterial a = entityViewManager.find(entityManager, DTOMaterial.class, id);

    The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

    Page<DTOMaterial> findAll(Pageable pageable);
    

    The best part is, it will only fetch the state that is actually necessary!

    In your particular case, it will probably be best if you implement the lookup by using a specification:

    default <T> List<T> getForSubmissionTask(Long id, Class<T> type) {
        return getForSubmissionTask(
            (root, query, cb) -> cb.equal(root.get("submissionTasks").get("id"), id), 
            type
        );
    }
    <T> List<T> getForSubmissionTask(Specification s, Class<T> type);