Um CRUD com JavaFX
Todo programador já fez(ou vai fazer) um CRUD na vida e é, no geral, a forma que aprendemos conceitos básicos de uma tecnologia. Nesse capítulo vamos mostrar um simples CRUD em JavaFX para explorar algumas características da tecnologia. O objetivo é exercitar o que aprendemos para podemos fechar o livro. Sim, senhores e senhoras, estamos chegando n final.
Definição de CRUD
CRUD é uma sigla que vem das operações básicas que podemos fazer um algo armazenado em uma fonte de dados:
Create: Criar uma nova instância de algo na fonte dos dados;
Retrieve: Trazer os dados já armazenados;
Update: Atualizar dados já armazenados;
Delete: Apagar.
Onde armazenar os dados?
A fonte de dados pode ser um banco de dados(MySQL, MariaDB, Postgres, etc), um arquivo (arquivos em diversos formatos, como CSV), um Web Service (que após chamado, salva os dados em algum lugar) ou até mesmo a própria memória dinâmica do computador (sendo que os dados se perdem quando a aplicação é fechada). Aqui vamos usar um arquivo CSV, uma forma simples de armazenar objetos em um arquivo de texto.
O motivo de não usarmos um banco de dados tradicional é que o foco do artigo é mostrar JavaFX. Um banco de dados comum implica em termos que falar de SQL, conexão com o banco, select, etc, isso vai fugir do foco.
Nosso CRUD
Nossa aplicação simples vai ser um CRUD de Contas. Sim, bills, contas! Pagamos todo mês contas de luz, água, gás, faturas, etc, etc, argh. Para representar a conta, criamos um objeto Java chamado Conta com três atributos: id (gerado automaticamente para controle interno), concessionária, descrição e data de vencimento. É isso, simples, não? Em breve mostramos o código dessa classe, mas agoras que conhecemos os campos, veja a telinha que modelamos usando o SceneBuilder que gerou um FXML (se não sabe o que é isso, veja esse artigo sobre FXML, ou veja o capítulo Usando FXML).
Começamos com uma tabela com três colunas representando os campos, após a tabela temos os campos de texto para entrada do nome, descrição e um campo para entrada de data do tipo DatePicker, que não foi abordado nesse livro, e por fim os botões de ações.
A lógica da aplicação é a seguinte:
- A tabela tem ID tblContas e três colunas: clConc, clDesc e clVenc. Elas são populadas com os dados de um objeto do tipo Conta;
- Os campos de texto e o campo de data tem um (txtConc, txtDesc, dpVenc) serão injetados no controller para que possamos saber o valor que o usuário entrou;
Cada um dos botões ação:
- salvar: salva o objeto de acordo com a informação entrada pelo usuário. Não está habilitado quando um campo está selecionado na tabela;
- atualizar: Só está habilitado quando selecionamos uma linha da coluna e permite atualizar os dados dessa linha (os campos de entrada de dados vão ser atualizados com o valor selecionado para serem modificados pelo usuário);
- apagar: apaga a linha selecionada;
- limpar: limpa o campo selecionado atualmente.
As operações e os elementos da tela ficam na classe ContasController. Código que veremos já já.
Fazendo as operações com o banco de dados
As operações em sí com o banco ficam na interface ContasService. Ela contém os métodos salvar, que recebe uma instância de conta a ser salva, atualizar, que recebe a conta já salva para ser atualizada, apagar, que apaga uma conta e buscarTodos, que retorna todas as contas selecionadas. É nessa classe que fazemos as operações.
Todo o código poderia ficar dentro do controller, MAS ISSO É COISA FEIA, temos que definir os métodos em uma interface e, vejam que interessante, usando a capacidade do Java 8 de definir métodos padrões, criamos um método getInstance para retornar a implementação que queremos dessa interface. Assim, criamos a clase ContasCSVService, que é uma implementação da interface, e retornamos uma nova instância nesse método! Podemos, obviamente, criar uma interface, por exemplo ContasBDService que faria a mesma coisa, mas que invés de usar um arquivo CSV, se comunica com um banco de dados, a mesma ideia poderia ser aplicada para um arquivo XLS, como ContasXLSService, e por aí vai. O código do controller, que os métodos da interface, não iria sofrer nenhuma modificação, pois só precisaríamos trocar a intância de ContasService retornada no método getNewInstance! (claro que com CDI e outras tecnologias, sequer criar manualmente precisaríamos). Veja a nossa interface:
VAMOS AO CÓDIGO!
Agora, depois desse mooonnnntteee de papo, vamos ao código. Claro que se você leu acima com atenção, vai ser muito simples de entender tudo, mas mesmo assim o código está comentado na medida do possível.
Não vou colocar o todo código nesse capítulo, pois já está muito extenso, mas veja o código da classe Conta, da interface ContasService e da classe ContasController. Note que você pode encontrar o código no anexo javafx-pratico.zip no pacote javafxpratico.crud.
import java.util.Date;
/**
*
* Nossa classe de modelo do objeto que "sofrerá" as operações de CRUD
* @author wsiqueir
*
*/
public class Conta {
private int id;
private String concessionaria;
private String descricao;
private Date dataVencimento;
// gets e sets omitidos..
}
import java.net.URL;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.ResourceBundle;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
/**
*
* O controller da aplicação, onde a mágica acontece
* @author wsiqueir
*
*/
public class ContasController implements Initializable {
@FXML
private TableView<Conta> tblContas;
@FXML
private TableColumn<Conta, String> clConsc;
@FXML
private TableColumn<Conta, String> clDesc;
@FXML
private TableColumn<Conta, Date> clVenc;
@FXML
private TextField txtConsc;
@FXML
private TextField txtDesc;
@FXML
private DatePicker dpVencimento;
@FXML
private Button btnSalvar;
@FXML
private Button btnAtualizar;
@FXML
private Button btnApagar;
@FXML
private Button btnLimpart;
private ContasService service;
// Esse método é chamado ao inicializar a aplicação, igual um construtor.
// Ele vem da interface Initializable
@Override
public void initialize(URL location, ResourceBundle resources) {
service = ContasService.getNewInstance();
configuraColunas();
configuraBindings();
atualizaDadosTabela();
}
// métodos públicos chamados quando o botão é clicado
public void salvar() {
Conta c = new Conta();
pegaValores(c);
service.salvar(c);
atualizaDadosTabela();
}
public void atualizar() {
Conta c = tblContas.getSelectionModel().getSelectedItem();
pegaValores(c);
service.atualizar(c);
atualizaDadosTabela();
}
public void apagar() {
Conta c = tblContas.getSelectionModel().getSelectedItem();
service.apagar(c.getId());
atualizaDadosTabela();
}
public void limpar() {
tblContas.getSelectionModel().select(null);
txtConsc.setText("");
txtDesc.setText("");
dpVencimento.setValue(null);
}
// métodos privados do controller
// pega os valores entrados pelo usuário e adiciona no objeto conta
private void pegaValores(Conta c) {
c.setConcessionaria(txtConsc.getText());
c.setDescricao(txtDesc.getText());
c.setDataVencimento(dataSelecionada());
}
// método utilitário para pega a data que foi selecionada (que usa o tipo novo do java 8 LocalDateTime)
private Date dataSelecionada() {
LocalDateTime time = dpVencimento.getValue().atStartOfDay();
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
}
// chamado quando acontece alguma operação de atualização dos dados
private void atualizaDadosTabela() {
tblContas.getItems().setAll(service.buscarTodas());
limpar();
}
// configura as colunas para mostrar as propriedades da classe Conta
private void configuraColunas() {
clConsc.setCellValueFactory(new PropertyValueFactory<>("concessionaria"));
clDesc.setCellValueFactory(new PropertyValueFactory<>("descricao"));
clVenc.setCellValueFactory(new PropertyValueFactory<>("dataVencimento"));
}
// configura a lógica da tela
private void configuraBindings() {
// esse binding só e false quando os campos da tela estão preenchidos
BooleanBinding camposPreenchidos = txtConsc.textProperty().isEmpty()
.or(txtDesc.textProperty().isEmpty())
.or(dpVencimento.valueProperty().isNull());
// indica se há algo selecionado na tabela
BooleanBinding algoSelecionado = tblContas.getSelectionModel().selectedItemProperty().isNull();
// alguns botões só são habilitados se algo foi selecionado na tabela
btnApagar.disableProperty().bind(algoSelecionado);
btnAtualizar.disableProperty().bind(algoSelecionado);
btnLimpart.disableProperty().bind(algoSelecionado);
// o botão salvar só é habilitado se as informações foram preenchidas e não tem nada na tela
btnSalvar.disableProperty().bind(algoSelecionado.not().or(camposPreenchidos));
// quando algo é selecionado na tabela, preenchemos os campos de entrada com os valores para o
// usuário editar
tblContas.getSelectionModel().selectedItemProperty().addListener((b, o, n) -> {
if (n != null) {
LocalDate data = null;
data = n.getDataVencimento().toInstant()
.atZone(ZoneId.systemDefault()).toLocalDate();
txtConsc.setText(n.getConcessionaria());
txtDesc.setText(n.getDescricao());
dpVencimento.setValue(data);
}
});
}
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* Uma implementação do ContasService para lidar com arquivo CSV
* @author wsiqueir
*
*/
public class ContasCSVService implements ContasService {
// divisor de colunas no arquivo
private static final String SEPARADOR = ";";
// o caminho para o arquivo deve ser selecionado aqui
private static final Path ARQUIVO_SAIDA = Paths.get("./dados.csv");
// os dados do arquivo
private List<Conta> contas;
// formato de data usado no arquivo
final SimpleDateFormat formatoData = new SimpleDateFormat("dd/MM/yyyy");
public ContasCSVService() {
carregaDados();
}
@Override
public void salvar(Conta conta) {
conta.setId(ultimoId() + 1);
contas.add(conta);
salvaDados();
}
@Override
public void atualizar(Conta conta) {
Conta contaAntiga = buscaPorId(conta.getId());
contaAntiga.setConcessionaria(conta.getConcessionaria());
contaAntiga.setDataVencimento(conta.getDataVencimento());
contaAntiga.setDescricao(conta.getDescricao());
salvaDados();
}
@Override
public List<Conta> buscarTodas() {
return contas;
}
@Override
public void apagar(int id) {
Conta conta = buscaPorId(id);
contas.remove(conta);
salvaDados();
}
public Conta buscaPorId(int id) {
return contas.stream().filter(c -> c.getId() == id).findFirst()
.orElseThrow(() -> new Error("Conta não encontrada"));
}
// salva a lista de dados no arquivo, gerando um novo CSV e escrevendo o arquivo completamente
private void salvaDados() {
StringBuffer sb = new StringBuffer();
for (Conta c : contas) {
String linha = criaLinha(c);
sb.append(linha);
sb.append(System.getProperty("line.separator"));
}
try {
Files.delete(ARQUIVO_SAIDA);
Files.write(ARQUIVO_SAIDA, sb.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
System.exit(0);
}
}
// o ID mais alto é retornado aqui para continuarmos contando os IDs
private int ultimoId() {
return contas.stream().mapToInt(Conta::getId).max().orElse(0);
}
// carrega os dados do arquivo para a lista contas
private void carregaDados() {
try {
if(!Files.exists(ARQUIVO_SAIDA)) {
Files.createFile(ARQUIVO_SAIDA);
}
contas = Files.lines(ARQUIVO_SAIDA).map(this::leLinha).collect(Collectors.toList());
} catch (IOException e) {
e.printStackTrace();
System.exit(0);
}
}
// transforma uma linha do CSV para o tipo Conta
private Conta leLinha(String linha) {
String colunas[] = linha.split(SEPARADOR);
int id = Integer.parseInt(colunas[0]);
Date dataVencimento = null;
try {
dataVencimento = formatoData.parse(colunas[3]);
} catch (ParseException e) {
e.printStackTrace();
System.exit(0);
}
Conta conta = new Conta();
conta.setId(id);
conta.setConcessionaria(colunas[1]);
conta.setDescricao(colunas[2]);
conta.setDataVencimento(dataVencimento);
return conta;
}
// transforma um objeto conta em um arquivo CSV
private String criaLinha(Conta c) {
String dataStr = formatoData.format(c.getDataVencimento());
String idStr = String.valueOf(c.getId());
String linha = String.join(SEPARADOR, idStr, c.getConcessionaria(), c.getDescricao(),
dataStr);
return linha;
}
}
Esse foi nosso projeto de exemplo. Não há nada demais nele, não com o código.