Criando um banco SQLite
Vamos usar SQLite com Dart puro, usando o pacote sqlite3. Ele é um pacote usado para acessar SQLite a partir de aplicações Dart, e funciona bem para aprender a base antes de entrar em Flutter, sqflite, drift ou banco remoto como PostgreSQL.
A ideia será criar um banco chamado infra.db, uma tabela chamada servidores e alguns registros de servidores. Esse exemplo é simples de propósito, porque o foco agora é entender o fluxo básico. Primeiro, crie um projeto Dart:
dart create banco_sqlite
cd banco_sqlite
dart pub add sqlite3
Agora, substitua o conteúdo de bin/banco_sqlite.dart por este código:
import 'package:sqlite3/sqlite3.dart';
void main() {
final db = sqlite3.open('infra.db');
db.execute('''
CREATE TABLE IF NOT EXISTS servidores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
ip TEXT NOT NULL,
status TEXT NOT NULL
);
''');
final insert = db.prepare('''
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
''');
insert.execute(['web01', '192.168.0.10', 'ativo']);
insert.execute(['db01', '192.168.0.20', 'ativo']);
insert.execute(['old01', '192.168.0.30', 'inativo']);
insert.dispose();
final servidores = db.select('''
SELECT id, nome, ip, status
FROM servidores;
''');
for (final servidor in servidores) {
print(
'ID: ${servidor['id']} | '
'Nome: ${servidor['nome']} | '
'IP: ${servidor['ip']} | '
'Status: ${servidor['status']}',
);
}
db.dispose();
}
Para executar, faça:
dart run
Esse programa cria o arquivo infra.db no diretório do projeto. Se o arquivo já existir, o Dart abre o banco existente. Se não existir, o SQLite cria o arquivo automaticamente. Agora explicando o código.
A linha abaixo importa o pacote sqlite3, que é usado para abrir o banco SQLite e executar comandos SQL.
import 'package:sqlite3/sqlite3.dart';
A linha abaixo abre o banco infra.db. A variável db representa a conexão com o banco, então ela será usada para criar tabela, inserir dados e consultar registros.
final db = sqlite3.open('infra.db');
Agora vamos falar do db.execute:
db.execute('''
CREATE TABLE IF NOT EXISTS servidores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
ip TEXT NOT NULL,
status TEXT NOT NULL
);
''');
Ele serve para enviar um comando SQL para o SQLite. O execute é usado para comandos que não precisam retornar dados, como criar tabela ou alterar estrutura.
O SQL CREATE TABLE IF NOT EXISTS servidores cria a tabela servidores apenas se ela ainda não existir. Isso evita erro quando o programa roda mais de uma vez, porque sem o IF NOT EXISTS o SQLite tentaria criar uma tabela que já existe.
A coluna id é do tipo INTEGER, que representa número inteiro. O PRIMARY KEY define essa coluna como chave primária, ou seja, o identificador único de cada servidor. O AUTOINCREMENT faz o banco gerar o próximo número automaticamente.
As colunas nome, ip e status usam TEXT, porque guardam texto. O NOT NULL impede que esses campos fiquem vazios, já que um servidor sem nome, sem IP ou sem status não faria muito sentido nesse cadastro.
A linha abaixo é bastante importante:
final insert = db.prepare('''
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
''');
Aqui criamos um comando. O prepare é usado quando o SQL tem valores que serão enviados depois. Os sinais ? são espaços reservados, e isso é melhor do que montar SQL juntando texto manualmente, porque evita erro com aspas e também reduz risco de SQL injection.
Agora vem a parte que inserimos dados no banco, para isso, usamos o execute e colocamos os valores nos lugares dos ?. O primeiro valor vai para nome, o segundo para ip e o terceiro para status.
insert.execute(['web01', '192.168.0.10', 'ativo']);
CRUD básico
O CRUD é o nome usado para as quatro operações básicas feitas em dados. Ele vem de Create, Read, Update e Delete. Para manter o exemplo prático, vamos continuar usando a tabela servidores.
Create
O Create é a operação usada para criar um novo registro no banco. No nosso caso, criar significa cadastrar um servidor.
final insert = db.prepare('''
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
''');
insert.execute(['app01', '192.168.0.40', 'ativo']);
insert.dispose();
O SQL usado aqui é:
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
O INSERT INTO servidores informa que o registro será criado dentro da tabela servidores. As colunas nome, ip e status indicam quais campos serão preenchidos. O VALUES (?, ?, ?) deixa os valores para serem enviados pelo Dart, e isso é melhor do que colocar os dados diretamente dentro da string SQL.
Read
O Read é a operação usada para ler dados do banco. Pode ser uma consulta geral, buscando todos os servidores, ou uma consulta filtrada, buscando apenas um servidor específico.
Consulta buscando todos:
final servidores = db.select('''
SELECT id, nome, ip, status
FROM servidores;
''');
for (final servidor in servidores) {
print('${servidor['id']} - ${servidor['nome']} - ${servidor['ip']}');
}
O SQL usado foi:
SELECT id, nome, ip, status
FROM servidores;
O SELECT indica que queremos consultar dados. Depois dele aparecem as colunas que devem ser retornadas. O FROM servidores indica a tabela de origem.
Consulta buscando por status:
final ativos = db.select('''
SELECT id, nome, ip, status
FROM servidores
WHERE status = ?;
''', ['ativo']);
for (final servidor in ativos) {
print('${servidor['nome']} está ativo');
}
O SQL usado foi:
SELECT id, nome, ip, status
FROM servidores
WHERE status = ?;
O WHERE é usado para filtrar os registros. Nesse caso, o banco só retorna linhas onde a coluna status tem o valor informado pelo Dart.
No código Dart acima, estamos buscando apenas servidores ativos, isso fica fácil de identificar por causa desta parte da query WHERE status = ?;. Além dela, depois do que é fornecido ao Select é passo o valor ['ativo'].
Update
O Update é a operação usada para alterar um registro existente. Normalmente a atualização deve usar o id, porque ele identifica exatamente qual linha será alterada.
final update = db.prepare('''
UPDATE servidores
SET status = ?
WHERE id = ?;
''');
update.execute(['manutencao', 1]);
update.dispose();
O SQL usado aqui é:
UPDATE servidores
SET status = ?
WHERE id = ?;
O UPDATE servidores informa que a tabela servidores será alterada. O SET status = ? define que a coluna status receberá um novo valor. O WHERE id = ? limita a alteração a um servidor específico.
Essa parte do WHERE é muito importante. Sem ela, o banco poderia alterar todos os servidores da tabela.
Delete
O Delete é a operação usada para remover um registro do banco. Assim como no update, o ideal é apagar usando o id.
final delete = db.prepare('''
DELETE FROM servidores
WHERE id = ?;
''');
delete.execute([3]);
delete.dispose();
O SQL usado foi:
DELETE FROM servidores
WHERE id = ?;
O DELETE FROM servidores informa que registros serão apagados da tabela servidores. O WHERE id = ? limita a remoção ao servidor com aquele id.
Sem WHERE, o comando apagaria todos os registros da tabela. Por isso, em comandos de alteração ou remoção, o WHERE precisa ser tratado com bastante cuidado.
Exemplo completo com CRUD
Agora juntando tudo em um único exemplo executável:
import 'package:sqlite3/sqlite3.dart';
void main() {
final db = sqlite3.open('infra.db');
db.execute('''
CREATE TABLE IF NOT EXISTS servidores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
ip TEXT NOT NULL,
status TEXT NOT NULL
);
''');
db.execute('DELETE FROM servidores;');
criarServidor(db, 'web01', '192.168.0.10', 'ativo');
criarServidor(db, 'db01', '192.168.0.20', 'ativo');
criarServidor(db, 'old01', '192.168.0.30', 'inativo');
print('Servidores cadastrados:');
listarServidores(db);
print('\nServidores ativos:');
listarServidoresPorStatus(db, 'ativo');
atualizarStatusServidor(db, 1, 'manutencao');
print('\nDepois de atualizar o servidor 1:');
listarServidores(db);
apagarServidor(db, 3);
print('\nDepois de apagar o servidor 3:');
listarServidores(db);
db.dispose();
}
void criarServidor(Database db, String nome, String ip, String status) {
final insert = db.prepare('''
INSERT INTO servidores (nome, ip, status)
VALUES (?, ?, ?);
''');
insert.execute([nome, ip, status]);
insert.dispose();
}
void listarServidores(Database db) {
final servidores = db.select('''
SELECT id, nome, ip, status
FROM servidores
ORDER BY id;
''');
for (final servidor in servidores) {
print(
'ID: ${servidor['id']} | '
'Nome: ${servidor['nome']} | '
'IP: ${servidor['ip']} | '
'Status: ${servidor['status']}',
);
}
}
void listarServidoresPorStatus(Database db, String status) {
final servidores = db.select('''
SELECT id, nome, ip, status
FROM servidores
WHERE status = ?
ORDER BY id;
''', [status]);
for (final servidor in servidores) {
print(
'ID: ${servidor['id']} | '
'Nome: ${servidor['nome']} | '
'IP: ${servidor['ip']} | '
'Status: ${servidor['status']}',
);
}
}
void atualizarStatusServidor(Database db, int id, String novoStatus) {
final update = db.prepare('''
UPDATE servidores
SET status = ?
WHERE id = ?;
''');
update.execute([novoStatus, id]);
update.dispose();
}
void apagarServidor(Database db, int id) {
final delete = db.prepare('''
DELETE FROM servidores
WHERE id = ?;
''');
delete.execute([id]);
delete.dispose();
}
No começo do exemplo existe esta linha:
db.execute('DELETE FROM servidores;');
Ela apaga os registros antigos antes de inserir os dados de teste. Isso foi colocado apenas para o exemplo ficar previsível toda vez que rodar. Em uma aplicação real, normalmente isso não ficaria no código, porque apagaria os dados do usuário ou do sistema.
A função criarServidor representa o Create. Ela recebe a conexão com o banco e os dados do servidor, monta o comando INSERT e salva o registro.
A função listarServidores representa o Read. Ela faz um SELECT, percorre os resultados e imprime cada servidor.
A função listarServidoresPorStatus também é Read, mas com filtro. Ela usa WHERE status = ? para buscar apenas servidores com um status específico.
A função atualizarStatusServidor representa o Update. Ela muda o status de um servidor usando o id, porque o id identifica exatamente qual linha deve ser alterada.
A função apagarServidor representa o Delete. Ela remove um servidor pelo id, evitando apagar outras linhas sem querer.