d0x
d0x

Reputation: 11573

Query by a parent field but return a child class

Is it possible to write a Service that provides a method that selects entities by a parent field but returns a child class that gets passed in by a parameter (or generic on the method)?

Assuming the Parent has a String field called flag and two childs Child1 and Child2, the ParentService should be usable like this:

Child1 child1 = parentService.findFlag(Child1.class, "de");
Child2 child2 = parentService.findFlag(Child2.class, "de");

or

Child1 child1 = parentService.findFlag<Child1>("de");
Child2 child2 = parentService.findFlag<Child2>("de");

Detailed example:

Because it is quite hard to explain, this code should explain the scenario from above:

Java Classes:

Now it would be handy to add a find(UiParameter.class, commonFieldSelector) or find(BatchParameter.class, commonFieldSelector) method to the ParameterService.

Currently there are 4 boilerblate classes (UiParameterService, UiParameterRepository, BatchParameterService, BatchParameterRepository) with just 1 method that looks almost similar.

@Data
@EqualsAndHashCode(callSuper = true)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING)
@DiscriminatorValue("PARAMETER")
public abstract class Parameter extends AbstractEntity
{
    // all children will share that
    @ManyToOne
    private Application commonField;
}

@Data
@Entity
@EqualsAndHashCode(callSuper = true)
public class UiParameter extends Parameter
{
    private String foo;
}

@Data
@Entity
@EqualsAndHashCode(callSuper = true)
public class BatchParameter extends Parameter
{
    private int size;
}

public interface ParameterRepository extends JpaRepository<Parameter, Long>
{
    // That works great, but it returns all kind of children...
    List<Parameter> findByCommonField(final Application commonField);
}

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ParameterService
{
    private final ParameterRepository parameterRepository;

    public ... findByCommonField...(...)
    {
        return ... magic ...;
    }
}

The usual Service/Repository for all Childs approach leads in our case into boilerplate classes having just a findFlag method that gets copied from Child1 to ChildN.

Changing the Domain (inheritance strategy) will be a problem.

Spring-Solution based on Maciejs answer:

public interface ParameterRepository extends JpaRepository<Parameter, Long>
{
    @Query("SELECT e FROM Parameter e WHERE TYPE(e) = ?1 AND e.commonField = ?2")
    <E extends Parameter> List<E> findByTopic(final Class<E> e, final Application commonField);
}

Upvotes: 3

Views: 2878

Answers (1)

Maciej Dobrowolski
Maciej Dobrowolski

Reputation: 12122

There's TYPE operator in JPQL, you can use it as following:

SELECT e
FROM EntityClass
WHERE TYPE(e) = EntityClassSubclass

You can use this query and then write generic method which casts results to desired type.

i.e.

private static <T extends SuperClass> List<T> queryForClass(Class<T> myClass) {

    Query q = em.createQuery("SELECT p FROM SuperClass p WHERE TYPE(p) = :myClass");
    q.setParameter("myClass", myClass);
    List<? extends SuperClass> resultList = q.getResultList();

    return (List<T>) resultList;
}

Upvotes: 2

Related Questions