Introdução ao Banco de Dados
Um banco de dados é um lugar usado para guardar informações de forma organizada, principalmente quando esses dados precisam ser consultados, alterados, removidos ou relacionados com outros dados depois. Até poderíamos salvar informações em um arquivo de texto, JSON ou CSV, e isso funciona em casos pequenos, mas começa a ficar ruim quando a aplicação precisa procurar registros específicos, evitar duplicação, atualizar apenas uma parte dos dados ou garantir que cada item tenha uma identificação única.
Imagine um sistema simples para controlar servidores. Em vez de deixar tudo solto em um arquivo assim:
Servidor: web01
IP: 192.168.0.10
Status: ativo
Sistema: Ubuntu
O banco organiza essas informações em uma estrutura parecida com uma tabela:
| id | nome | ip | status | sistema |
|---|---|---|---|---|
| 1 | web01 | 192.168.0.10 | ativo | Ubuntu |
| 2 | db01 | 192.168.0.20 | ativo | Debian |
| 3 | old01 | 192.168.0.30 | inativo | CentOS |
Nesse exemplo, o banco é responsável por guardar os dados dos servidores de forma persistente, ou seja, os dados continuam existindo mesmo depois que o programa fecha. Isso é diferente de uma variável, que existe apenas enquanto o programa está rodando.
Tabela
A tabela representa um conjunto de dados do mesmo tipo. Em um sistema interno, por exemplo, poderia existir uma tabela para usuários, outra para produtos, outra para servidores, outra para logs e outra para backups.
Cada tabela guarda registros relacionados ao mesmo assunto, porque isso deixa os dados mais fáceis de entender e também evita misturar informações que têm finalidades diferentes. Uma tabela chamada usuarios poderia ser assim:
| id | nome | status | |
|---|---|---|---|
| 1 | Fulano | fulano@email.com | ativo |
| 2 | Ana | ana@email.com | ativo |
| 3 | Carlos | carlos@email.com | bloqueado |
A tabela usuarios é usada para guardar usuários. A tabela servidores seria usada para guardar servidores. Essa separação existe porque cada tipo de dado tem campos próprios e regras próprias.
Coluna
A coluna representa um campo da tabela. Na tabela usuarios, por exemplo, as colunas são id, nome, email e status. Cada coluna define qual informação pode ser guardada naquele lugar.
Em SQL, a criação dessa tabela poderia ficar assim:
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
nome TEXT,
email TEXT,
status TEXT
);
O comando CREATE TABLE usuarios cria uma tabela chamada usuarios. A coluna id, que usa o tipo INTEGER, é usada para guardar um número inteiro. O trecho PRIMARY KEY define essa coluna como chave primária, que representa o identificador único de cada registro. As colunas nome, email e status usam o tipo TEXT, porque são campos de texto.
Essa estrutura é parecida com a ideia de definir quais chaves vão existir em um dicionário no Python ou em um Map no Dart, mas com uma diferença importante. No banco, essa estrutura fica registrada e o banco passa a seguir esse formato ao guardar os dados.
Em Python, um usuário poderia ser representado assim:
usuario = {
"id": 1,
"nome": "Fulano",
"email": "fulano@email.com",
"status": "ativo"
}
Em Dart, a mesma ideia usando Map ficaria assim:
final usuario = {
'id': 1,
'nome': 'Fulano',
'email': 'fulano@email.com',
'status': 'ativo',
};
Mais para frente, esse Map vai ser útil porque muitas bibliotecas de banco em Dart trabalham com esse formato ao inserir ou ler dados.
Linha ou registro
A linha representa um registro dentro da tabela. Se a tabela é usuarios, cada linha representa um usuário. Se a tabela é servidores, cada linha representa um servidor.
| id | nome | status | |
|---|---|---|---|
| 1 | Fulano | fulano@email.com | ativo |
Na tabela acima, existe um registro de usuário. Esse registro tem um id, um nome, um email e um status. O banco guarda esses valores na mesma linha porque eles pertencem ao mesmo usuário. Em uma tabela de servidores, a ideia seria igual:
| id | nome | ip | status |
|---|---|---|---|
| 1 | web01 | 192.168.0.10 | ativo |
Essa linha representa o servidor web01.
Chave primária
A chave primária é o campo usado para identificar um registro de forma única. Normalmente esse campo se chama id, porque fica simples de entender e funciona bem na maioria dos sistemas.
Isso existe porque nem sempre os outros dados são únicos. Dois usuários podem ter o mesmo nome, dois produtos podem ter descrições parecidas e dois servidores podem seguir um padrão de nome parecido. O id evita confusão, porque cada registro tem um identificador próprio, exemplo:
| id | nome | |
|---|---|---|
| 1 | Fulano | fulano@email.com |
| 2 | Fulano | outro@email.com |
Mesmo que o nome seja igual, o banco sabe que são registros diferentes, porque o id é diferente. Na prática, quando uma aplicação precisa atualizar um usuário, o ideal é atualizar pelo id, não pelo nome. Isso evita alterar o registro errado.
Um exemplo de alteração de dados em SQL seria assim:
UPDATE usuarios
SET status = 'bloqueado'
WHERE id = 1;
O comando UPDATE usuarios informa que a tabela usuarios será alterada. O trecho SET status = 'bloqueado' define o novo valor da coluna status. O WHERE id = 1 limita a alteração ao usuário com id igual a 1, e isso é essencial, porque sem o WHERE o banco poderia alterar todos os registros da tabela.
Relacionamento
O Relacionamento é quando uma tabela aponta para outra. Isso é usado quando uma informação depende de outra, mas não faz sentido repetir os mesmos dados várias vezes. Imagine uma tabela de usuários:
| id | nome |
|---|---|
| 1 | Fulano |
| 2 | Ana |
E uma tabela de servidores:
| id | nome | ip | usuario_id |
|---|---|---|---|
| 1 | web01 | 192.168.0.10 | 1 |
| 2 | db01 | 192.168.0.20 | 1 |
| 3 | app01 | 192.168.0.30 | 2 |
A coluna usuario_id, que faz parte da tabela servidores, guarda o id de um usuário. Nesse caso, os servidores web01 e db01 pertencem ao usuário Fulano, enquanto o servidor app01 pertence à usuária Ana, isso evita repetição.
Sem relacionamento, a tabela de servidores poderia acabar guardando o nome e o email do dono em todos os servidores, e isso fica ruim quando algum dado precisa mudar. Se o email do Fulano mudar, seria necessário alterar várias linhas. Com relacionamento, os dados do Fulano ficam na tabela usuarios, e a tabela servidores guarda apenas a referência para ele.
Esse é um dos motivos pelos quais banco relacional é tão usado em sistemas administrativos, financeiros, internos e de controle. Ele ajuda a organizar dados que possuem ligação entre si.
Tipos de banco de dados
Existem vários tipos de banco de dados, mas para começar com Dart o mais importante é entender a diferença entre banco relacional e banco não relacional. Essa diferença muda a forma como os dados são organizados, como as consultas são feitas e também o tipo de problema que cada banco costuma resolver melhor.
Banco relacional
Um banco relacional é um banco que organiza os dados em tabelas, usando linhas, colunas, chaves primárias e relacionamentos. Ele é chamado de relacional porque uma tabela pode se relacionar com outra, como já vimos na introdução.
Esse tipo de banco é muito usado em sistemas onde os dados precisam ter estrutura mais definida, como cadastro de usuários, produtos, pedidos, contas, pagamentos, servidores, backups e logs estruturados.
Alguns bancos relacionais comuns são:
- SQLite
- PostgreSQL
- MySQL
- MariaDB
Eles usam SQL, que é a linguagem usada para criar tabelas, inserir dados, consultar registros, atualizar informações e apagar dados.
Banco não relacional
Um banco não relacional, também chamado de NoSQL, não organiza os dados obrigatoriamente em tabelas com linhas e colunas. Ele pode guardar dados em formatos diferentes, como documentos, chave e valor, grafos ou colunas distribuídas.
Um exemplo comum é o banco orientado a documentos, como MongoDB. Nele, um usuário poderia ser salvo em um formato parecido com JSON:
{
"id": 1,
"nome": "Fulano",
"email": "fulano@email.com",
"servidores": [
{
"nome": "web01",
"ip": "192.168.0.10"
},
{
"nome": "db01",
"ip": "192.168.0.20"
}
]
}
Perceba que, nesse caso, os servidores estão dentro do próprio documento do usuário. Não existe obrigatoriamente uma tabela separada de usuarios e outra de servidores.
Isso pode ser útil quando os dados variam muito de formato ou quando a aplicação trabalha com documentos grandes e flexíveis. Por outro lado, quando existem muitos relacionamentos e regras entre os dados, o banco relacional costuma ser mais simples de manter e entender.
Para o nosso caso, como o foco é aprender banco de dados com Dart de forma progressiva, o ideal é começar com banco relacional, porque ele ensina melhor os fundamentos de tabela, coluna, linha, chave primária, relacionamento e SQL.
SQLite
O SQLite é um banco relacional leve, que funciona em um arquivo local. Ele não precisa de um servidor rodando separado, então a aplicação acessa o banco diretamente pelo arquivo.
Ele é diferente de PostgreSQL ou MySQL, que normalmente rodam como serviços no sistema. O SQLite é muito usado em aplicações locais, scripts, ferramentas internas, aplicativos offline e sistemas pequenos. Ele é uma ótima escolha para começar porque reduz a quantidade de coisas para configurar, não precisa criar usuário de banco, liberar porta, subir serviço, configurar autenticação ou mexer com rede. O banco fica em um arquivo e a aplicação Dart acessa esse arquivo.
PostgreSQL
O PostgreSQL é um banco relacional mais completo, usado em aplicações maiores, sistemas web, APIs, serviços internos e ambientes onde vários usuários ou várias aplicações precisam acessar o mesmo banco.
Diferente do SQLite, o PostgreSQL roda como um serviço. A aplicação Dart não acessa um arquivo diretamente. Ela se conecta ao PostgreSQL usando host, porta, usuário, senha e nome do banco.
O PostgreSQL é indicado quando o sistema precisa de mais controle, mais segurança, múltiplas conexões ao mesmo tempo, permissões, usuários, backup mais estruturado, replicação, transações mais pesadas e recursos avançados de SQL.
Nesse caso, SQLite já não seria a melhor escolha, porque ele foi feito para uso local e simples. O PostgreSQL faz mais sentido quando o banco precisa ficar centralizado e ser acessado por uma aplicação em produção.
MySQL e MariaDB
O MySQL também é um banco relacional muito usado, principalmente em aplicações web. Já o MariaDB é um fork do MySQL, ou seja, nasceu a partir do MySQL e mantém bastante compatibilidade com ele. Na prática, os dois aparecem muito em hospedagens, sistemas PHP, WordPress, aplicações web tradicionais e sistemas internos. Assim como PostgreSQL, eles rodam como serviço e a aplicação se conecta usando rede.
Tanto o MySQL quanto o MariaDB usam SQL, então boa parte dos conceitos aprendidos com SQLite continua valendo. Tabelas, colunas, registros, SELECT, INSERT, UPDATE, DELETE, chave primária e relacionamento continuam existindo. A diferença está nos detalhes do banco, nos recursos disponíveis, na configuração, no tipo de dado, no comportamento de algumas consultas e na forma como cada banco lida com permissões, usuários, transações e desempenho.
Diferença prática entre eles
O SQLite é mais simples porque o banco fica em um arquivo local. Ele é bom para aprender, criar ferramentas pequenas, scripts, apps offline e aplicações que não precisam de um banco central acessado por várias pessoas ao mesmo tempo.
O PostgreSQL é mais completo e mais indicado para aplicações maiores, APIs, sistemas em produção e casos onde várias conexões acessam o mesmo banco.
O MySQL e MariaDB também são bancos relacionais usados em produção, muito comuns em sistemas web e hospedagens. Eles cumprem um papel parecido com o PostgreSQL em muitos cenários, embora cada um tenha suas próprias características.
Criptografia no SQLite
Quero adiantar algo um pouco avançado, que é o uso de criptografia em banco SQLite. Podemos usar o SQLCipher, com ele, o banco é aberto com uma senha/chave. Sem essa senha/chave, o arquivo não abre como um banco SQLite normal.
Não confunda login, biometria ou PIN (que é solicitado na aplicação do celular) como proteção no banco de dados, isso protege o acesso pela interface do app, mas não significa que o arquivo SQLite esteja criptografado. Se o banco estiver em SQLite comum, ele continua sendo um arquivo local que pode ser lido com ferramentas de SQLite caso alguém consiga acesso ao arquivo.
No Flutter, normalmente não se usa o SQLite diretamente "na mão", é mais comum usar um pacote que faz a ponte entre o código Dart e a implementação nativa do SQLite no Android/iOS. Para SQLite comum, um pacote bastante usado é o sqflite, que é um plugin Flutter para SQLite.
Para SQLite criptografado, existe o sqflite_sqlcipher, que é um fork do sqflite e usa SQLCipher no Android e no iOS. A API dele é parecida com a do sqflite, mas o openDatabase recebe um parâmetro password, que é usado para abrir o banco criptografado.
Onde guardar a chave?
Não adianta criptografar o banco e deixar a senha fixa no código, o ideal é solicitar essa senha para que o usuário digite ela ou guardar ela de forma segura.
Algo bem comum é gerar uma chave aleatória na primeira execução do app e guardar essa chave em um local seguro do próprio sistema operacional, um pacote que podemos usar para isso é o flutter_secure_storage, que é usado para armazenar dados sensíveis em um formato chave/valor usando recursos seguros da plataforma, como iOS, Android, macOS, Windows e Linux.
ASsim, o banco fica criptografado com SQLCipher, e a chave do banco fica guardada no armazenamento seguro da plataforma.
Dart e banco de dados
Uma aplicação Dart não "fala" com o banco sozinha, ela precisa de uma biblioteca, também chamada de pacote ou driver, que é responsável por fazer a ponte entre o código Dart e o banco de dados. Isso acontece porque cada banco tem uma forma própria de receber comandos, abrir conexão, autenticar, executar SQL e devolver resultados.
Se o banco for SQLite, a aplicação normalmente acessa um arquivo local. Se o banco for PostgreSQL, MySQL ou MariaDB, a aplicação normalmente abre uma conexão de rede com um serviço de banco rodando em algum lugar.
A aplicação Dart precisa de um pacote
O Dart tem o gerenciador de pacotes pub, que é usado para adicionar bibliotecas ao projeto. Para banco de dados, isso é necessário porque o Dart puro não possui uma API única para conversar com todos os bancos. Para SQLite em Dart, existe o pacote sqlite3, que fornece bindings para SQLite usando dart:ffi em plataformas nativas.
Para Flutter, principalmente em Android e iOS, também é comum encontrar o sqflite, que é um plugin Flutter para SQLite. Já para PostgreSQL, existe o pacote postgres, que é um driver usado para conectar e executar consultas em bancos PostgreSQL.
A escolha do pacote depende do tipo de aplicação:
| Caso | Banco comum | Pacote comum |
|---|---|---|
| Script Dart local | SQLite | sqlite3 |
| App Flutter mobile | SQLite | sqflite ou sqflite_sqlcipher |
| API ou serviço Dart | PostgreSQL | postgres |
| App com banco centralizado | PostgreSQL/MySQL/MariaDB | driver específico do banco |
Conexão com banco local
Quando a aplicação usa SQLite, o banco fica em um arquivo local. Isso combina bem com aplicações pequenas, ferramentas internas, scripts, aplicativos offline e apps mobile.
Nesse caso, não existe um servidor de banco separado. O próprio app abre o arquivo do SQLite e executa os comandos nele.
Conexão com banco em servidor
Quando a aplicação usa PostgreSQL, MySQL ou MariaDB, o banco normalmente roda como um serviço. A aplicação Dart precisa saber onde esse banco está e quais credenciais usar.
Esse modelo faz mais sentido quando o banco precisa ser acessado por uma API, por vários usuários ou por mais de uma instância da aplicação. O banco fica centralizado, e a aplicação apenas se conecta nele.
Exemplo simples em Dart com SQLite
O exemplo abaixo usa o pacote sqlite3, que é uma forma simples de praticar banco com Dart sem precisar configurar PostgreSQL, MySQL ou MariaDB agora. Primeiro, em um projeto Dart, seria necessário adicionar o pacote:
dart pub add sqlite3
Depois, o arquivo bin/main.dart poderia ficar assim:
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('''
INSERT INTO servidores (nome, ip, status)
VALUES ('web01', '192.168.0.10', 'ativo');
''');
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();
}
A linha db.dispose(); fecha a conexão com o banco. Isso é importante porque conexões e arquivos abertos consomem recursos do sistema. Em aplicações maiores, a conexão costuma ser controlada por uma classe própria de banco, em vez de ficar aberta e fechada de qualquer jeito em várias partes do código.
Async e Await
O acesso ao banco geralmente é uma operação de entrada e saída, porque depende de arquivo, disco, rede ou serviço externo. Por isso, em Dart/Flutter, muitas bibliotecas trabalham com Future, async e await.
No exemplo com sqlite3, a API usada é mais direta e síncrona. Em Flutter com sqflite, por exemplo, é comum ver algo assim:
final db = await openDatabase('app.db');
O await aparece porque abrir o banco pode depender do sistema operacional, do arquivo local e da plataforma mobile. Isso evita travar a interface do app enquanto a operação acontece.