- Domain Model
- Domain Instance
- User entity and resource
- H2 database, test profile, JPA
- JPA repository, dependency injection, database seeding
- Service layer, component registration
- Order, Instant, ISO 8601
- OrderStatus enum
- Entidade Category
A camada de Resources é um controlador. Deve somente intermediar as operações de aplicação e regras de negócio.
Portanto, o Resources (controlador/UserResource) irá depender do Service (service/UserService).
E o Service depende do Repository/UserRepository - a interface.
Ou seja: @RestControllers (UserResource) tem dependencia de @Services (UserService) e por sua vez, Services dependem de @Repository.
Sabemos que o UserService importado para dentro do UserResource com @AutoWired foi injetada automaticamente pelo Spring.
Mas para a variável service (advinda do UserService) funcionar dentro da classe (UserResource), a classe UserService deve ser registrada como um componente do Spring.
@Component
public class UserService {}
E assim, essa classe poderá ser injetada automaticamente com o @AutoWired na outra classe:
public class UserResource {
@Autowired
private UserService service;
Diferença entre Service, Resource e Repository
Importa para si o Repository.
O service é responsável pela lógica da aplicação. Ele encapsula as operações que são específicas da aplicação, como validações, cálculos complexos, interações entre entidades de domínio, e outras regras de negócio.
É comum que os Services sejam injetados em outros componentes, como Controllers (no caso do Spring MVC) ou outros Services, para que a lógica de negócio seja executada de forma organizada e isolada. Exemplo:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// Métodos para salvar, atualizar, deletar produtos, etc.
}
Importa para si o UserService.
Resource representa um ponto de extremidade (endpoint) na sua aplicação RESTful. Ou seja, é um controlador que lida com requisições HTTP e retorna respostas, geralmente em JSON ou XML.
Um resource pode ser um controlador Spring MVC anotado com @RestController, que combina a funcionalidade de @Controller e @ResponseBody. Exemplo:
@RestController
@RequestMapping("/api/products")
public class ProductResource {
@Autowired
private ProductService productService;
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
// Outros métodos para lidar com POST, PUT, DELETE, etc.
}
O repository é uma camada de abstração que lida com o acesso a dados. Ele encapsula a lógica de acesso a banco de dados ou outra fonte. Em geral, Repository é usado para recuperar dados, salvar, atualizar e deletar registros no banco de dados.
Repositories são frequentemente implementados usando o padrão de deisgn Repository e não anotados com @Repository. Exemplo:
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Métodos para acessar dados relacionados a produtos
}
Anotações Spring @
Informa que uma classe também é uma entidade. Estabelecerá a ligação entre a entidade nomeada e uma tabela de mesmo nome no banco de dados.
Uma entidade é uma tabela no banco de dados.
Além disso, cada tabela possui um nome. Então precisamos usar a anotação @Table.
Aqui podemos especificar detalhes da tabela que serão utilizados para persistir as nossas entidades no banco de dados.
Se não usarmos essa anotação, não teremos erro. A menos que já exista uma tabela com esse nome no banco de dados.
É uma enum. Usamos para informar que geração do valor do identificador único será gerenciada pelo proprio provedor de persistência.
Usamos logo após @Id. Se não passarmos essa anotação, significa que a responsabilidade de gerar e gerir as chaves primárias, será da aplicação.
-
GenerationType.AUTO - Padrão. Deixa o provedor de persistência a escolha da estratégia mais adequada.
-
GenerationType.IDENTITY - Será um autoincrement. Os valores serão gerados para cada registro inserido no banco.
-
GenerationType.SEQUENCE - Será ingerido a partir de uma sequência. Caso não seja especificado um nome para a sequence, será utilizada uma sequence padrão, a qual será global, para todas as entidades.
-
GenerationType.TABLE - É necessário criar uma tabela para gerenciar as chaves primárias. Essa opção é pouco recomendada.
Para o Spring identificar que é uma classe de configuração.
- Essa classe de configuração ela não é nem controller, service ou repository. Ela é uma classe auxiliar que vai fazer umas configurações na aplicação.
- Criamos um package chamado config e criamos uma entidade chamada TestConfig.
- Para o Spring identificar que é uma classe de configuração, passaremos uma anottation @Configuration.
Além disso, para caracterizarmos essa classe como perfil de teste, passaremos uma anottation @Profile("test").
Essa classe TestConfig vai servir para database seeding. Ou seja, popular o banco de dados. Sabemos que para acessar/salvar coisas no banco de dados utilizamos o Repository.
@Configuration
@Profile("test")
public class TestConfig {}
Ela associa uma instância numa classe. No nosso exemplo, nós importamos para a classe TesteConfig o UserRepository e usamos essa anotação.
@Configuration
@Profile("test")
public class TestConfig implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
É um esteriótipo, suas especializações são:
- @Repository
- Pacote repositories.
- @Service
- pacote services.
- Controller/RestController
- pacote resources
Quando uma classe é anotada com @Component significa que a mesma usará o padrão de injeção de depêndencia, e será elegível para auto-configuração e auto-detecção de objeto instanciado anotados à partir de escaneamento de classpath que o IoC Container do Spring faz.
A anotação @Component deve ser usada em nível de classe, dessa forma:
@Component
public class Foo {
//implementação da classe
}
Para referenciar a mesma em outro contexto (obter a instância), pode se usar a anotação @Autowired, dessa forma:
@Component
public class Bar {
@Autowired
private Foo foo;
}
Para que os dados sejam serializados diretamente na response. Usamos @ResponseBody nos métodos. Para não ficarmos reescrevendo toda hora, utilizamos @RestController.
É util para aplicações mais tradicionais, porque você pode rendarizar templates ou serialização diretas de dados (@ResponseBody).
Agora, se a aplicação é toda API Restful, sem templates HTML, utilize @RestController.
Elimina a necessidade de ficar reescrevendo e anotando os métodos com @ResponseBody. Já que todos os dados serão serializados em JSON/XML e jogados na response.
O RestController é inserido em classes que possuem ResourceLayer. Se temos uma classe Usuario, por exemplo, teremos uma UserResource onde ele será inserido, conforme abaixo.
@RestController
@RequestMapping(value = "/users")
public class UserResource {}
Geralmente usamos nas classes Controllers, pois dessa forma, todos os métodos herdam o endereço do controller, evitando repetições.
@RequestMapping
public class HelloController {
@GetMapping("/hello")
public String olaMundo() {
return "Hello World!";
}
@GetMapping("/hello/bye")
public String olaMundo() {
return "Bye World!";
}
}
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String olaMundo() {
return "Hello World!";
}
@GetMapping("/bye")
public String olaMundo() {
return "Bye World!";
}
}
Utilizado para fazer relacionamento entre classes. Exemplo: Temos uma classe de Order e Usuários. Naturalmente, teremos várias ordens para um usuário. E um usuário para várias ordens.
- @ManyToOne - Como são várias ordens para um usuário, usamos a anotação abaixo na linha onde o usuário foi importado.
@ManyToOne @JoinColumn(name = "client_id") private User client;
- @OneToMany - Um usuário pra várias ordens. O mappedBy diz: com qual nome a classe User foi importada para a outra.
@OneToMany(mappedBy = "client")
private List<Order> orders = new ArrayList<>();
Se temos uma associação de mão dupla (muitos para um). O Jackson (biblioteca de serialização) fica chamando.
O pedido chama usuário, usuário chama pedido... e assim fica um looping.
Portanto, tem que colocar o JsonIgnore em algum dos dois lados.
- Criar projeto Spring Boot Java
- Implementar modelo de domínio
- Estruturar camadas lógicas: resource, service, repository
- Configurar banco de dados de teste (H2)
- Povoar o banco de dados
- CRUD - Create, Retrieve, Update, Delete
- Tratamento de exceções
@RestController
@RequestMapping(value = "/users")
public class UserResource {
@GetMapping
public ResponseEntity<User> findAll() {
User u = new User(1L, "Maria", "maria@gmail.com", "99999", "12345");
return ResponseEntity.ok().body(u);
}
}
-
@RequestMapping - Utilizamos ela nas classes Controllers, porque assim todos os métodos herdam o endereço do controller. Se usássemos @GetMapping, teríamos que repetir a anotação em todo método da classe.
Por exemplo, acima passamos o Mapping com o valor "/users". Portanto, todo método por padrão, irá herder esse endereço. -
@GetMapping - Feito para requisições GET, conforme utilizado no método acima.
- JPA & H2 dependencies
- application.properties
- application-test.properties
- Entity: JPA mapping
Dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
application.properties:
spring.profiles.active=test
spring.jpa.open-in-view=true
application-test.properties:
# DATASOURCE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
# H2 CLIENT
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JPA, SQL
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.defer-datasource-initialization=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Entity: JPA Mapping:
Colocar na classe User algumas anotações do JPA. Instruindo ao JPA como converter os objetos para o modelo relacional.
- Colocar @Entity em cima da classe.
- Colocar uma @Table(name = "tb_user"). Isso porque a palavra User é uma palavra reservada do banco de dados H2 então precisamos renomear para essa tabela não ter nenhum tipo de conflito.
- Settar primaryKey, nesse caso ID.
- Sabemos que ID é uma coluna autoincrementada, então colocamos @GeneratedValue(strategy = GenerationType.IDENTITY).
@Entity
@Table(name = "tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- UserRepository extends JPARepository<User, Long>
- Configuration class for "test" profile
- @Autowired UserRepository
- Instantiate objects in memory
- Persist objects
UserRepository
UserRepository será responsável por fazer operações com a entidade User. Para criamos, faremos o UserRepository extender o JPARepository, passando o tipo da Entidade que vamos acessar e o tipo da chave.
- Criamos um package chamado repositories dentro de course e criamos o UserRepository (que será uma interface);
- Dentro da interface criada, extendemos passando o JpaRepository e parâmetros;
public interface UserRepository extends JpaRepository <User, Long > {
}
Configuration class for "test" profile
Essa classe de configuração ela não é nem controller, service ou repository. Ela é uma classe auxiliar que vai fazer umas configurações na aplicação.
- Criamos um package chamado config e criamos uma entidade chamada TestConfig.
- Para o Spring identificar que é uma classe de configuração, passaremos uma anottation @Configuration. Além disso, para caracterizarmos essa classe como perfil de teste, passaremos uma anottation @Profile.
@Configuration
@Profile("test")
public class TestConfig {
}
@Autowired UserRepository
Essa classe TestConfig vai servir para database seeding. Ou seja, popular o banco de dados. Sabemos que para acessar/salvar coisas no banco de dados utilizamos o Repository.
Portanto, teremos o nosso primeiro caso de injeção de dependência.
Para fazermos o Spring entender que o objeto dependerá de outro, importar o UserRepository:
@Autowired
private UserRepository userRepository;
A anotação @AutoWired irá associar uma instância de UserRepository dentro de TestConfig através do Spring.
Instantiate objects in memory
O TestConfig irá implementar a interface CommandLineRunner.
public class TestConfig implements CommandLineRunner {,
@Override
public void run(String... args) throws Exception {
User u1 = new User(null, "Maria Brown", "maria@gmail.com", "988888888", "123456");
User u2 = new User(null, "Alex Green", "alex@gmail.com", "977777777", "123456");
}
}
Persist Objects (Saving)
Tudo dentro do método run será executado quando a operação for iniciada (no programa principal -CourseApplication-).
- Instanciamos os objetos desejados;
- E depois chamamos userRepository.saveAll(e aqui dentro, passamos um Arrays inserindo os objetos acima).
@Override
public void run(String... args) throws Exception {
User u1 = new User(null, "Maria Brown", "maria@gmail.com", "988888888", "123456");
User u2 = new User(null, "Alex Green", "alex@gmail.com", "977777777", "123456");
userRepository.saveAll(Arrays.asList(u1, u2));
}
}
Sabemos que o UserService importado para dentro do UserResource com @AutoWired foi injetada automaticamente pelo Spring.
Mas para a variável service (advinda do UserService) funcionar dentro da classe (UserResource), a classe UserService deve ser registrada como um componente do Spring.
Para fazer isso usamos uma Annotation:
@Component
public class UserService {}
E assim, essa classe poderá ser injetada automaticamente com o @AutoWired na outra classe:
public class UserResource {
@Autowired
private UserService service;
@Repository - Registra um repository.
@Service - Registra um serviço na camada de serviço.
Como a nossa classe é uma classe de serviço, daremos preferência para essa annotation.
@Service
public class UserService {
Faz a implementação do método.
Esse método vai retornar um User.
Para recuperarmos esse User por Id, usaremos novamente o repository.findById(id) e alocaremos esse Id em um Optional.
public User findById(Long id) {
Optional<User> obj = repository.findById(id);
return obj.get();
}
o obj.get retorna o objeto do tipo que especificamos, neste caso, User.
Criamos a função, mas no Mapping passamos um value:
@GetMapping(value = "/{id}")
public ResponseEntity<User> findById() {}
Isso indica que a nossa requisição vai aceitar um id dentro da URL.
O nosso parâmetro dentro da função vai ser exatamente o que está dentro do value.
Para o Spring aceitar esse id e considerar ele como parâmetro, a gente coloca uma Anottation: @PathVariable
@GetMapping(value = "/{id}")
public ResponseEntity<User> findById(@PathVariable Long id) {}
O @PathVariable ele serve para usarmos como parâmetro o que está dentro das chaves {}.
Basic new entity checklist:
- Basic new entity checklist:
- Entity
- "To many" association, lazy loading, JsonIgnore
- Entity
- Repository
- Seed
- Service
- Resource
Como fazer o relacionamento entre Order e User?
Bom, a nossa situação é: muitas orders para um só Usuário. E um Usuário para muitas ordens.
Na importação do User dentro de Orders colocamos a Anottation: @ManyToOne e @JoinColumn.
@ManyToOne
@JoinColumn(name = "client_id")
private User client;
Na importação do Order dentro de User colocamos a Anottation: @OneToMany e dentro passos o mappedBy. O mappedBy basicamente, diz como o User foi nomeado dentro da outra classe.
@OneToMany(mappedBy = "client")
private List<Order> orders = new ArrayList<>();
O @PathVariable ele serve para usarmos como parâmetro o que está dentro das chaves {}.
Quando nós criamos as variáveis Enums, elas ficam em sequência: 0, 1, 2, 3...
Para que eventualmente alguem não chegue no código e adicione um novo Enum no meio dos outros, mudando a ordem, precisamos criar uma variavel do tipo int, um construtor e um método get.
public enum OrderStatus {
WAITING_PAYMENT(1),
PAID(2),
SHIPPED(3),
DELIVERED(4),
CANCELED(5);
private int code;
private OrderStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
Criaremos também um método estático para converter um valor numérico para tipo Enum.
public static OrderStatus valueOf(int code) {
for (OrderStatus value : OrderStatus.values()) {
if (value.getCode() == code) {
return value;
}
}
throw new IllegalArgumentException("Invalid OrderStatus code");
}
}
Um simples for loop testando se o valor da classe Enum (value.getCode() é igual ao code passado como parâmetro).
Na classe Order, nós vamos fazer uma pequena alteração. Ao invés da variável ser do tipo OrderStatus, ela será do tipo integer.
antes
private OrderStatus orderStatus;
depois
private Integer orderStatus;
O que muda? O construtor retornará um setOrderStatus.
public Order(Long id, Instant moment, OrderStatus orderStatus, User client) {
this.id = id;
this.moment = moment;
setOrderStatus(orderStatus);
this.client = client;
}
Os métodos get e set, continuarão a retornar OrderStatus, mas ficarão assim:
private Integer orderStatus;
public OrderStatus getOrderStatus() {
return OrderStatus.valueOf(orderStatus);
}
Como agora na classe lá em cima está como Integer, nós passamos o orderStatus (agora intenger),
como parâmetro no método criando na classe OrderStatus :)
public void setOrderStatus(OrderStatus orderStatus) {
if (orderStatus!= null) {
this.orderStatus = orderStatus.getCode();
}
}
Já no método set, nós simplesmente pegamos na classe OrderStatus o getCode().
Aqui foi só criar a classe na entidade, depois o resource e service reaproveitando o que já foi aprendido juntamente com as anotações :).
Usaremos Set no link entre Categories e Products para garantir que nenhum produto tenha uma categoria mais de uma vez ou vice-versa.
public class Product implements Serializable {
private Set<Category> categories = new HashSet<>();
}
public class Category implements Serializable {
private Set<Product> products = new HashSet<>();
}
Bom, como um produto pode ter várias categorias e uma categoria pode ter vários produtos...
Primeiramente nós vamos escolher uma das duas classes, neste caso, será Product.
-
Passar a anotação @ManyToMany e @JoinTable.
-
Na JoinTable, passaremos o nome da tabela e quais as chaves estrangeiras que vão associar a tabela de Produto com Categoria.
-
Ao colocarmos name, criamos a tabela de associação.
E precisamos falar também qual será o nome da chave estrangeira, referente a tabela de Product.
Para fazermos isso, é só passar o joinColums = @JoinColumn(name = "nome da chave estrangeira") do Product.
Nós sabemos que na tabela de associação ela vai ter chaves estrangeiras das duas tabelas, neste caso, Product e Category.
Nós usaremos o inverseJoinColumns para definirmos a chave estrangeira da outra entidade (Category).
@ManyToMany
@JoinTable(name = "tb_product_category",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
private Set<Category> categories = new HashSet<>();
E agora precisamos ir à classe Category, e colocar uma referência para o mapeamento que acabamos de fazer!
@ManyToMany(mappedBy = "categories")
private Set<Product> products = new HashSet<>();
E para associarmos um produto a uma categoria? Primeiro, vamos a classe TestConfig.
- Acessaremos o product, pegando sua categorie e adicionando a categoria.
p1.getCategories().add(cat2)
Selecionamos a classe a ser utilizada.
O JoinTable(name), cria a nova tabela. O JoinColumns cria uma coluna com o nome passado. O inverseJoinColumns é o nome da outra entidade não usada.
Na outra classe, usamos mappedby colocando o nome da outra coleção "Set", que é categories.