Comment ajouter une méthode personnalisée à Spring Data JPA

160

Je regarde dans Spring Data JPA. Considérez l'exemple ci-dessous où je ferai fonctionner toutes les fonctionnalités crud et finder par défaut et si je veux personnaliser un finder, cela peut également être fait facilement dans l'interface elle-même.

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {

  @Query("<JPQ statement here>")
  List<Account> findByCustomer(Customer customer);
}

Je voudrais savoir comment puis-je ajouter une méthode personnalisée complète avec son implémentation pour le AccountRepository ci-dessus? Comme c'est une interface, je ne peux pas implémenter la méthode là-bas.

Sharad Yadav
la source

Réponses:

290

Vous devez créer une interface distincte pour vos méthodes personnalisées:

public interface AccountRepository 
    extends JpaRepository<Account, Long>, AccountRepositoryCustom { ... }

public interface AccountRepositoryCustom {
    public void customMethod();
}

et fournissez une classe d'implémentation pour cette interface:

public class AccountRepositoryImpl implements AccountRepositoryCustom {

    @Autowired
    @Lazy
    AccountRepository accountRepository;  /* Optional - if you need it */

    public void customMethod() { ... }
}

Voir également:

axtavt
la source
21
Cette implémentation personnalisée peut-elle injecter le référentiel réel, afin qu'elle puisse utiliser les méthodes définies ici? Plus précisément, j'aimerais faire référence à diverses fonctions find * définies dans l'interface Repository dans une implémentation find de niveau supérieur. Puisque ces fonctions find * () n'ont pas d'implémentation, je ne peux pas les déclarer dans l'interface personnalisée ou la classe Impl.
JBCP
18
J'ai suivi cette réponse, malheureusement maintenant Spring Data essaie de trouver la propriété "customMethod" sur mon objet "Account" car il essaie de générer automatiquement une requête pour toutes les méthodes définies sur AccountRepository. Un moyen d'arrêter ça?
Nick Foote
41
@NickFoote notez que le nom de la classe que vous implémentez dans votre référentiel doit être: AccountRepositoryImplnot :, AccountRepositoryCustomImpletc. - c'est une convention de dénomination très stricte.
Xeon
5
@ wired00 Je pense que cela crée une référence circulaire et je ne vois pas comment @JBCP l'a fait fonctionner. Quand j'essaie de faire quelque chose de similaire, je me retrouve avec une exception:Error creating bean with name 'accountRepositoryImpl': Bean with name 'accountRepositoryImpl' has been injected into other beans [accountRepository] in its raw version as part of a circular reference, but has eventually been wrapped.
Robert Hunt
6
Ouais, voyez mon commentaire précédent sur le fait que cela ne fonctionne pas si vous étendez QueryDslRepositorySupportVous devez également injecter le référentiel via l'injection de champ ou de setter plutôt que l'injection de constructeur, sinon il ne pourra pas créer le bean. Cela semble fonctionner mais la solution semble un peu `` sale '', je ne suis pas sûr qu'il y ait des plans pour améliorer la façon dont cela fonctionne de la part de l'équipe Spring Data.
Robert Hunt
72

En plus de la réponse d'axtavt , n'oubliez pas que vous pouvez injecter Entity Manager dans votre implémentation personnalisée si vous en avez besoin pour créer vos requêtes:

public class AccountRepositoryImpl implements AccountRepositoryCustom {

    @PersistenceContext
    private EntityManager em;

    public void customMethod() { 
        ...
        em.createQuery(yourCriteria);
        ...
    }
}
gelées
la source
10
Merci, cependant, je veux savoir comment utiliser Pageable et Page dans l'implémentation personnalisée. Des entrées?
Wand Maker
17

La réponse acceptée fonctionne, mais présente trois problèmes:

  • Il utilise une fonctionnalité Spring Data non documentée lors de la dénomination de l'implémentation personnalisée en tant que AccountRepositoryImpl. La documentation indique clairement qu'il doit être appeléAccountRepositoryCustomImpl , le nom de l'interface personnalisée plusImpl
  • Vous ne pouvez pas utiliser l'injection de constructeur, uniquement @Autowired qui est considérée comme une mauvaise pratique
  • Vous avez une dépendance circulaire à l'intérieur de l'implémentation personnalisée (c'est pourquoi vous ne pouvez pas utiliser l'injection de constructeur).

