Como criar uma classe service para JPARepository?

Estou usando a seguinte interface para fazer a persistência de dados na minha aplicação:

@Repository
public interface PessoaRepository extends JpaRepository<Pessoa, Integer> {
   Pessoa findById(int id);
   List<Pessoa> findAll();
   **String findByName(String name);**
}

Eu li que é uma boa prática implementar essa interface numa classe service. Sobrescreveria os métodos da seguinte maneira:

@Service
public class PessoaService implements pessoaRepository {
   @Autowired
   PessoaRepository pessoaRepository;

   @Override
   public Pessoa findById(int id) {
       pessoaRepository.Pessoa findById(int id);
   }
   *...Implementação dos outros métodos...*
}

O problema é: como vou implementar o método findByName? Pois o método findById já vem com o JpaRepository.

Ramon, acredito que vc interpretou errado a boa prática.

Primeiro porque não faz sentido nós, os programadores, implementarmos os repositórios porque quem implementa por nós automáticamente é o Spring Data.

Segundo que a boa prática, até onde entendo, diz pra gente dividir as camadas. Os repositórios ficam só com o acesso ao banco, os controladores lidam com as requisições e, no meio destes dois, nós temos os serviços.

Então vc teria um cenário assim:

@Repository
interface PessoaRepository extends JpaRepository<Pessoa, Integer> {
  Pessoa findById(int id);

  List<Pessoa> findAll();

  String findByName(String name);
}

@Service
class PessoaService {
  @Autowired
  private PessoaRepository repository;

  List<Pessoa> findAll() {
    // Aqui vai qualquer lógica de negócio
    return repository.findAll();
  }
}

@RestController
class PessoaController {
  @Autowired
  private PessoaService service;

  @GetMapping
  List<Pessoa> index() {
    return service.findAll();
  }
}

Atente para o comentário indicando o lugar da lógica de negócio. Com esta configuração evitamos que o controlador faça mais do que deveria.

Uma coisa comum também é o seguinte: Ao invés de criar PessoaService como uma classe concreta, vc pode separa em 2 partes, a interface e a implementação. Neste caso seria assim:

interface PessoaService {
  List<Pessoa> findAll();
}

@Service
class PessoaServiceImpl implements PessoaService {
  @Autowired
  private PessoaRepository repository;

  @Override
  public List<Pessoa> findAll() {
    // Aqui vai qualquer lógica de negócio
    return repository.findAll();
  }
}

Mesmo separando desta forma, o código do seu controlador não mudará, ele continuará dependendo de PessoaService e o Spring se encarregará de encontrar sua implementação e injetá-la corretamente.

1 curtida

Muito obrigado!

1 curtida

Eu fiz o que voce fez. Criei uma interface “Pessoaservice”, implementei em uma classe implementation. Para usar no controller, por exemplo, eu uso a interface(anotação autowired num atributo private, tudo certinho) PessoaService para persistir os dados. Mas me gera esse erro aqui:

  • Field UserEntityService in br.com.InitialPopulation required a bean of type ‘br.com.service.UserEntityService’ that could not be found.
  • The injection point has the following annotations:
    • @org.springframework.beans.factory.annotation.Autowired(required=true)

OBS: quando troco a interface “PessoaService” por "PessoaRepository a persistência funciona.

Do jeito como vc descreveu eu não faço ideia do que pode ter dado de errado, mas se vc mandar o código certinho eu posso tentar descobrir.

Segue os códigos das interfaces e classes abaixo abaixo:
OBS - 1: estou usando o JUnit para testes

//interface repository
@Repository
public interface ArticleRepository extends JpaRepository<Article, Integer> {
Article findByTitle(String title);

@Query("select p from Article p where p.title like %?1%")
List<Article> findAllByTitle(String title);
}

