Programação para Web II

Fagno Alves Fonseca <fagno.fonseca@ifto.edu.br> Mestre em Modelagem Computacional de Sistemas – UFT.

1. Java Persistence API e Frameworks ORM

A Java Persistence API (JPA) é uma especificação para persistência de dados em Java, que oferece uma API de mapeamento objeto-relacional e soluções para integrar persistência em sistemas corporativos. Ela foi criada com o intuito de ter uma padronização no desenvolvimento de soluções com ORM em Java.

Mapeamento objeto relacional (object-relational mapping, ORM) é uma técnica de programação para conversão de dados entre banco de dados relacionais e linguagens de programação orientada a objetos. Entre os principais framework ORM, podemos destacar Hibernate, TopLink, EclipseLink, entre outros.

1.1. Dependências

Após criar o projeto web com maven utilizando o Spring Boot, adicione ao arquivo pom.xml as dependências a seguir.

Dependência
<!-- Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- MySQL JDBC and R2DBC driver -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
Você pode adicionar as dependências ao criar o projeto no Spring Boot ou adicionar caso o já exista.

1.2. Mapeamento básico

Para efetuar o mapeamento objeto/relacional de uma classe, precisamos inserir a anotação @Entity em uma classe.

Criando uma entidade
import javax.persistence.Entity;

@Entity
public class Pessoa{
}

A anotação @Entity diz que a classe é uma entidade, que representa uma tabela do banco de dados.

Precisamos inserir também as anotações @Id, que declara o identificador no banco de dados e a anotação @GeneratedValue, que determina esse identificador sendo do tipo auto-incremento, neste caso, o atributo 'id'.

Entidade Pessoa
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "tb_pessoa")
public class Pessoa implements Serializable{

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private int id;
    private String nome;
    private int idade;

    //getters e setters

}

A classe possui a anotação @Table(name = "tb_pessoa"), que define o nome da tabela ao persistir os dados no banco. Caso você não utilize a anotação a tabela terá o mesmo nome da classe, ou seja, 'Pessoa'.

Em JPA é uma boa pratica sempre implementar a interface Serializable. Quando o objeto é serializado, todas as variáveis de instância referentes a classe deste objeto serão serializadas. A serialização significa salvar o estado atual dos objetos em arquivos em formato binário para o seu computador, sendo assim esse estado poderá ser recuperado posteriormente recriando o objeto em memória assim como ele estava no momento da sua serialização.

  • Principais anotações:

1.3. O arquivo application.properties

Neste arquivo são informadas as propriedades a serem utilizadas na nossa aplicação, neste caso, as configurações de conexão com nosso banco de dados, entre outras configurações da JPA. Este arquivo já é definido por padrão ao criar o projeto no diretório src/main/resources da aplicação.

application.properties
##MYSQL
spring.datasource.url= jdbc:mysql://localhost:3306/BANCO_DADOS?createDatabaseIfNotExist=true
spring.datasource.username= USUARIO
spring.datasource.password= SENHA

##JPA
spring.jpa.hibernate.ddl-auto= update
spring.jpa.show-sql= true

No arquivo 'application.properties' as propriedades de conexão com o banco de dados são definidas utilizando as principais propriedades abaixo:

  • spring.datasource.url: descrição da URL de conexão com o banco de dados

  • spring.datasource.username: nome do usuário do banco de dados

  • spring.datasource.password: senha do usuário do banco de dados

Outras propriedades do Hibernate são inseridas no arquivo conforme descrito abaixo.

  • spring.jpa.show-sql: informa se os comandos SQL devem ser exibidos na console (importante para debug, mas deve ser desabilitado em ambiente de produção)

  • spring.jpa.hibernate.ddl-auto: cria ou atualiza automaticamente a estrutura das tabelas no banco de dados.

Pronto! Para gerar a tabela no banco de dados, basta apenas executar o projeto.

2. EntityManager

Um EntityManager é responsável por gerenciar entidades no contexto de persistência. Através dos métodos dessa interface, é possível persistir, pesquisar e excluir objetos do banco de dados.

Para criar um EntityManager necessitamos de uma instância de EntityManagerFactory. No entanto, em nossos exemplos o Spring tem a responsabilidade de gerenciar as dependências que temos de um EntityManager, ou seja, não precisamos nos preocupar com a criação do EntityManager. Temos apenas que indicar ao Spring que o faça através de anotações.