J'ai trouvé un moyen de le rendre parfait, mais non sans utiliser une autre fonctionnalité Spring Data non documentée:

public interface AccountRepository extends AccountRepositoryBasic,
                                           AccountRepositoryCustom 
{ 
}

public interface AccountRepositoryBasic extends JpaRepository<Account, Long>
{
    // standard Spring Data methods, like findByLogin
}

public interface AccountRepositoryCustom 
{
    public void customMethod();
}

public class AccountRepositoryCustomImpl implements AccountRepositoryCustom 
{
    private final AccountRepositoryBasic accountRepositoryBasic;

    // constructor-based injection
    public AccountRepositoryCustomImpl(
        AccountRepositoryBasic accountRepositoryBasic)
    {
        this.accountRepositoryBasic = accountRepositoryBasic;
    }

    public void customMethod() 
    {
        // we can call all basic Spring Data methods using
        // accountRepositoryBasic
    }
}
Danila Piatov
la source
Cela a fonctionné. Je tiens à souligner l'importance du nom du paramètre dans le constructeur doit suivre la convention dans cette réponse (doit être accountRepositoryBasic). Sinon, Spring s'est plaint qu'il y avait 2 choix de haricots pour l'injection dans mon *Implconstructeur.
chèvre le
alors quelle est l'utilisation de AccountRepository
Kalpesh Soni
@KalpeshSoni les méthodes des deux AccountRepositoryBasicet AccountRepositoryCustomsera disponible via un injectéAccountRepository
geg
1
Pouvez-vous s'il vous plaît indiquer la manière dont le contexte doit être créé? Je ne suis pas en mesure de tout mettre en place. Je vous remercie.
franta kocourek
12

L'utilisation est limitée, mais pour les méthodes personnalisées simples, vous pouvez utiliser des méthodes d'interface par défaut telles que:

import demo.database.Customer;
import org.springframework.data.repository.CrudRepository;

public interface CustomerService extends CrudRepository<Customer, Long> {


    default void addSomeCustomers() {
        Customer[] customers = {
            new Customer("Józef", "Nowak", "[email protected]", 679856885, "Rzeszów", "Podkarpackie", "35-061", "Zamknięta 12"),
            new Customer("Adrian", "Mularczyk", "[email protected]", 867569344, "Krosno", "Podkarpackie", "32-442", "Hynka 3/16"),
            new Customer("Kazimierz", "Dejna", "[email protected]", 996435876, "Jarosław", "Podkarpackie", "25-122", "Korotyńskiego 11"),
            new Customer("Celina", "Dykiel", "[email protected]", 947845734, "Żywiec", "Śląskie", "54-333", "Polna 29")
        };

        for (Customer customer : customers) {
            save(customer);
        }
    }
}

ÉDITER:

Dans ce tutoriel de printemps, il est écrit:

Spring Data JPA vous permet également de définir d'autres méthodes de requête en déclarant simplement leur signature de méthode.

Il est donc même possible de déclarer simplement une méthode comme:

Customer findByHobby(Hobby personHobby);

et si l'objet Hobbyest une propriété du client, Spring définira automatiquement la méthode pour vous.

Tomasz Mularczyk
la source
6

J'utilise le code suivant pour accéder aux méthodes de recherche générées à partir de mon implémentation personnalisée. Faire passer l'implémentation via la fabrique de haricots évite les problèmes de création de haricots circulaires.

public class MyRepositoryImpl implements MyRepositoryExtensions, BeanFactoryAware {

    private BrandRepository myRepository;

    public MyBean findOne(int first, int second) {
        return myRepository.findOne(new Id(first, second));
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        myRepository = beanFactory.getBean(MyRepository.class);
    }
}
Peter Rietzler
la source
5

