Vamos aprender como configurar o Spring Data em conjunto com o Spring MVC, utilizar anotações para controle transacional, usar spectations no Spring Data para termos uma DSL de nossas consultas
Nesse projeto utilizaremos a versão 4.X.X do Spring, Maven para resolução de dependências, e o Tomcat como container
Não vamos entrar na criação do projeto Maven, suponho que já possua o conhecimento, e já tenha o projeto criado, caso não pode dar uma olhada aqui
As dependências que vamos utilizar são as seguintes
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.10.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.2.125</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.2.2.Final</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
Usaremos o Jackson para serialização de nossa entidade em JSON, o hibernate como implementação do JPA, o banco de dados será em memória utilizando o H2
Vamos começar configurando nosso projeto, vamos criar a classe SpringConfig, no pacote "me.efraimgentil.springmvcanddata.config", essa classe irá contér as configurações básicas de beans do Spring
package me.efraimgentil.springmvcanddata.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = {"me.efraimgentil.springmvcanddata" })
public class SpringConfig {
}
Nessa classe estamos apenas sinalizando ao Spring, por meio da anotação ComponentScan, para escanear a partir do pacote raiz em busca dos components do Spring
Em seguida temos a configuração do Spring MVC, para gerenciar nossas rotas
package me.efraimgentil.springmvcanddata.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "me.efraimgentil.springmvcanddata.controller" })
public class SpringMvcConfig extends WebMvcConfigurerAdapter {
}
Aqui, também não temos nada novo, estamos habilitando o MVC com a anotação EnableWebMvc, ela é basicamente o equivalente a configuração xml '
Vamos configurar agora nossa base de dados, controle transacional e os repositórios com Spring Data
package me.efraimgentil.springmvcanddata.config;
import java.util.Properties;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "me.efraimgentil.springmvcanddata.repository"
, entityManagerFactoryRef="entityManagerFactory"
, transactionManagerRef="transactionManager")
public class PersistenceConfig {
@Bean
public DataSource datasource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName( "org.h2.Driver");
ds.setUrl("jdbc:h2:mem:data;DB_CLOSE_DELAY=-1");
ds.setUsername("sa");
ds.setPassword("");
return ds;
}
@Bean(name= "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource ds ) {
LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean ();
emfb.setDataSource(ds);
emfb.setPackagesToScan("me.efraimgentil.springmvcanddata.domain");
emfb.setPersistenceProviderClass(HibernatePersistenceProvider.class);
Properties jpaProterties = new Properties();
jpaProterties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
jpaProterties.put("hibernate.format_sql", true );
jpaProterties.put("hibernate.show_sql", true);
jpaProterties.put("hibernate.hbm2ddl.auto", "update");
emfb.setJpaProperties(jpaProterties);
return emfb;
}
@Bean(name="transactionManager")
public JpaTransactionManager transactionmanager(EntityManagerFactory emf){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory( emf );
return transactionManager;
}
}
Essa configuração é mais extensa, primeiro temos a anotação @EnableTransactionManagement ela irá permitir que utilizemos a anotação @Transactional em nossos métodos para sinalizar que serão rodados dentro de uma transação, em seguida temos a anotação @EnableJpaRepositories, ela habilita os repositórios do Spring Data, especificamos na anotação em que pacotes esses repositórios residirão.
Temos o nosso datasource, estamos fazendo uma configuração simples utilizando o DriverManagerDataSource, não utilize essa configuração em produção, pois a cada chamada do getConnection será criada uma nova conexão, para simplificar o exemplo estamos utilizando essa implementação.
Temos também a configuração do nosso EntityManagerFactory, aqui apenas configuramos o datasource que será utilizado, o pacote onde nossas entidades estão residindo, o provider que será utilizado no caso HibernatePersistenceProvider, e configuramos o hibernate.
Por fim temos a configuração do nosso transactionManager, aqui utilizamos o JpaTransactionManager, e apontamos para o nosso EntityManagerFactory configurado anteriormente
Veja que na assinatura dos métodos apontam para o DataSource e o EntityManagerFactory, não se preocupe, o Spring vai tratar da injeção desses objetos para você
Com isso vamos finalizar a configuração criando nosso WebInitializer como a seguir
package me.efraimgentil.springmvcanddata.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { SpringConfig.class , PersistenceConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { SpringMvcConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
Com isso temos nosso projeto configurado e pronto para rodar, agora vamos criar nossa entidade User e nosso repositório UserRepository como a seguir:
package me.efraimgentil.springmvcanddata.domain;
// imports
@Entity
@Table(name="user")
public class User implements Serializable {
private static final long serialVersionUID = -5613292949395788401L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
@JsonIgnore
private char[] password;
private String role;
private Date createdAt;
public User() { }
public User(String name, char[] password) {
super();
this.name = name;
this.password = password;
this.createdAt = new Date();
}
public User(String name, char[] password , String role ) {
this( name , password );
this.role = role;
}
public User(String name, char[] password , String role , boolean active ) {
this( name , password , role);
this.active = active;
}
/* gets sets */
}
package me.efraimgentil.springmvcanddata.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import me.efraimgentil.springmvcanddata.domain.User;
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
}
Não precisamos anotar nosso repositório com nenhuma anotação, já marcamos em nossas configurações qual package escanear, o Spring vai verificar a interface e reconhecer que se trata de um repositório, já que a interfece está extendẽndo de JpaRepository
Ok agora vamos criar nosso controller, que irá servir nossa entidade como JSON, veja a seguir
package me.efraimgentil.springmvcanddata.controller;
import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import me.efraimgentil.springmvcanddata.domain.User;
import me.efraimgentil.springmvcanddata.repository.UserRepository;
import static org.springframework.data.jpa.domain.Specifications.*;
@Controller
@RestController
public class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = { "/" })
public List<User> allUsers(){
return repository.findAll();
}
@RequestMapping(value = { "/{id}" })
public User user( @PathVariable("id") Integer id ){
return repository.findOne(id);
}
@RequestMapping(value = { "/active" })
public List<User> allActiveUsers( ){
return repository.findAll(
where( (root, query, cb) -> { return cb.equal( root.get("active"), Boolean.TRUE ); } ) );
}
@RequestMapping(value = { "/with-role/{role}" })
public List<User> allUsersWithRole( final @PathVariable("role") String role ){
return repository.findAll(
where( (root, query, cb) -> { return cb.equal( root.get("role"), role ); } ) );
}
@RequestMapping(value = { "/active/with-role/{role}" })
public List<User> allActiveUsersWithRole( final @PathVariable("role") String role ){
return repository.findAll(
where( (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> { return cb.equal( root.get("active"), Boolean.TRUE ); } )
.and( (root, query, cb) -> { return cb.equal( root.get("role"), role ); } )
);
}
}
Nos injetamos a interface repository, que irá tratar nossas requisições a base de dados, não se preocupe com a implementação da interface, o Spring Data vai cuidar disso para você, e fornecer alguns métodos padrões necessários para toda aplicação, como save, delete, find etc...
Note o método allActiveUsers no controller, nele estamos fazendo uma chamada ao método findAll do repositório, e passando como parâmetro a chamada where(...), que recebe como parâmetro um Specification, no nosso caso estamos passando um lambda expression e estamos deixando o Java cuidar do resto, o retorno da nossa expressão lambda DEVE retornar uma Prediction ou null ( null simplesmente será ignorado )
O método where esta vindo de um import static org.springframework.data.jpa.domain.Specifications.*;
Perceba que é bem prático utilizar as Specifications, porém como pode ser visto no método allActiveUsersWithRole, com mais critérios para consulta, fica mais complexo ler e entender o que está acontecendo, por isso vamos criar uma classe chamada UserSpecs como a seguir
package me.efraimgentil.springmvcanddata.domain.specs;
import org.springframework.data.jpa.domain.Specification;
import me.efraimgentil.springmvcanddata.domain.User;
public class UserSpecs {
public static Specification<User> active(){
return (root , query , cb) -> {
return cb.equal( root.get("active"), Boolean.TRUE );
};
}
public static Specification<User> withRole(final String role){
return (root , query , cb) -> {
return cb.equal( cb.upper( root.get("active") ), role.toUpperCase() );
};
}
}
Com isso podemos usar esses métodos com nomes mais intuitivos, e até encadear suas chamadas, criando assim uma DSL própria do nosso sistema, claro que aqui estamos fazendo um exemplo simples, mas é possível trabalhar com coisas bem mais complexas como sub-queries. Veja a seguir como fica o nosso controller
/* imports */
import static org.springframework.data.jpa.domain.Specifications.*;
import static me.efraimgentil.springmvcanddata.domain.specs.UserSpecs.*;
@Controller
@RestController
public class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = { "/" })
public List<User> allUsers(){
return repository.findAll();
}
@RequestMapping(value = { "/{id}" })
public User user( @PathVariable("id") Integer id ){
return repository.findOne(id);
}
@RequestMapping(value = { "/active" })
public List<User> allActiveUsers( ){
return repository.findAll( active() );
}
@RequestMapping(value = { "/with-role/{role}" })
public List<User> allUsersWithRole( @PathVariable("role") String role ){
return repository.findAll( withRole( role ) );
}
@RequestMapping(value = { "/active/with-role/{role}" })
public List<User> allActiveUsersWithRole( final @PathVariable("role") String role ){
return repository.findAll( where( active() ).and( withRole(role) ) );
}
}
Veja que temos uma API bem mais fluida, e facilita bastante a leitura do que estamos buscando, na tradução literal seria como "busque tudo onde ativo e com perfil especificado"
Agora basta fazer o deploy no tomcat e começar a experimentar, o UserController está mapeado para a raiz do projeto então provavelmente você vai acessar como na url a seguir "http://localhost:8080/springmvcanddata/". Faça seus próprios testes, crie uma entidade ou tente pegar uma implementação do trabalho e mudar para Spring Data, vai ser divertido!
Você pode encontrar os fontes do projeto aqui