O Spring controla todo o ciclo da vida do EntityManagerFactory e do EntityManager, bem como suas transações, através da Injeção de dependências.

3. Inversão de controle e Injeção de dependência

Injeção de dependências (ou Dependency Injection – DI) é um tipo de inversão de controle (ou Inversion of Control – IoC), processo responsável por fazer o controle da injeção de dependências nas aplicações.

A Inversão de Controle permite a outro elemento o controle e responsabilidade sobre como e quando um objeto deve ser criado.

A injeção de dependência define as dependências declaradas de uma maneira segura e dinâmica. Injeção de dependência é tornar disponível a instância de uma classe quando for necessário. Desta forma, passamos a responsabilidade de criar instâncias para o container.

3.1. Escopos de componentes

O escopo de um bean define o ciclo de vida e a visibilidade desse bean nos contextos em que é usado. Através da injeção de dependência, indicamos como os componentes serão criados, para que o nosso container possa fazer isso quando necessário.

  • Singleton (padrão): um bean com escopo singleton indica ao container criar uma única instância de bean. Este escopo é o valor padrão se nenhum outro escopo for especificado.

  • Prototype: um bean com escopo prototype indica uma instância diferente toda vez que for solicitado ao contêiner.

Existem quatro escopos adicionais disponíveis apenas em um contexto de aplicativo com reconhecimento da Web.

  • Request: cria uma instância de bean para uma única solicitação HTTP, ou seja, cada solicitação HTTP tem sua própria instância de um bean criada.

  • Session: cria uma instância de bean única para uma Sessão HTTP.

  • Application: cria uma instância única do bean para o ciclo de vida de um ServletContext.

  • websocket: cria uma única definição de bean para o ciclo de vida de um WebSocket.

3.2. Definindo um Repository

Criaremos a classe PessoaRepository para acesso aos dados da entidade Pessoa.

Classe PessoaRepository
...
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.springframework.stereotype.Repository;

@Repository
public class PessoaRepository {

    @PersistenceContext
    private EntityManager em;

    public void save(Pessoa pessoa){
        em.persist(pessoa);
    }

    public Pessoa pessoa(Long id){
        return em.find(Pessoa.class, id);
    }

    public List<Pessoa> pessoas(){
        Query query = em.createQuery("from Pessoa");
        return query.getResultList();
    }

    public void remove(Long id){
        Pessoa p = em.find(Pessoa.class, id);
        em.remove(p);
    }

    public void update(Pessoa pessoa){
        em.merge(pessoa);
    }
}

A anotação @Repository definida em nossa classe, indica que esta classe se trata de um repositório, ou seja, um componente responsável pelo acesso a dados em algum mecanismo de persistência, neste exemplo, um banco de dados.

O atributo do tipo EntityManager anotado com @PersistenceContext informa ao container do Spring a responsabilidade de gerenciar a dependência que temos de um EntityManager.

3.3. Alterando nosso Controller

Devemos alterar nosso controller do exemplo anterior de modo que a instância da nossa classe PessoaRepository sejá controlada pelo container. Com a anotação @Autowired o Spring fará a injeção dessa dependência no momento que o controller for criado.

Incluimos também no nosso controller a anotação @Transactional, fazendo com que o Spring assuma a responsabilidade em gerenciar as transações.

Classe PessoasController
...
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.beans.factory.annotation.Autowired;

@Transactional
@Controller
@RequestMapping("pessoas")
public class PessoasController {

    @Autowired
    PessoaRepository repository;
    ...

}

4. Mapeamento com associações

4.1. um-para-um

O relacionamento um-para-um, também conhecido como one-to-one. Neste relacionamento teremos os atributos das entidades relacionadas que serão persistidas na mesma tabela. Como exemplo, iremos utilizar uma classe Pessoa que tem um relacionamento One-to-One com a classe endereço.

one to one
Figura 1. @OneToOne

A implementação do exemplo acima é descrito a seguir. Na classe Pessoa, adicionamos o atributo endereco e e mapeamos com @OneToOne. Adicionamos também a anotação @JoinColumn para definir o nome da coluna que faz referência ao id da tabela tb_endereco na tabela tb_pessoa.

Por padrão, o nome da coluna é definido com o nome do atributo da associação, mais underscore, mais o nome do atributo do identificador da entidade destino caso você não utilize o @JoinColumn.

Classe Pessoa
...
@Entity
@Table(name = "tb_pessoa")
public class Pessoa{

    @Id
    @GeneratedValue
    private int id;