Comme spécifié dans la fonctionnalité documentée , le Implsuffixe nous permet d'avoir une solution propre:

  • Définir dans l' @Repositoryinterface, par exempleMyEntityRepository , des méthodes Spring Data ou des méthodes personnalisées
  • Créez une classe MyEntityRepositoryImpl(le Implsuffixe est la magie) n'importe où (n'a même pas besoin d'être dans le même package) qui implémente uniquement les méthodes personnalisées et annotez cette classe avec @Component** ( @Repository ne sera pas fonctionnera ).
    • Cette classe peut même injecter MyEntityRepositoryvia @Autowiredpour une utilisation dans les méthodes personnalisées.


Exemple:

Classe d'entité:

package myapp.domain.myentity;

@Entity
public class MyEntity {

    @Id
    private Long id;

    @Column
    private String comment;

}

Interface du référentiel:

package myapp.domain.myentity;

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {

    // EXAMPLE SPRING DATA METHOD
    List<MyEntity> findByCommentEndsWith(String x);

    List<MyEntity> doSomeHql(Long id);

    List<MyEntity> useTheRepo(Long id);

}

Bean d'implémentation des méthodes personnalisées:

package myapp.infrastructure.myentity;

@Component // Must be @Component !!
public class MyEntityRepositoryImpl { // must have the repo name + Impl !!

    @PersistenceContext
    private EntityManager entityManager;

    @Autowired
    private MyEntityRepository myEntityRepository;

    @SuppressWarnings("unused")
    public List<MyEntity> doSomeHql(Long id) {
        String hql = "SELECT eFROM MyEntity e WHERE e.id = :id";
        TypedQuery<MyEntity> query = entityManager.createQuery(hql, MyEntity.class);
        query.setParameter("id", id);
        return query.getResultList();
    }

    @SuppressWarnings("unused")
    public List<MyEntity> useTheRepo(Long id) {
        List<MyEntity> es = doSomeHql(id);
        es.addAll(myEntityRepository.findByCommentEndsWith("DO"));
        es.add(myEntityRepository.findById(2L).get());
        return es;
    }

}

Les petits inconvénients que j'ai identifiés sont:

  • Les méthodes personnalisées de la Implclasse sont marquées comme inutilisées par le compilateur, donc le@SuppressWarnings("unused") suggestion.
  • Vous avez une limite d'une Implclasse. (Alors que dans l'implémentation régulière des interfaces de fragment, les documents suggèrent que vous pourriez en avoir beaucoup.)
acdcjunior
la source
Il y a une petite mise en garde lors des tests. Si vous en avez besoin, faites-le moi savoir et je mettrai à jour la réponse.
acdcjunior
comment correctement Autowire MyEntityRepositoryImpl?
Konstantin Zyubin le
@KonstantinZyubin Vous autowire MyEntityRepository, pas le *Impl.
acdcjunior
4

Si vous souhaitez pouvoir effectuer des opérations plus sophistiquées, vous devrez peut-être accéder aux composants internes de Spring Data, auquel cas les opérations suivantes (comme ma solution provisoire pour DATAJPA-422 ):

public class AccountRepositoryImpl implements AccountRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    private JpaEntityInformation<Account, ?> entityInformation;

    @PostConstruct
    public void postConstruct() {
        this.entityInformation = JpaEntityInformationSupport.getMetadata(Account.class, entityManager);
    }

    @Override
    @Transactional
    public Account saveWithReferenceToOrganisation(Account entity, long referralId) {
        entity.setOrganisation(entityManager.getReference(Organisation.class, organisationId));
        return save(entity);
    }

    private Account save(Account entity) {
        // save in same way as SimpleJpaRepository
        if (entityInformation.isNew(entity)) {
            entityManager.persist(entity);
            return entity;
        } else {
            return entityManager.merge(entity);
        }
    }

}
NealeU
la source
4

