Aula 12: CRUD Completo com Jakarta Faces
Construa operações CRUD completas com Jakarta Faces, implementando um sistema de gerenciamento de filmes para o CritiqueHub com validação de dados, persistência JPA e interface interativa usando PrimeFaces.
Objetivos
- Implementar um CRUD completo (Create, Read, Update, Delete) com Jakarta Faces
- Integrar PrimeFaces, JPA (Panache) e Bean Validation em uma aplicação coesa
- Compreender o escopo
@ViewScopede por que ele é ideal para operações CRUD - Construir interfaces ricas com DataTable, diálogos de confirmação e feedback visual
- Comparar na prática os escopos
@ViewScopedvs@RequestScoped
Conteúdo
Contexto e Motivação
Após compreender os fundamentos do Jakarta Faces, é hora de aplicá-los em um cenário real e completo: a construção de um sistema CRUD (Create, Read, Update, Delete). Este é um dos padrões mais comuns no desenvolvimento de aplicações empresariais, permitindo que usuários criem, visualizem, atualizem e removam dados de forma intuitiva e segura.
Neste capítulo, vamos implementar o gerenciamento de Filmes para a plataforma CritiqueHub. Este módulo será essencial para que administradores da plataforma possam manter o catálogo de filmes atualizado, permitindo que usuários avaliem e comentem sobre os filmes cadastrados.
CRUD é um padrão universal no desenvolvimento de software. Dominar sua implementação com Jakarta Faces é fundamental para construir aplicações corporativas robustas e escaláveis.
A implementação de um CRUD completo exige a integração de diversos conceitos:
- Persistência de Dados: Uso de JPA para armazenar e recuperar informações do banco de dados
- Validação: Garantir que apenas dados válidos sejam persistidos
- Interface Interativa: Proporcionar experiência fluida com AJAX e componentes ricos
- Escopos Adequados: Gerenciar o ciclo de vida dos beans para performance e consistência
- Transações: Assegurar integridade dos dados durante operações de escrita
| Funcionalidade | Importância no CritiqueHub |
|---|---|
| Cadastro de Filmes | Permite que administradores adicionem novos filmes ao catálogo |
| Listagem de Filmes | Exibe todos os filmes cadastrados com opções de ação |
| Edição de Filmes | Atualiza informações de filmes já existentes |
| Remoção de Filmes | Remove filmes obsoletos ou duplicados do sistema |
| Busca e Filtro | Facilita localização de filmes específicos no catálogo |
O que é CRUD?
CRUD é um acrônimo para as quatro operações básicas de persistência de dados:
- Create (Criar): Inserir novos registros no banco de dados
- Read (Ler): Consultar e exibir registros existentes
- Update (Atualizar): Modificar registros existentes
- Delete (Deletar): Remover registros do banco de dados
View Scope no Jakarta Faces
O View Scope (@ViewScoped) é um escopo especial que mantém o estado do bean enquanto o usuário permanece na mesma página. Isso é ideal para operações CRUD porque:
- Preserva dados durante múltiplas requisições AJAX
- Mantém o estado do formulário durante validações
- Evita perda de dados durante operações interativas
- Oferece melhor performance que Session Scope
Use @ViewScoped quando precisar manter estado durante interações na mesma página. Use @RequestScoped para operações simples e @SessionScoped apenas quando dados precisam persistir entre páginas diferentes.
Exercícios (Checkpoints)
Laboratório 8 — CRUD de Filmes com Jakarta Faces e PrimeFaces
Nesta seção, vamos construir um sistema completo de gerenciamento de filmes para o CritiqueHub, integrando Jakarta Faces, PrimeFaces, JPA e Bean Validation.
Passo 1: Configuração do Projeto
Vamos utilizar o projeto do laboratório anterior, adicionando as dependências necessárias para persistência de dados.
No arquivo pom.xml, certifique-se de ter as seguintes dependências:
<dependencies>
<!-- PrimeFaces -->
<dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces</artifactId>
<version>3.15.7</version>
</dependency>
<!-- Hibernate ORM com Panache -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- Bean Validation -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
</dependencies>
O Panache simplifica significativamente o uso de JPA, eliminando boilerplate e fornecendo métodos utilitários prontos.
Configure o arquivo src/main/resources/application.properties:
# Configuração do banco de dados
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=critiquehub
quarkus.datasource.password=critiquehub123
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/critiquehub
# Hibernate - drop e recria as tabelas a cada inicialização (apenas desenvolvimento)
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql
# Jakarta Faces
quarkus.faces.welcome-files=index.xhtml
# PrimeFaces
quarkus.primefaces.theme=nova-light
Em produção, altere database.generation para update ou validate para evitar perda de dados.
Passo 2: Estrutura de Diretórios
Organize o projeto com a seguinte estrutura:
src/
├── main/
│ ├── java/
│ │ └── br/upf/ads175/critiquehub/
│ │ ├── entity/
│ │ │ └── Filme.java
│ │ ├── service/
│ │ │ └── FilmeService.java
│ │ └── controller/
│ │ └── FilmeController.java
│ ├── resources/
│ │ ├── META-INF/
│ │ │ └── resources/
│ │ │ └── filmes.xhtml
│ │ ├── application.properties
│ │ └── import.sql
│ └── webapp/
│ └── WEB-INF/
│ └── web.xml
Passo 3: Criando a Entidade Filme
Crie a classe Filme que representa um filme no banco de dados:
// Filme.java
package br.upf.ads175.critiquehub.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
/**
* Entidade que representa um filme no catálogo do CritiqueHub.
*/
@Entity
@Table(name = "filmes")
public class Filme extends PanacheEntity {
@NotBlank(message = "O título é obrigatório")
@Size(max = 150, message = "O título não pode ter mais de 150 caracteres")
@Column(nullable = false, length = 150)
private String titulo;
@NotBlank(message = "O diretor é obrigatório")
@Size(max = 100, message = "O nome do diretor não pode ter mais de 100 caracteres")
@Column(nullable = false, length = 100)
private String diretor;
@NotNull(message = "O ano é obrigatório")
@Min(value = 1888, message = "O ano deve ser maior ou igual a 1888")
@Max(value = 2100, message = "O ano deve ser menor ou igual a 2100")
@Column(nullable = false)
private Integer ano;
@NotBlank(message = "O gênero é obrigatório")
@Size(max = 50, message = "O gênero não pode ter mais de 50 caracteres")
@Column(nullable = false, length = 50)
private String genero;
@Size(max = 1000, message = "A sinopse não pode ter mais de 1000 caracteres")
@Column(length = 1000)
private String sinopse;
public Filme() {
}
public Filme(String titulo, String diretor, Integer ano, String genero) {
this.titulo = titulo;
this.diretor = diretor;
this.ano = ano;
this.genero = genero;
}
// Getters e Setters
public String getTitulo() { return titulo; }
public void setTitulo(String titulo) { this.titulo = titulo; }
public String getDiretor() { return diretor; }
public void setDiretor(String diretor) { this.diretor = diretor; }
public Integer getAno() { return ano; }
public void setAno(Integer ano) { this.ano = ano; }
public String getGenero() { return genero; }
public void setGenero(String genero) { this.genero = genero; }
public String getSinopse() { return sinopse; }
public void setSinopse(String sinopse) { this.sinopse = sinopse; }
}
Ao estender PanacheEntity, a entidade ganha automaticamente o campo id e métodos utilitários como findAll(), findById(), persist() e delete().
Passo 4: Criando o Service
O service contém a lógica de negócio e gerencia transações:
// FilmeService.java
package br.upf.ads175.critiquehub.service;
import br.upf.ads175.critiquehub.entity.Filme;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.List;
/**
* Service que gerencia operações de negócio relacionadas a filmes.
*/
@ApplicationScoped
public class FilmeService {
/**
* Lista todos os filmes ordenados por título.
*/
public List<Filme> listarTodos() {
return Filme.listAll();
}
/**
* Busca um filme por ID.
*/
public Filme buscarPorId(Long id) {
return Filme.findById(id);
}
/**
* Salva um novo filme no banco de dados.
*/
@Transactional
public void salvar(Filme filme) {
filme.persist();
}
/**
* Atualiza um filme existente.
*/
@Transactional
public void atualizar(Filme filme) {
Filme filmeExistente = Filme.findById(filme.id);
if (filmeExistente != null) {
filmeExistente.setTitulo(filme.getTitulo());
filmeExistente.setDiretor(filme.getDiretor());
filmeExistente.setAno(filme.getAno());
filmeExistente.setGenero(filme.getGenero());
filmeExistente.setSinopse(filme.getSinopse());
}
}
/**
* Remove um filme do banco de dados.
*/
@Transactional
public void remover(Long id) {
Filme.deleteById(id);
}
/**
* Busca filmes por título (filtro).
*/
public List<Filme> buscarPorTitulo(String titulo) {
return Filme.list("LOWER(titulo) LIKE LOWER(?1)", "%" + titulo + "%");
}
}
A anotação @Transactional é obrigatória para métodos que modificam dados. Ela garante que as operações sejam atômicas e possam ser revertidas em caso de erro.
Passo 5: Criando o Controller (Managed Bean)
O controller gerencia a interação entre a interface e o service:
// FilmeController.java
package br.upf.ads175.critiquehub.controller;
import br.upf.ads175.critiquehub.entity.Filme;
import br.upf.ads175.critiquehub.service.FilmeService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
/**
* Controller ViewScoped para gerenciar a interface de filmes.
*/
@Named
@ViewScoped
public class FilmeController implements Serializable {
@Inject
private FilmeService filmeService;
private Filme filme;
private List<Filme> filmes;
private String filtro;
private boolean modoEdicao;
@PostConstruct
public void init() {
filme = new Filme();
carregarFilmes();
}
/**
* Carrega todos os filmes do banco de dados.
*/
public void carregarFilmes() {
try {
if (filtro == null || filtro.trim().isEmpty()) {
filmes = filmeService.listarTodos();
} else {
filmes = filmeService.buscarPorTitulo(filtro);
}
} catch (Exception e) {
addMessage(FacesMessage.SEVERITY_ERROR,
"Erro ao carregar filmes", e.getMessage());
}
}
/**
* Salva um filme (novo ou atualizado).
*/
public void salvar() {
try {
if (modoEdicao) {
filmeService.atualizar(filme);
addMessage(FacesMessage.SEVERITY_INFO,
"Sucesso!", "Filme atualizado com sucesso!");
} else {
filmeService.salvar(filme);
addMessage(FacesMessage.SEVERITY_INFO,
"Sucesso!", "Filme cadastrado com sucesso!");
}
novo();
carregarFilmes();
} catch (Exception e) {
addMessage(FacesMessage.SEVERITY_ERROR,
"Erro ao salvar", e.getMessage());
}
}
/**
* Prepara o formulário para edição de um filme existente.
*/
public void editar(Filme filmeParaEditar) {
this.filme = filmeParaEditar;
this.modoEdicao = true;
}
/**
* Remove um filme do banco de dados.
*/
public void remover(Long id) {
try {
filmeService.remover(id);
addMessage(FacesMessage.SEVERITY_INFO,
"Sucesso!", "Filme removido com sucesso!");
carregarFilmes();
} catch (Exception e) {
addMessage(FacesMessage.SEVERITY_ERROR,
"Erro ao remover", e.getMessage());
}
}
/**
* Limpa o formulário e prepara para novo cadastro.
*/
public void novo() {
filme = new Filme();
modoEdicao = false;
}
/**
* Aplica o filtro de busca.
*/
public void aplicarFiltro() {
carregarFilmes();
}
/**
* Limpa o filtro de busca.
*/
public void limparFiltro() {
filtro = "";
carregarFilmes();
}
/**
* Adiciona uma mensagem JSF para o usuário.
*/
private void addMessage(FacesMessage.Severity severity,
String summary, String detail) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(severity, summary, detail));
}
// Getters e Setters
public Filme getFilme() { return filme; }
public void setFilme(Filme filme) { this.filme = filme; }
public List<Filme> getFilmes() { return filmes; }
public void setFilmes(List<Filme> filmes) { this.filmes = filmes; }
public String getFiltro() { return filtro; }
public void setFiltro(String filtro) { this.filtro = filtro; }
public boolean isModoEdicao() { return modoEdicao; }
public void setModoEdicao(boolean modoEdicao) { this.modoEdicao = modoEdicao; }
}
@PostConstruct é executado após a criação do bean e injeção de dependências, sendo ideal para inicialização de dados.
Passo 6: Criando a Página XHTML com PrimeFaces
Crie o arquivo src/main/resources/META-INF/resources/filmes.xhtml:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>Gerenciamento de Filmes - CritiqueHub</title>
<style>
.filme-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.form-grid {
margin-bottom: 20px;
}
</style>
</h:head>
<h:body>
<div class="filme-container">
<h:form id="formFilme">
<!-- Cabeçalho -->
<p:panel header="Gerenciamento de Filmes - CritiqueHub"
style="margin-bottom: 20px;">
<p>Sistema completo de cadastro e gerenciamento
do catálogo de filmes</p>
</p:panel>
<!-- Formulário de Cadastro/Edição -->
<p:panel header="#{filmeController.modoEdicao ?
'Editar Filme' : 'Novo Filme'}"
style="margin-bottom: 20px;">
<h:panelGrid columns="2" cellpadding="10"
styleClass="form-grid">
<!-- Campo Título -->
<p:outputLabel for="titulo" value="Título:*"
style="font-weight: bold;"/>
<p:inputText id="titulo"
value="#{filmeController.filme.titulo}"
required="true"
requiredMessage="O título é obrigatório"
style="width: 400px;"/>
<!-- Campo Diretor -->
<p:outputLabel for="diretor" value="Diretor:*"
style="font-weight: bold;"/>
<p:inputText id="diretor"
value="#{filmeController.filme.diretor}"
required="true"
requiredMessage="O diretor é obrigatório"
style="width: 400px;"/>
<!-- Campo Ano -->
<p:outputLabel for="ano" value="Ano:*"
style="font-weight: bold;"/>
<p:inputNumber id="ano"
value="#{filmeController.filme.ano}"
required="true"
requiredMessage="O ano é obrigatório"
decimalPlaces="0"
minValue="1888"
maxValue="2100"
style="width: 150px;"/>
<!-- Campo Gênero -->
<p:outputLabel for="genero" value="Gênero:*"
style="font-weight: bold;"/>
<p:inputText id="genero"
value="#{filmeController.filme.genero}"
required="true"
requiredMessage="O gênero é obrigatório"
style="width: 300px;"/>
<!-- Campo Sinopse -->
<p:outputLabel for="sinopse" value="Sinopse:"
style="font-weight: bold;"/>
<p:inputTextarea id="sinopse"
value="#{filmeController.filme.sinopse}"
rows="4"
cols="50"
maxlength="1000"
style="width: 400px;"/>
<!-- Botões -->
<h:outputText value=""/>
<h:panelGroup>
<p:commandButton
value="#{filmeController.modoEdicao ?
'Atualizar' : 'Salvar'}"
action="#{filmeController.salvar}"
update="formFilme"
icon="pi pi-check"
styleClass="ui-button-success"
style="margin-right: 10px;"/>
<p:commandButton value="Cancelar"
action="#{filmeController.novo}"
update="formFilme"
icon="pi pi-times"
styleClass="ui-button-secondary"/>
</h:panelGroup>
</h:panelGrid>
</p:panel>
<!-- Filtro de Busca -->
<p:panel header="Buscar Filmes"
style="margin-bottom: 20px;">
<h:panelGrid columns="3" cellpadding="10">
<p:inputText id="filtro"
value="#{filmeController.filtro}"
placeholder="Digite o título do filme"
style="width: 400px;"/>
<p:commandButton value="Buscar"
action="#{filmeController.aplicarFiltro}"
update="formFilme"
icon="pi pi-search"
styleClass="ui-button-info"/>
<p:commandButton value="Limpar"
action="#{filmeController.limparFiltro}"
update="formFilme"
icon="pi pi-refresh"
styleClass="ui-button-secondary"/>
</h:panelGrid>
</p:panel>
<!-- Tabela de Filmes -->
<p:dataTable id="tabelaFilmes"
value="#{filmeController.filmes}"
var="f"
emptyMessage="Nenhum filme cadastrado"
paginator="true"
rows="10"
paginatorPosition="bottom">
<p:column headerText="ID"
style="width: 60px; text-align: center;">
<h:outputText value="#{f.id}"/>
</p:column>
<p:column headerText="Título" sortBy="#{f.titulo}">
<h:outputText value="#{f.titulo}"/>
</p:column>
<p:column headerText="Diretor" sortBy="#{f.diretor}">
<h:outputText value="#{f.diretor}"/>
</p:column>
<p:column headerText="Ano" sortBy="#{f.ano}"
style="width: 100px; text-align: center;">
<h:outputText value="#{f.ano}"/>
</p:column>
<p:column headerText="Gênero" style="width: 150px;">
<h:outputText value="#{f.genero}"/>
</p:column>
<p:column headerText="Ações"
style="width: 180px; text-align: center;">
<p:commandButton icon="pi pi-pencil"
title="Editar"
action="#{filmeController.editar(f)}"
update="formFilme"
styleClass="ui-button-warning"
style="margin-right: 5px;"/>
<p:commandButton icon="pi pi-trash"
title="Remover"
action="#{filmeController.remover(f.id)}"
update="formFilme"
styleClass="ui-button-danger">
<p:confirm header="Confirmação"
message="Deseja realmente remover
o filme '#{f.titulo}'?"
icon="pi pi-exclamation-triangle"/>
</p:commandButton>
</p:column>
</p:dataTable>
<!-- Mensagens -->
<p:messages autoUpdate="true" closable="true"
style="margin-top: 20px;"/>
<!-- Diálogo de Confirmação -->
<p:confirmDialog global="true">
<p:commandButton value="Sim" type="button"
styleClass="ui-button-success"
icon="pi pi-check"/>
<p:commandButton value="Não" type="button"
styleClass="ui-button-danger"
icon="pi pi-times"/>
</p:confirmDialog>
</h:form>
</div>
</h:body>
</html>
Componentes PrimeFaces utilizados:
p:panel: Agrupa conteúdo com cabeçalhop:inputText/p:inputTextarea: Campos de entrada melhoradosp:inputNumber: Campo numérico com validaçãop:commandButton: Botões com ícones e estilosp:dataTable: Tabela com paginação e ordenaçãop:messages: Exibe mensagens de feedbackp:confirmDialog: Diálogo de confirmação para exclusão
Passo 7: Dados de Exemplo
Crie o arquivo src/main/resources/import.sql com dados iniciais:
-- Inserir filmes brasileiros no catálogo
INSERT INTO filmes (titulo, diretor, ano, genero, sinopse) VALUES
('Central do Brasil', 'Walter Salles', 1998, 'Drama',
'Uma professora aposentada e um menino órfão embarcam numa jornada
pelo Brasil em busca do pai do garoto.'),
('Cidade de Deus', 'Fernando Meirelles', 2002, 'Crime',
'A história da criminalidade na favela Cidade de Deus ao longo
de três décadas, narrada por um jovem fotógrafo.'),
('Tropa de Elite', 'José Padilha', 2007, 'Ação',
'Um capitão do BOPE enfrenta dilemas éticos enquanto lida
com a violência no Rio de Janeiro.'),
('O Auto da Compadecida', 'Guel Arraes', 2000, 'Comédia',
'As aventuras e trapalhadas de João Grilo e Chicó no sertão nordestino.'),
('Que Horas Ela Volta?', 'Anna Muylaert', 2015, 'Drama',
'Uma empregada doméstica enfrenta mudanças quando sua filha
vem morar com ela na casa dos patrões.');
Passo 8: Configuração web.xml
Crie o arquivo src/main/webapp/WEB-INF/web.xml:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="4.0">
<!-- Modo desenvolvimento -->
<context-param>
<param-name>jakarta.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<!-- Tema PrimeFaces -->
<context-param>
<param-name>primefaces.THEME</param-name>
<param-value>nova-light</param-value>
</context-param>
<!-- Configuração Faces Servlet -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>filmes.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Passo 9: Executando a Aplicação
Inicie o banco de dados PostgreSQL (via Docker):
docker run --name postgres-critiquehub \
-e POSTGRES_DB=critiquehub \
-e POSTGRES_USER=critiquehub \
-e POSTGRES_PASSWORD=critiquehub123 \
-p 5432:5432 \
-d postgres:15
Execute a aplicação:
./mvnw quarkus:dev
Acesse http://localhost:8080/filmes.xhtml e você verá:
- Formulário de Cadastro: Com validações automáticas
- Busca por Título: Filtro dinâmico dos filmes
- Tabela Interativa: Com paginação, ordenação e ações
- Operações CRUD Completas: Criar, editar e remover filmes
- Feedback Visual: Mensagens de sucesso e erro
- Confirmação de Exclusão: Diálogo antes de remover
O Quarkus dev mode permite alterações em tempo real. Modifique o código Java ou XHTML e veja as mudanças imediatamente.
Demonstração do View Scope
Para demonstrar na prática como o @ViewScoped mantém o estado do bean durante múltiplas interações AJAX, vamos adicionar um contador de interações e informações de debug.
Entendendo o Ciclo de Vida do View Scope
O ciclo de vida do @ViewScoped difere significativamente de outros escopos:
| Escopo | Criação do Bean | Destruição do Bean |
|---|---|---|
@RequestScoped | A cada requisição HTTP | Ao final da requisição |
@ViewScoped | Na primeira requisição à página | Quando o usuário sai da página ou sessão expira |
@SessionScoped | Na primeira requisição do usuário | Quando a sessão expira ou é invalidada |
Por que ViewScope é ideal para CRUD?
- Requisições AJAX não recriam o bean: Todas as operações (salvar, editar, remover) mantêm o estado
- Mais leve que SessionScope: Bean é descartado ao sair da página
- Preserva contexto: Dados do formulário não se perdem durante validações
- Melhor performance: Evita serialização/deserialização constante
Modificando o Controller
Adicione os seguintes atributos e métodos ao FilmeController:
@Named
@ViewScoped
public class FilmeController implements Serializable {
// ... atributos existentes ...
private int contadorInteracoes = 0;
private String timestampCriacao;
private String ultimaAcao = "Nenhuma ação executada ainda";
@PostConstruct
public void init() {
filme = new Filme();
carregarFilmes();
timestampCriacao = java.time.LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern(
"dd/MM/yyyy HH:mm:ss")
);
System.out.println("=== FilmeController CRIADO em: "
+ timestampCriacao + " ===");
}
/**
* Incrementa contador para demonstrar ViewScope.
*/
public void incrementarContador() {
contadorInteracoes++;
ultimaAcao = "Contador incrementado";
System.out.println("Contador: " + contadorInteracoes
+ " | Bean criado em: " + timestampCriacao);
}
// Getters adicionais para demonstração
public int getContadorInteracoes() { return contadorInteracoes; }
public String getTimestampCriacao() { return timestampCriacao; }
public String getUltimaAcao() { return ultimaAcao; }
}
Adicionando Painel de Debug na Página
Adicione este painel logo após o cabeçalho principal em filmes.xhtml:
<!-- Painel de Demonstração do View Scope -->
<p:panel header="Demonstração: View Scope em Ação"
style="margin-bottom: 20px; background-color: #f0f8ff;"
toggleable="true" collapsed="false">
<h:panelGrid columns="2" cellpadding="10" style="width: 100%;">
<h:outputText value="Bean criado em:"
style="font-weight: bold; color: #2c5aa0;"/>
<h:outputText value="#{filmeController.timestampCriacao}"
style="color: #2c5aa0;"/>
<h:outputText value="Número de interações:"
style="font-weight: bold; color: #2c5aa0;"/>
<h:outputText value="#{filmeController.contadorInteracoes}"
style="color: #2c5aa0; font-size: 18px;
font-weight: bold;"/>
<h:outputText value="Última ação:"
style="font-weight: bold; color: #2c5aa0;"/>
<h:outputText value="#{filmeController.ultimaAcao}"
style="color: #2c5aa0;"/>
</h:panelGrid>
<p:separator/>
<div style="margin-top: 10px;">
<p:commandButton value="Incrementar Contador"
action="#{filmeController.incrementarContador}"
update="@form"
icon="pi pi-plus"
styleClass="ui-button-info"
style="margin-right: 10px;"/>
<p style="margin-top: 15px; font-size: 12px; color: #666;
font-style: italic;">
<strong>Como funciona:</strong> O @ViewScoped mantém o bean
vivo enquanto você permanece nesta página. Todas as operações
AJAX (salvar, editar, remover, filtrar) incrementam o contador
<strong>sem recriar o bean</strong>.
<br/>
<strong>Teste:</strong> Abra outra aba ou atualize a página
(F5) — o contador será zerado porque um novo bean será criado.
</p>
</div>
</p:panel>
Testando o View Scope
O painel de demonstração na interface mostra claramente como o @ViewScoped funciona:
| Comportamento | Explicação |
|---|---|
| Bean criado em | Mostra o timestamp de quando o bean foi instanciado. Este valor permanece constante durante toda a interação na página. |
| Número de interações | Incrementa a cada operação AJAX (salvar, editar, remover, filtrar). Demonstra que o bean mantém seu estado entre requisições AJAX. |
| Última ação | Registra a última operação executada, provando que o estado é preservado. |
Experimente:
- Execute várias operações (cadastrar, editar, filtrar)
- Observe o contador aumentando — o bean não é recriado
- Clique em "Incrementar Contador" múltiplas vezes via AJAX
- Abra a página em outra aba — verá um novo timestamp (novo bean)
- Atualize a página (F5) — contador volta a zero (novo bean criado)
Comparação com RequestScoped:
Se o controller fosse @RequestScoped em vez de @ViewScoped:
- A cada clique de botão, um novo bean seria criado
- O contador sempre seria 1, nunca incrementaria
- O timestamp mudaria a cada ação
- O formulário perderia dados durante edição
Exercício Prático: Comparando Escopos
Para solidificar o entendimento, faça este experimento:
1. Crie uma versão RequestScoped (apenas para teste):
@Named("filmeRequestBean")
@RequestScoped
public class FilmeRequestController implements Serializable {
// Mesma implementação do FilmeController
// mas com @RequestScoped
}
2. Crie uma página de comparação comparacao-escopos.xhtml:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>Comparação: ViewScoped vs RequestScoped</title>
</h:head>
<h:body style="padding: 20px;">
<h:form>
<p:panel header="Comparação de Escopos JSF">
<!-- ViewScoped -->
<p:panel header="@ViewScoped (recomendado para CRUD)"
style="margin-bottom: 20px; background-color: #e8f5e9;">
<h:panelGrid columns="2">
<h:outputText value="Contador:"
style="font-weight: bold;"/>
<h:outputText
value="#{filmeController.contadorInteracoes}"
style="font-size: 24px; color: green;"/>
<h:outputText value="Criado em:"/>
<h:outputText
value="#{filmeController.timestampCriacao}"/>
</h:panelGrid>
<p:commandButton value="Incrementar ViewScoped"
action="#{filmeController.incrementarContador}"
update="@form"
styleClass="ui-button-success"/>
</p:panel>
<!-- RequestScoped -->
<p:panel header="@RequestScoped (não recomendado para CRUD)"
style="background-color: #ffebee;">
<h:panelGrid columns="2">
<h:outputText value="Contador:"
style="font-weight: bold;"/>
<h:outputText
value="#{filmeRequestBean.contadorInteracoes}"
style="font-size: 24px; color: red;"/>
<h:outputText value="Criado em:"/>
<h:outputText
value="#{filmeRequestBean.timestampCriacao}"/>
</h:panelGrid>
<p:commandButton value="Incrementar RequestScoped"
action="#{filmeRequestBean.incrementarContador}"
update="@form"
styleClass="ui-button-danger"/>
</p:panel>
</p:panel>
</h:form>
</h:body>
</html>
3. Execute e observe:
- No ViewScoped: contador incrementa (2, 3, 4...) e timestamp fixo
- No RequestScoped: contador sempre 1 e timestamp mudando constantemente
Esta demonstração visual torna evidente por que @ViewScoped é a escolha correta para operações CRUD: ele mantém o estado da interface durante múltiplas interações, proporcionando melhor experiência do usuário e performance.
No console do servidor, você verá logs indicando quando o bean é criado:
=== FilmeController CRIADO em: 15/10/2025 14:30:45 ===
Contador: 1 | Bean criado em: 15/10/2025 14:30:45
Contador: 2 | Bean criado em: 15/10/2025 14:30:45
Contador: 3 | Bean criado em: 15/10/2025 14:30:45
Note que o timestamp de criação permanece o mesmo, provando que o bean não foi recriado.
Resumo: ViewScope em Ação
A demonstração prática do View Scope revela seu funcionamento através de três indicadores:
| Indicador | O que mostra | Por que é importante |
|---|---|---|
| Timestamp de Criação | Momento exato em que o bean foi instanciado | Permanece constante durante toda interação na página, provando que o bean não é recriado |
| Contador de Interações | Número de operações AJAX executadas | Incrementa continuamente (1, 2, 3...), demonstrando preservação de estado |
| Última Ação | Registro da operação mais recente | Mostra o contexto sendo mantido entre requisições |
Principais aprendizados:
- View Scope: Mantém estado durante interações na mesma página (demonstrado na prática)
- Bean Validation: Validações declarativas garantem integridade dos dados
- Panache: Simplifica operações JPA eliminando boilerplate
- PrimeFaces: Componentes ricos elevam a experiência do usuário
- Separação de Camadas: Entity, Service e Controller com responsabilidades bem definidas
- AJAX Eficiente: Operações assíncronas sem perda de contexto