    private String nome;

    private int idade;

    @OneToOne
    @JoinColumn(name = "id_endereco")
    private Endereco endereco;

    //getters e setters

}
Classe Endereco
...
@Entity
@Table(name = "tb_endereco")
public class Endereco{

    @Id
    @GeneratedValue
    private int id;

    private String logradouro;

    private String bairro;

    private String cep;

    //getters e setters

}
Não esqueça que precisamos de uma instância persistida de Endereço para atribuir a Pessoa.

4.1.1. Associação bidirecional

A associação do exemplo entre pessoa e endereço é unidirecional, ou seja, podemos obter o endereço a partir de uma pessoa, mas não conseguimos obter a pessoa a partir de um endereco.

one to one bi
Figura 2. @OneToOne bidirecional

Para tornar a associação um-para-um bidirecional, precisamos apenas incluir o atributo de Pessoa na classe Endereco e mapearmos com @OneToOne usando o atributo mappedBy.

Classe Endereco
...
@Entity
@Table(name = "tb_endereco")
public class Endereco{

    @Id
    @GeneratedValue
    private int id;

    private String logradouro;

    private String bairro;

    private String cep;

    @OneToOne(mappedBy="endereco")
    private Pessoa pessoa;

    //getters e setters

}
O valor do mappedBy deve ser igual ao nome do atributo de endereço definido na classe Pessoa.

Em relacionamentos OneToOne, qualquer um dos lados pode ser o dominante. O mappedBy deve ser colocado na classe que não é dona do relacionamento, ou seja, o lado fraco. Neste exemplo, definimos Pessoa como classe dominante. Na tabela tb_endereco não terá id da tabela tb_pessoa.

4.2. um-para-muitos

Neste exemplo, vamos alterar nosso relacionamento para que uma pessoa tenha vários endereços e um endereço só tenha uma pessoa. A anotação @OneToMany deve ser utilizada para mapear coleções, neste caso, inserida do lado não dominante (fraco).

one to many bi
Figura 3. @OneToMany bidirecional
Classe Pessoa
...
@Entity
@Table(name = "tb_pessoa")
public class Pessoa{

    @Id
    @GeneratedValue
    private int id;

    private String nome;

    private int idade;

    @OneToMany(mappedBy = "pessoa")
    private List<Endereco> enderecos;

    //getters e setters

}
Classe Endereco
...
@Entity
@Table(name = "tb_endereco")
public class Endereco{

    @Id
    @GeneratedValue
    private int id;

    private String logradouro;

    private String bairro;

    private String cep;

    @ManyToOne
    @JoinColumn(name = "id_pessoa")
    private Pessoa pessoa;

    //getters e setters

}

4.3. muitos-para-muitos

Neste exemplo, vamos alterar nosso relacionamento para que uma pessoa tenha vários endereços e um endereço tenha várias pessoas. Usamos a anotação @ManyToMany para mapear a propriedade de coleção.

No relacionamento muitos para muitos é criada uma tabela de associação com os nomes das entidades relacionadas, separados por underscore, com duas colunas, com nomes dos identificadores gerados automaticamente.

Podemos customizar o nome da tabela de associação e das colunas com a anotação @JoinTable, conforme exeplo a seguir.

many to many
Figura 4. @OneToMany bidirecional
Classe Pessoa
...
@Entity
public class Pessoa implements Serializable{

    @Id
    @GeneratedValue
    private int id;

    private String nome;

    private int idade;

    @ManyToMany
    @JoinTable(name = "pessoas_enderecos",
    joinColumns = @JoinColumn(name = "id_pessoa"),
    inverseJoinColumns = @JoinColumn(name = "id_endereco"))
    private List<Endereco> enderecos=new ArrayList();

    //getters e setters

...

No exemplo acima, foi definido com a anotação @JoinTable o nome da tabela que será gerada e também com a anotação @JoinColumn o nome das colunas que representam os identificadores da associação entre pessoa e endereço.

Na classe endereço vamos utilizar a anotação @ManyToMany, porém apontando apenas o "mappedby" conforme a seguir.

Classe Endereco
...
@Entity
public class Endereco implements Serializable{

    @Id
    @GeneratedValue
    private int id;

    private String logradouro;

    private String bairro;

    private String cep;

    @ManyToMany(mappedBy = "enderecos")
    private List<Pessoa> pessoas=new ArrayList();

