web-dev-qa-db-fra.com

Utilisation de génériques dans les référentiels Spring Data JPA

J'ai un certain nombre de types d'objets simples qui doivent être conservés dans une base de données. J'utilise Spring JPA pour gérer cette persistance. Pour chaque type d'objet, je dois créer les éléments suivants:

import org.springframework.data.jpa.repository.JpaRepository;

public interface FacilityRepository extends JpaRepository<Facility, Long> {
}


public interface FacilityService {
    public Facility create(Facility facility);
}

@Service
public class FacilityServiceImpl implements FacilityService {

    @Resource
    private FacilityRepository countryRepository;

    @Transactional
    public Facility create(Facility facility) {
        Facility created = facility;
        return facilityRepository.save(created);
    }
}

Il m'est venu à l'esprit qu'il peut être possible de remplacer les multiples classes pour chaque type d'objet par trois classes basées sur des génériques, économisant ainsi beaucoup de codage standard. Je ne sais pas exactement comment s'y prendre et en fait si c'est une bonne idée?

26
skyman

Tout d'abord, je sais que nous élevons un peu la barre ici, mais c'est déjà énormément moins de code que ce que vous aviez à écrire sans l'aide de Spring Data JPA.

Deuxièmement, je pense que vous n'avez pas besoin de la classe de service en premier lieu, si tout ce que vous faites est de transférer un appel vers le référentiel. Nous vous recommandons d'utiliser des services devant les référentiels si vous avez une logique métier qui nécessite l'orchestration de différents référentiels dans une transaction ou si vous avez une autre logique métier à encapsuler.

De manière générale, vous pouvez bien sûr faire quelque chose comme ceci:

interface ProductRepository<T extends Product> extends CrudRepository<T, Long> {

    @Query("select p from #{#entityName} p where ?1 member of p.categories")
    Iterable<T> findByCategory(String category);

    Iterable<T> findByName(String name);
}

Cela vous permettra d'utiliser le référentiel côté client comme ceci:

class MyClient {

  @Autowired
  public MyClient(ProductRepository<Car> carRepository, 
                  ProductRepository<Wine> wineRepository) { … }
}

et cela fonctionnera comme prévu. Cependant, il y a quelques points à noter:

Cela ne fonctionne que si les classes de domaine utilisent l'héritage d'une seule table. Les seules informations sur la classe de domaine que nous pouvons obtenir à bootstrap = le temps, ce sera des objets Product. Ainsi, pour des méthodes comme findAll() et même findByName(…), les requêtes pertinentes commenceront par select p from Product p where…. Ceci est en raison du fait que la recherche de réflexion ne pourra jamais produire Wine ou Car à moins que vous créez une interface de référentiel dédiée pour capturer le béton saisir des informations.

De manière générale, nous recommandons de créer des interfaces de référentiel par racine agrégée . Cela signifie que vous n'avez pas de dépôt pour chaque classe de domaine en soi. Encore plus important, une abstraction 1: 1 d'un service sur un référentiel manque également complètement le point. Si vous créez des services, vous n'en créez pas pour chaque référentiel (un singe pourrait le faire, et nous ne sommes pas des singes, n'est-ce pas?;). Un service expose une API de niveau supérieur, est beaucoup plus un lecteur de cas d'utilisation et orchestre généralement les appels vers plusieurs référentiels.

De plus, si vous créez des services au-dessus des référentiels, vous voulez généralement forcer les clients à utiliser le service au lieu du référentiel (un exemple classique ici est qu'un service de gestion des utilisateurs déclenche également la génération de mot de passe et le cryptage, de sorte qu'en aucun cas ce serait une bonne idée de laisser les développeurs utiliser le référentiel directement car ils contourneraient efficacement le chiffrement). Donc, vous voulez généralement être sélectif sur qui peut persister quels objets de domaine pour ne pas créer de dépendances partout.

Sommaire

Oui, vous pouvez créer des référentiels génériques et les utiliser avec plusieurs types de domaines, mais il existe des limitations techniques assez strictes. Pourtant, d'un point de vue architectural, le scénario que vous décrivez ci-dessus ne devrait même pas apparaître car cela signifie que vous êtes de toute façon confronté à une odeur de design.

65
Oliver Drotbohm

Je travaille sur un projet pour créer le référentiel générique pour cassandra avec des données de printemps.

Créez d'abord une interface de référentiel avec du code.

StringBuilder sourceCode = new StringBuilder();
sourceCode.append("import org.springframework.boot.autoconfigure.security.SecurityProperties.User;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.AllowFiltering;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.Query;\n");
sourceCode.append("import org.springframework.data.repository.CrudRepository;\n");
sourceCode.append("\n");
sourceCode.append("public interface TestRepository extends CrudRepository<Entity, Long> {\n");
sourceCode.append("}");

Compiler le code et obtenir la classe, j'utilise org.mdkt.compiler.InMemoryJavaCompiler

ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
compiler = InMemoryJavaCompiler.newInstance();
compiler.useParentClassLoader(classLoader);
Class<?> testRepository = compiler.compile("TestRepository", sourceCode.toString());

Et initialisez le référentiel lors de l'exécution des données de printemps. C'est un peu délicat car je débogue le code SpringData pour trouver comment il initialise une interface de référentiel au printemps.

CassandraSessionFactoryBean bean = context.getBean(CassandraSessionFactoryBean.class);
RepositoryFragments repositoryFragmentsToUse = (RepositoryFragments) Optional.empty().orElseGet(RepositoryFragments::empty); 
CassandraRepositoryFactory factory = new CassandraRepositoryFactory(
    new CassandraAdminTemplate(bean.getObject(), bean.getConverter()));
factory.setBeanClassLoader(compiler.getClassloader());
Object repository = factory.getRepository(testRepository, repositoryFragmentsToUse);

Vous pouvez maintenant essayer la méthode de sauvegarde du référentiel et vous pouvez essayer d'autres méthodes telles que findById.

Method method = repository.getClass().getMethod("save", paramTypes);
T obj = (T) method.invoke(repository, params.toArray());

Un exemple complet de code et d'implémentation que j'ai mis dans ce dépôt https://github.com/maye-msft/generic-repository-springdata .

Vous pouvez l'étendre à JPA avec la même logique.

1
Ye Ma