Compte tenu de votre extrait de code, veuillez noter que vous ne pouvez passer des objets natifs qu'à la méthode findBy ###, disons que vous souhaitez charger une liste de comptes appartenant à certains clients, une solution est de le faire,

 @Query("Select a from Account a where a."#nameoffield"=?1")
      List<Account> findByCustomer(String "#nameoffield");

Faites en sorte que le nom de la table à interroger soit le même que la classe Entity. Pour d'autres implémentations, veuillez consulter ceci

Samba
la source
1
Le est une faute de frappe sur la requête, il devrait être nameoffie l d, je n'ai pas le droit de le réparer.
BrunoJCM
3

Il y a une autre question à considérer ici. Certaines personnes s'attendent à ce que l'ajout d'une méthode personnalisée à votre référentiel les expose automatiquement en tant que services REST sous le lien '/ search'. Ce n'est malheureusement pas le cas. Spring ne prend pas en charge cela actuellement.

Il s'agit d'une fonctionnalité `` par conception '', Spring Data Rest vérifie explicitement si la méthode est une méthode personnalisée et ne l'expose pas en tant que lien de recherche REST:

private boolean isQueryMethodCandidate(Method method) {    
  return isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method);
}

C'est une question d'Oliver Gierke:

C'est par conception. Les méthodes de référentiel personnalisées ne sont pas des méthodes de requête car elles peuvent implémenter efficacement n'importe quel comportement. Ainsi, il nous est actuellement impossible de décider de la méthode HTTP sous laquelle exposer la méthode. POST serait l'option la plus sûre, mais ce n'est pas conforme aux méthodes de requête génériques (qui reçoivent GET).

Pour plus de détails, consultez ce numéro: https://jira.spring.io/browse/DATAREST-206

Lukasz Magiera
la source
C'est malheureux, j'ai perdu tellement de temps à essayer de découvrir ce que j'ai fait de mal, et enfin, je comprends qu'il n'y a pas une telle fonctionnalité. Pourquoi auraient-ils même implémenté cette fonctionnalité? Avoir moins de haricots? Pour avoir toutes les méthodes DAO au même endroit? J'aurais pu y parvenir par d'autres moyens. Quelqu'un sait-il quel est l'objectif de la fonctionnalité "Ajouter un comportement à des référentiels uniques"?
Skeeve
Vous pouvez exposer n'importe quelle méthode de référentiel via REST en ajoutant simplement l' @RestResource(path = "myQueryMethod")annotation à la méthode. La citation ci-dessus indique simplement que Spring ne sait pas comment vous voulez le mapper (c'est-à-dire GET vs POST, etc.), c'est donc à vous de le spécifier via l'annotation.
GreenGiant
1

Ajout d'un comportement personnalisé à tous les référentiels:

Pour ajouter un comportement personnalisé à tous les référentiels, vous ajoutez d'abord une interface intermédiaire pour déclarer le comportement partagé.

public interface MyRepository <T, ID extends Serializable> extends JpaRepository<T, ID>
{

    void sharedCustomMethod( ID id );
}

Désormais, vos interfaces de référentiel individuelles étendront cette interface intermédiaire au lieu de l'interface de référentiel pour inclure la fonctionnalité déclarée.

Ensuite, créez une implémentation de l'interface intermédiaire qui étend la classe de base du référentiel spécifique à la technologie de persistance. Cette classe agira alors comme une classe de base personnalisée pour les proxys du référentiel.

public class MyRepositoryImpl <T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID>
{

    private EntityManager entityManager;

       // There are two constructors to choose from, either can be used.
    public MyRepositoryImpl(Class<T> domainClass, EntityManager entityManager)
    {
        super( domainClass, entityManager );

        // This is the recommended method for accessing inherited class dependencies.
        this.entityManager = entityManager;
    }


    public void sharedCustomMethod( ID id )
    {
        // implementation goes here
    }
}

Spring Data Repositories Partie I. Référence entrez la description de l'image ici

Ali Yeganeh
la source
0

J'étend le SimpleJpaRepository:

public class ExtendedRepositoryImpl<T extends EntityBean> extends SimpleJpaRepository<T, Long>
    implements ExtendedRepository<T> {

    private final JpaEntityInformation<T, ?> entityInformation;

    private final EntityManager em;

    public ExtendedRepositoryImpl(final JpaEntityInformation<T, ?> entityInformation,
                                                      final EntityManager entityManager) {
       super(entityInformation, entityManager);
       this.entityInformation = entityInformation;
       this.em = entityManager;
    }
}

et ajoute cette classe à @EnableJpaRepositoryries repositoryBaseClass.

Devilluminati
la source