//interface service
public interface ArticleService {
Optional<Article> findById(Integer id);
Article findByTitle(String title);
List<Article> findAll();
List<Article> findAllByTitle(String title);
Article save(Article article);
}
//classe service implementation
@Service
public class ArticleServiceImpl implements ArticleService{
@Autowired
ArticleRepository articleRepository;

@Override
public Optional<Article> findById(Integer id) {
	return articleRepository.findById(id);
}

@Override
public Article findByTitle(String title) {
	return articleRepository.findByTitle(title);
}

@Override
public List<Article> findAll() {
	return articleRepository.findAll();
}

@Override
public Article save(Article article) {
	return articleRepository.save(article);
}

@Override
public List<Article> findAllByTitle(String title) {
	return articleRepository.findAllByTitle(title);
}
}
//classe teste
@DataJpaTest
@DisplayName("Classe de teste do ArticleService")
public class ArticleServiceTest {

@Autowired
private ArticleService articleService;

@Test
@DisplayName("Metodo save persiste um novo aluno no banco de dados quando bem sucedido")
void save_Persist_WhenSuccessful() {

	final Article result =  articleService.save(new Article("article 1", "autor 1", LocalDate.of(1998, 05, 12), "O lorem ipsum é um texto bastante conhecido dos profissionais de design e editoração. Trata-se de um poema em latim, escrito por Cícero em 45 a.C, com as letras embaralhadas. É utilizado no mundo todo para preencher espaços em publicações antes da inserção do conteúdo real."));
			
	assertAll(
			() -> assertThat(result).isNotNull(),
			() -> assertThat(result.getAuthor()).isNotBlank(),
			() -> assertThat(result.getTitle()).isNotBlank(),
			() -> assertThat(result.getText()).isNotBlank(),
			() -> assertThat(result.getText()).hasSizeGreaterThan(200),
			() -> assertThat(result.getDate()).asInstanceOf(InstanceOfAssertFactories.LOCAL_DATE)
			);
}
...outros métodos

Entendi. Se vc não estiver usando a anotação SpringBootTest, o JUnit não injeta os beans automaticamente mesmo. Vc tem que ver como configurar para ele injetar para vc.

Ramon, achei uma possível solução para vc bem aqui:
https://stackoverflow.com/a/41084739/3334365

Basicamente ele orienta a usar a anotação @Import na sua classe de teste. Eu testei aqui e deu certo, minha classe de teste ficou assim:

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;

import static org.junit.jupiter.api.Assertions.assertTrue;

@DataJpaTest
@Import(ArticleServiceImpl.class)
class DemoApplicationTests {
    @Autowired
    private ArticleService articleService;

    @Test
    void test() {
        assertTrue(articleService.findAll().isEmpty());
    }
}

Perceba que no @Import eu passei a classe que Implementa ArticleService, mas ali no @Autowired eu usei a própria interface ArticleService.

Obrigado. Então no meu código normal(a aplicação real, usando o banco de dados real) eu não preciso dessa anotação “import” correto?

Não deveria precisar.

Normalmente vc teria uma classe anotada com @SpringBootApplication e só isso já bastaria. Mesmo que não tenha, há outras formas de lidar.

1 curtida

Amigo @wldomiciano, Poderia me tirar uma dúvida sobre testes. Seguindo essa organização de código que você me informou(interface repository, interface service e uma classe implementation). Digamos que eu vá fazer testes(com o objetivo de saber se meus métodos estão funcionando) qual devo testar? Tipo, eu testo o implementation, ou o service ou o repository?

Vc não vai testar sua interface service porque ela só teria métodos abstratos. Vc testaria apenas a classe que implementa esta interface.

Sobre testar o repository, eu sei que é importante testar quando vc tem queries complexas. Vc não precisaria testar um findById, por exemplo. Mas eu ainda não sei muito sobre boas práticas de testes, então é bom vc pesquisar mais a fundo sobre isso.

1 curtida

Mas tem diferença entre testar a interface repository e a serviceImpl? Não são a mesma coisa, quero dizer, elas não persistem os dados no banco?

Desculpa pelo tanto de pergunta. Mas tenho outra duvida, na interface service vi la em cima que você coloca o método “save” para retornar um objeto, não poderia colocar esse método para retornar nada(void)? tem diferença entre retornar e não retornar nada?

Nem sempre ServiceImpl e Repository farão a mesma coisa.

Em sistemas onde a lógica de negócio é simples, pode até ser ServiceImpl apenas repasse as chamadas para o Repository sem fazer nada mais.

Porém, há situações em que vc precisa fazer calculos, ajustes e validações antes de salvar os dados no banco. É neste cenário que ter um ServiceImpl vale mais a pena.

Por exemplo, digamos que na sua regra de negócio não é permitido o cadastro de usuários com menos de 18 anos. Vc faria a verificação de idade no service.

class ServiceImpl implements Service {
  void save(String userName, int userAge) {
    if (userAge < 18) {
      throw new UserAgeNotAllowedException();
    } else {
      this.repository.save(new User(userName, userAge));
    }
  }
}

O que eu já vi sobre testar o service e testar o repository é que quando vc for testar o repository, vai precisar levantar um banco de dados, nem que seja um H2, para isso vc usaria o @DataJpaTest.

E quando vc for testar o service, vc vai focar apenas na lógica adicional que este service faz. Para isso vc apenas mockaria o repository ao invés de acessar um banco de dados real. Mas claro que tudo depende do seu cenário e das suas necessidades.

Poderia usar void. Tudo depende do que vc precisa.

Imagina uma situação em que o cliente da sua API quer cadastrar um certo item e, se o cadastro for bem sucedido, ele precisa saber o id com o qual este item foi cadastrado.

Como é o banco que vai gerar este id, o jeito mais fácil de o cliente ter esta informação imediatamente é se eu retorná-la após o salvamento. Foi este cenário que eu imaginei quando pensei naquele código.