2

Is there a way to make a generic Spring Data JPA repository handle correctly methods like findAll()? e.g. AnimalRepository<Dog>.findAll return only Dogs, instead of all animals? Or at least, how would be the best workaround at that?

Say I have this:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Animal extends BaseEntity {
    ...
}

@Entity
public class Dog extends Animal {
    ...
}

@Entity
public class Capybara extends Animal {
    ...
}

public interface AnimalRepository<T extends Animal> extends JpaRepository<T, Long> {

}

@Service("animalService")
public class AnimalServiceImpl<T extends Animal> implements AnimalService<T> {

    private final AnimalRepository<T> animalRepository;

    @Autowired
    public AnimalServiceImpl(AnimalRepository<T> aR) {
        this.animalRepository = aR;
    }

    @Override
    public void save(T animal) {
        animalRepository.save(animal);
    }

    @Override
    public List<T> findAll() {
        return animalRepository.findAll();
    }

    ...

}

It works almost perfectly, saves each animal on its own table and so. The only problem is: the findAll() returns BOTH Capybaras and Dogs. This answer explains:

This only works if the domain classes use single table inheritance. The only information about the domain class we can get at bootstrap time is that it will be Product objects. So for methods like findAll() and even findByName(…) the relevant queries will start with select p from Product p where…. This is due to the fact that the reflection lookup will never ever be able to produce Wine or Car unless you create a dedicated repository interface for it to capture the concrete type information.

Ok, sadly the code gets a bit less clean having multiple repositories instead of only one. But I would still like to keep a single AnimalService class. This is the way I did:

@Service("animalService")
public class AnimalServiceImpl<T extends Animal> implements AnimalService<T> {

    private final DogRepository dogRepository;
    private final CapybaraRepository capybaraRepository;

    @Autowired
    public AnimalServiceImpl(AnimalRepository<T> aR) {
        this.animalRepository = aR;
    }

    @Override
    public void save(T animal) {
        animalRepository.save(animal);
    }

    @Override
    public List<T> findAllDogs() {
        return dogRepository.findAll();
    }

    @Override
    public List<T> findAllCapybaras() {
        return capybaraRepository.findAll();
    }

    ...

}

If the Repository really can't handle the findAll() according to the type <T>, what would be the cleanest way to have a single AnimalService? Surely there must be a better way than what I did, since it gets real ugly, really fast that way if you have more complexity in the service and more than a couple animals.


EDIT: Given that Spring's DI considers AnimalRepository<Dog> and AnimalRepository<Capybara> as being the same thing (injecting the same Repository onto a Service that uses the Capybara one and another Service who uses the Dog one), I had to create a different Repository and Service for each of them (Option B of @ESala answer):

@NoRepositoryBean
public interface AnimalRepository<T extends Animal> extends JpaRepository<T, Long> {
}

public interface DogRepository extends AnimalRepository<Dog> {   
}

public interface CapybaraRepository extends AnimalRepository<Capybara> {   
}

public abstract class AnimalService<T extends Animal> {
    private final AnimalRepository<T> animalRepository;

    AnimalService(AnimalRepository<T> repo) {
        this.animalRepository = repo;
    }

    public void salvar(T palavra) {
        animalRepository.save(palavra);
    }

    public List<T> findAll() {
        return animalRepository.findAll();
    }

}

@Service
public class DogService extends AnimalService<Dog> {

    @Autowired
    public DogService(DogRepository repo) {
        super(repo);
    }
}

@Service
public class CapybaraService extends AnimalService<Capybara> {

    @Autowired
    public CapybaraService(CapybaraRepository repo) {
        super(repo);
    }
}

There's probably a better way, so I'll stay open to suggestions.

2 Answers2

3

On the repository: since you don't use single table inheritance, you are going to need a repository instance for each T, no way around that. This means you will have an instance of AnimalRepository<Dog> and an instance of AnimalRepository<Capybara>.

On the service: somewhere in your application you will need a switch/case that directs you to the correct repository depending on the type.

You have 2 options, a) having a single service instance that handles all types and internally directs the query to the corresponding repository, or b) having a service instance for each T and selecting the instance elsewhere.

I prefer option a). You can adapt your current implementation to return the correct type with a single findAll() by getting a reference to the T type and using a switch/case for selecting the appropriate repository.

Getting a reference to <T> at runtime is messy because of type erasure, but it can be done.

ESala
  • 6,878
  • 4
  • 34
  • 55
  • I tried doing this, but as it turns out, Spring singletons (`@Service, @Repository, @Components`, etc) doesn't account for a type ``. i.e.: Both `AnimalRepository` and `AnimalRepository` are the same. [More info](https://stackoverflow.com/a/25650321/9180376). – Cleyton Gonçalves Jan 08 '18 at 01:45
  • The option B is the one which works best using Spring's DI, given what I said on the comment above. I'll edit the question to lay out what I had to do. Thank you. – Cleyton Gonçalves Jan 08 '18 at 03:17
0

How about adding a category property/enum to Animal

@Override
public List<T> findAll(Animal animal) {
    if(animal.getCategory() == "Dog") 
       return findAllDogs();
    if(animal.getCategory() == "Capybara") 
       return findAllCapybaras();
}

@Override
private List<T> findAllDogs() {
    return dogRepository.findAll();
}

@Override
private List<T> findAllCapybaras() {
    return capybaraRepository.findAll();
}

NOTE: It's also possible to get the Class type of the generic but this is a little more tricky

How to get a class instance of generics type T

Garry Taylor
  • 940
  • 8
  • 19