    //getters e setters

...

Um nova tabela será gerada de acordo a nomeclatura definida nas anotações e apresentada na figura a seguir.

tabela n to n
Figura 5. tabela pessoas_enderecos

5. Mapeamento com Herança

A JPA define 3 formas de se fazer o mapeamento de herança:

  • Tabela Única por Hierarquia de Classes (single table)

  • Uma tabela para cada classe da hierarquia (joined)

  • Uma tabela para cada classe concreta (table per class)

5.1. Tabela Única por Hierarquia de Classes

Essa estratégia de mapeamento define uma unica tabela para toda a hierarquia de classes. Por exemplo, considere a Classe Pessoa como Superclasse, e outras 2 classes PessoaFisica e PessoaJuridica estendendo de Pessoa, teremos uma tabela com os dados de toda a hierarquia.

Pontos importantes:

  • As classes filhas precisam aceitar valores nulos. A falta da constraint NOT NULL pode ser um problema.

A identificaçao de quem é Pessoa Física ou Jurídica é feita por um atributo chamado discriminator, ou seja, um campo que identifica PessoaFisica(PF) e PessoaJuridica (PJ).

Classe Pessoa
...
@Entity
@Table(name = "tb_pessoa")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "tipo")
public abstract class Pessoa {

    @Id
    @GeneratedValue
    private Long id;

    private String nome;

    //getters e setters

...

A estratégia é definida com a anotação @Inheritance, neste caso SINGLE_TABLE. A anotação @DiscriminatorColumn foi usada para definir o nome da coluna do discriminator e identificar se é PF ou PJ.

A seguir vamos definir a classe PessoaFisica com a anotação @DiscriminatorValue indicando o discriminador como "F".

Classe PessoaFisica
...
@Entity
@DiscriminatorValue("F")
public class PessoaFisica extends Pessoa{

    private String cpf;

    //getters e setters

...

A seguir vamos definir a classe PessoaJuridica com a anotação @DiscriminatorValue indicando o discriminador como "J".

Classe PessoaJuridica
...
@Entity
@DiscriminatorValue("J")
public class PessoaJuridica extends Pessoa{

    private String cnpj;

    //getters e setters

...

5.2. Uma tabela para cada classe da hierarquia

Essa estratégia de mapeamento define uma tabela para cada classe da hierarquia. No nosso exemplo, teremos uma tabelas para a classe Pessoa, uma tabela para PessoaFisica e uma para PessoaJuridica.

Alteramos o exemplo anterior, definir a estratégia de herança para JOINED na entidade Pessoa.

Classe Pessoa
...
@Entity
@Table(name = "tbpessoa")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Pessoa {

    @Id
    @GeneratedValue
    private Long id;

    private String nome;

    //getters e setters

...

Nas subclasses, vamos adicionar a anotação @PrimaryKeyJoinColumn para informar o nome da coluna que faz referência à superclasse.

Classe PessoaFisica
...
@Entity
@Table(name = "tb_pessoafisica")
@PrimaryKeyJoinColumn(name = "id_pessoa")
public class PessoaFisica extends Pessoa{

    private String cpf;

    //getters e setters

...
Classe PessoaJuridica
...
@Entity
@Table(name = "tb_pessoajuridica")
@PrimaryKeyJoinColumn(name = "id_pessoa")
public class PessoaJuridica extends Pessoa{

    private String cnpj;

    //getters e setters

...

5.3. Uma tabela para cada classe concreta

Essa estratégia de mapeamento define tabelas apenas para classes concretas (subclasses). Cada tabela deve possuir as propriedades da subclasse incluindo as da superclasse.

Para utilizar essa forma de mapeamento, devemos anotar a classe conforme a seguir.

Classe Pessoa
...
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Pessoa {

    @Id
    @GeneratedValue(generator = "inc")
    @GenericGenerator(name = "inc", strategy = "increment")
    private Long id;

    private String nome;

    //getters e setters

...

Tivemos que mudar a estratégia de geração de identificadores. Não podemos usar a geração automática de chaves nativa do banco de dados.

Nas subclasses, definimos conforme a seguir.

Classe PessoaFisica
...
@Entity
@Table(name = "tb_pessoafisica")
public class PessoaFisica extends Pessoa{

    private String cpf;

    //getters e setters

...
Classe PessoaJuridica
...
@Entity
@Table(name = "tb_pessoajuridica")
public class PessoaJuridica extends Pessoa{

    private String cnpj;

    //getters e setters

...

6. Referências