Skip to main content

Introdução a Banco de dados



Introdução a banco de dados


Um banco de dados é um sistema que armazena, organiza e permite recuperar informações de forma estruturada. Em essência, é como um grande arquivo digital que permite inserir, buscar, modificar e apagar dados com eficiência, geralmente por meio de uma linguagem de consulta (como SQL). Ele é usado para guardar qualquer tipo de dado: usuários, senhas, registros financeiros, conteúdos de um site, entre outros.


No Python, o trabalho com banco de dados pode ser feito de várias formas. O próprio Python traz bibliotecas nativas para se conectar com bancos, além de uma grande variedade de bibliotecas de terceiros. A forma mais comum é através de drivers (como sqlite3, psycopg2, mysql-connector-python) ou via ORMs (Object Relational Mappers), como o SQLAlchemy ou o Django ORM, que permitem trabalhar com os dados como se fossem objetos Python.


Exemplo com sqlite3:

import sqlite3

# Conectar no banco (cria o arquivo caso nao exista):
conn = sqlite3.connect('meubanco.db')

# "Abre o canal de comunicação" com o banco:
cursor = conn.cursor()

cursor.execute('CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nome TEXT)')
cursor.execute('INSERT INTO usuarios (nome) VALUES (?)', ('Bruno',))

conn.commit()
conn.close()


SQLite


O SQLite, é embutido na linguagem Python e pode ser usado ter que instalar nada a mais. Ela cria um arquivo local com o banco dentro desse arquivo. Apesar de ser prático e não exigir instalação de servidor, o SQLite não oferece autenticação por padrão, não é indicado para múltiplos acessos concorrentes e sua segurança é muito baixa, sendo ideal apenas para testes ou aplicativos locais sem informações sensíveis.


A proteção do arquivo deve ser realizada por meio do sistema de arquivos locais. Não use sqlite com informações sensíveis como senhas, nome de usuários, tokens e etc. em ambiente de produção.



PostgreSQL


O PostgreSQL é uma das opções mais robustas e seguras disponíveis, amplamente usado em ambientes de produção. Ele é gratuito, tem código aberto e oferece suporte avançado para tipos de dados, permissões detalhadas e criptografia tanto em repouso quanto em trânsito, desde que configurado corretamente.


No Python, a integração é feita principalmente com as bibliotecas psycopg2 (para conexões síncronas) ou asyncpg (para conexões assíncronas). Ele é altamente recomendado quando há necessidade de lidar com dados críticos ou sigilosos.



MySQL / MariaDB


O MySQL e sua versão mantida pela comunidade, o MariaDB, também são bastante populares. São rápidos, relativamente fáceis de configurar e possuem bom suporte no Python por meio dos módulos mysql-connector ou PyMySQL.


Apesar de terem evoluído bastante, alguns mecanismos de segurança ainda ficam atrás do PostgreSQL, especialmente em configurações padrão. Mesmo assim, são opções seguras para produção quando bem configuradas, com autenticação e uso de SSL.



MongoDB (banco NoSQL, orientado a documentos)


O MongoDB é uma alternativa NoSQL que armazena os dados em documentos no formato BSON, uma variação do JSON. Ele é útil quando não se quer seguir uma estrutura fixa de tabelas e colunas, permitindo flexibilidade na modelagem dos dados.


Pode ser usado em Python com a biblioteca pymongo. Porém, seu uso exige cuidados com segurança, é comum ver instâncias mal configuradas expostas na internet, sem autenticação ou criptografia. Para ambientes internos bem controlados, é seguro, mas não se deve expor diretamente à internet sem proteção.



Redis


O Redis, por sua vez, é um banco de dados em memória baseado em chave-valor. Ele é extremamente rápido, sendo mais usado como cache, sistema de filas, contador de acessos, entre outros usos temporários.


Sua persistência é opcional e limitada. Embora tenha suporte a autenticação e ACLs mais recentes, não é adequado para guardar informações sensíveis ou dados críticos, já que seu foco é performance e não segurança. No Python, você pode usá-lo com a biblioteca redis-py.



Recomendação


Se você está aprendendo ou criando um projeto pessoal, pode começar com SQLite, pois é simples e já vem com o Python. Mas para sistemas sérios ou produção, escolha:

  • PostgreSQL: se quiser segurança, estabilidade e recursos avançados.
  • MySQL/MariaDB: se já estiver familiarizado com eles ou precisar de algo mais simples que o PostgreSQL.
  • MongoDB: se seus dados forem dinâmicos e não estruturados, mas sempre configure autenticação e SSL.
  • Redis: apenas para cache ou tarefas temporárias.


Placeholders


Os Placeholders (ou marcadores de posição) são símbolos usados em instruções SQL para representar valores que serão inseridos depois, são normalmente usados ao executar comandos como INSERT, UPDATE, SELECT ou DELETE. Eles são usados para evitar SQL Injection e tornar o código mais limpo e seguro.


Imagine que você precise buscar um usuário com base no nome digitado por alguém:

# Código inseguro (vulnerável a SQL Injection!)
cursor.execute("SELECT * FROM usuarios WHERE nome = '%s'" % nome)

Se o nome recebido for algo como "'; DROP TABLE usuarios; --", você já imagina o estrago (esse código vai deletar a tabela de usuários).


Agora veja o exemplo usando placeholder, impedindo que alguém consiga fazer SQL injection:

cursor.execute("SELECT * FROM usuarios WHERE nome = ?", (nome,))

Isso faz o interpretador de SQL tratar o valor como dado e não como código, protegendo contra ataques e erros.



Sintaxes de placeholders


A sintaxe dos placeholders vai depender do banco/driver que você está usando.

O SQLite / Python (DB-API) usa ? como placeholder:

cursor.execute("INSERT INTO produtos (nome, preco) VALUES (?, ?)", ("Café", 10.50))

O PostgreSQL com psycopg2 usa %s como placeholder, mesmo para valores que não são strings:

cursor.execute("SELECT * FROM clientes WHERE id = %s", (42,))

O MySQL com MySQLdb ou pymysql também usa %s:

cursor.execute("UPDATE contas SET saldo = %s WHERE numero = %s", (500.00, "12345-6"))


Placeholders nomeados


Alguns drivers permitem o uso de placeholders com nome:

cursor.execute("SELECT * FROM usuarios WHERE nome = :nome", {"nome": "João"})

Ou, no psycopg2:

cursor.execute("SELECT * FROM usuarios WHERE nome = %(nome)s", {"nome": "João"})

Isso é útil quando há muitos campos e você quer deixar o código mais legível.



Cuidados comuns


Quando você usa um placeholder como ? (ou %s, ou :nome, dependendo do banco), você está indicando que ali vai entrar um valor, mas você não fornece o valor diretamente na string do banco.


Em vez disso, você precisa passar esses valores separadamente, e o método execute() precisa de uma estrutura onde ele possa encontrar os valores na ordem certa, para isso o placeholder espera algo iterável, como uma tupla ou lista.


Veja o código abaixo, vamos usar ele para explicar esse comportamento de usar lista ou tupla:

>>> nome = "bruno"

>>> lista = ['laranja', 1, 2, 'maca']

>>> tupla = ('menino', 1, 'menina')

Agora vamos simular o que o cursor.execute() faz quando você passa esses valores como argumento.


Como a variável nome é do tipo string (o valor armazenado na variável nome é uma string), ao ser passada como argumento para o execute(), o Python entende que você está passando uma sequência de caracteres:

cursor.execute("SELECT * FROM tabela WHERE campo = ?", nome)

Basicamente, o execute() fará o seguinte (é apenas uma forma de você entender, mas não é de fato o que acontece):

>>> tuple(nome)
("b", "r", "u", "n", "o")

E isso quebra, porque seu SQL só tem um ?, mas você está fornecendo cinco valores. Mesmo passando uma lista ou tupla, você deve garantir que elas possuem o número de argumentos corretos, no caso, o número de ? que você forneceu para o execute():

>>> tuple(lista)
('laranja', 1, 2, 'maca')

>>> tuple(tupla)
('menino', 1, 'menina')

No exemplo acima daria erro porque estamos fornecendo mais que dois argumentos, mas você pode entender que nesses casos, a quantidade de argumentos se mantém.


Portanto, sempre use tuplas ou listas ao passar os valores que serão inseridos no banco.

cursor.execute("...", (valor1, valor2))  # Correto
cursor.execute("...", [valor1, valor2]) # Também funciona

# Recomendado:
cursor.execute("...", tupla)
cursor.execute("...", lista)


execute() e executemany()


Os métodos execute() e executemany() são parte do padrão Python DB-API 2.0, que define como os drivers de banco de dados relacionais devem se comportar em Python. Isso significa que todos os drivers compatíveis com DB-API 2.0 (como os usados com SQLite, PostgreSQL, MySQL, Oracle, etc.) implementam esses métodos da mesma forma básica.



execute()


Usado para executar uma única instrução SQL com ou sem valores. Exemplos:

# Consulta sem parâmetros
cursor.execute("SELECT * FROM usuarios")

# Consulta com um parâmetro
cursor.execute("SELECT * FROM usuarios WHERE nome = ?", ("Fulano",))

executemany()


Usado para executar a mesma instrução SQL várias vezes, ou seja, é usado para inserir vários valores num único comando. Muito útil em INSERT, UPDATE ou DELETE em lote. Exemplo:

dados = [
("João", 25),
("Maria", 30),
("José", 22),
]

cursor.executemany("INSERT INTO usuarios (nome, idade) VALUES (?, ?)", dados)

Isso é mais rápido e eficiente do que chamar execute() repetidamente em um for.



fetchall


A função fetchall() é usada após a execução de um comando SELECT para recuperar todos os resultados da consulta de uma vez. Ela retorna uma lista de tuplas, onde cada tupla representa uma linha retornada pelo banco de dados.


Exemplo com sqlite3:

import sqlite3

conn = sqlite3.connect("exemplo.db")
cur = conn.cursor()

cur.execute("SELECT nome, preco FROM produtos")
resultados = cur.fetchall()

for linha in resultados:
print(linha)

Se a tabela produtos tiver três linhas, fetchall() retornará uma lista com três tuplas. Se não houver resultados, retornará uma lista vazia: [].


O fetchall() deve ser chamado depois de um SELECT, e só pode ser usado uma vez por execução, pois ele consome todos os dados da consulta. Se você tentar chamar novamente, o cursor já estará vazio.


Se quiser buscar linha por linha, pode usar fetchone() ou iterar diretamente sobre o cursor:

for linha in cur:
print(linha)

Então, fetchall() é útil para carregar tudo de uma vez quando você sabe que o volume de dados é pequeno ou moderado.



Close e commit


A função commit() é usada para salvar permanentemente no banco de dados todas as alterações feitas desde o início da transação. Já a função close() encerra a conexão com o banco de dados, liberando os recursos usados por ela.


Essas duas funções são fundamentais quando se trabalha com bancos de dados (como sqlite3, psycopg2, mysql.connector):

  1. commit() deve ser chamado antes de close() se você quiser que as alterações feitas por execute() ou executemany() sejam persistidas. Se você esquecer o commit() e fizer apenas o close(), as alterações serão descartadas.

  2. close() deve ser sempre chamado ao final do uso da conexão, mesmo que você não tenha feito alterações. Isso garante que os recursos da conexão sejam liberados corretamente. Ignorar o close() pode gerar travamentos, conexões penduradas ou vazamento de memória em sistemas maiores.


Exemplo completo com sqlite3:

import sqlite3

conn = sqlite3.connect("exemplo.db")
cur = conn.cursor()

cur.execute("INSERT INTO produtos (nome, preco) VALUES (?, ?)", ("borracha", 0.5))

conn.commit() # grava no banco
conn.close() # encerra a conexão

Portanto, a ordem correta sempre que houver alterações é: commit() seguido de close(). Se for apenas leitura, pode-se fazer direto o close(), mas nunca omitir essa finalização.



CRUD


O CRUD é o acrônimo para Create, Read, Update e Delete, as quatro operações básicas de persistência em banco de dados. Em Python, a forma mais correta e segura de fazer essas operações é usando placeholders com o cursor do banco, para evitar injeção de SQL e garantir clareza.


Vou usar sqlite3 como exemplo porque funciona sem instalar nada, mas esse padrão se aplica também ao psycopg2 (PostgreSQL), mysql-connector, entre outros. Abaixo está um CRUD completo, com cada operação bem clara e usando placeholders ? (que o sqlite3 aceita).



Conectando ao banco e criando a tabela


import sqlite3

conn = sqlite3.connect("dbtest.db")
cur = conn.cursor()

cur.execute("""
CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
preco REAL NOT NULL
)
""")

conn.commit()


CREATE (Inserir dados)


def inserir_produto(nome, preco):
cur.execute("INSERT INTO produtos (nome, preco) VALUES (?, ?)", (nome, preco))
conn.commit()

Uso:

inserir_produto("Caneta", 1.99)


READ (Buscar dados)


def listar_produtos():
cur.execute("SELECT id, nome, preco FROM produtos")
return cur.fetchall()

Uso:

for produto in listar_produtos():
print(produto)

# Ou:
_id produto_nome produto_preco = listar_produtos()
print(_id produto_nome produto_preco)


UPDATE (Atualizar dados)


def atualizar_preco(id_produto, novo_preco):
cur.execute("UPDATE produtos SET preco = ? WHERE id = ?", (novo_preco, id_produto))
conn.commit()

Uso:

atualizar_preco(1, 2.49)


DELETE (Remover dados)


def deletar_produto(id_produto):
cur.execute("DELETE FROM produtos WHERE id = ?", (id_produto,))
conn.commit()

Uso:

deletar_produto(1)


Finalizando a conexão


conn.close()

Esses exemplos cobrem todo o ciclo CRUD com segurança, clareza e com o uso de placeholders para proteger contra SQL injection.



DBEAVER


O DBeaver é uma ferramenta gráfica (GUI) de administração e desenvolvimento de bancos de dados. Ele é muito usado por profissionais de banco de dados, desenvolvedores e administradores de sistemas porque é multiplataforma, gratuito (na versão Community) e suporta diversos bancos de dados.


Como falei, o DBeaver suporta vários SGBDs com o mesmo painel/interface, como:

  • DB Relacional: PostgreSQL, MySQL, MariaDB, SQLite, Oracle, SQL Server, DB2, Firebird, Sybase, etc.
  • DB não relacional: MongoDB, Cassandra, Redis, etc. (alguns requerem plug-ins adicionais ou a versão Enterprise)

Graças a sua interface gráfica amigável temos uma visualização de tabelas, esquemas, views, procedures de forma simples, ou seja, podemos facilmente ver o que está ou não dentro do banco sem ter que utilizar CLI, o que torna a tarefa mais simples e rápida.


Também podemos fazer a edição de dados diretamente nas tabelas (como se fosse uma planilha), dentre outras atividades. É possível importar e exportar os dados (em CSV, Excel, SQL, JSON, etc.). Também podemos fazer backup e restore (quando o banco suportar).


Também possui plugins para Git, SSH, LDAP (dependendo da versão), dentre outras tarefas.



Instalação


É possível instalar o BDeaver no Linux, usando .deb, .rpm, ou .tar.gz, também pode ser instalado via Snap ou Flatpak. Possui uma versão para Windows e macOS.


Para instalar acesse o site oficial https://dbeaver.io/.



Docker


Você pode subir o MySQL com Docker de forma que seu código Python e ferramentas como DBeaver consigam se conectar facilmente. Abaixo vai um passo a passo claro e funcional. Vamos usar isso para começar a criar os projetos com as melhores práticas.



Criar um container MySQL com Docker CLI


Use o comando abaixo para subir um container com Mysql usando a linha de comando do Docker:

docker run -d \
--name mysql-dev \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=dbtest \
-e MYSQL_USER=usuario \
-e MYSQL_PASSWORD=senha123 \
-p 3306:3306 \
mysql:8.0

Esse comando faz o seguinte:

  • Cria um banco chamado dbtest
  • Cria o usuário usuario com senha senha123
  • Expõe a porta 3306 (acesso externo pelo Python e DBeaver)

Caso você queira, pode subir usando Docker Compose, crie o arquivo abaixo:

vim docker-compose.yaml

E coloque esse conteúdo no arquivo criado acima:

docker-compose.yaml
version: '3.8'

services:
mysql:
image: mysql:8.0
container_name: mysql-dev
#restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dbtest
MYSQL_USER: usuario
MYSQL_PASSWORD: senha123
TZ: America/Sao_Paulo
command:
- --authentication-policy=caching_sha2_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb_force_recovery=0
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql

volumes:
mysql_data:

Para subir o compose use o comando abaixo:

docker-compose up -d

Para conectar via DBeaver:

  • Host: localhost
  • Porta: 3306
  • Usuário: usuario
  • Senha: senha123
  • Banco: dbtest

Se você receber o erro Public Key Retrieval is not allowed no DBeaver, isso acontece com o driver do MySQL 8 quando você tenta conectar com autenticação baseada em senha, mas a opção de recuperação da chave pública não está habilitada. Para resolver isso no DBeaver faça:

Clique com o botão direito na conexão do banco criado (no banco que voce adicionou), vá em Edit Connection -> Driver Properties (aba da conexão). Encontre a propriedade allowPublicKeyRetrieval e altere o valor para true.


Pode ser necessário garantir que useSSL esteja como false, a não ser que você tenha SSL configurado.


Para conectar via Python (com mysql-connector-python), instale o driver:

pip3 install mysql-connector-python

# ou:
pip3 install pymysql

Código exemplo:

import mysql.connector

conn = mysql.connector.connect(
host="localhost",
user="usuario",
password="senha123",
database="dbtest"
)

cursor = conn.cursor()
cursor.execute("SELECT DATABASE()")
print(cursor.fetchone())

conn.close()


Usar dotenv


É mais que recomendado usar dotenv, para entender como ele funciona no Docker, consulte minha documentação sobre Docker



Dotenv em container

Quando usamos um arquivo .env com o docker-compose, as variáveis definidas ali são automaticamente passadas como variáveis de ambiente para dentro dos containers se assim definidos pelo environment:. Isso significa que, se o seu código Python estiver rodando dentro do container, ele pode acessar essas variáveis normalmente usando funções como os.getenv("NOME_DA_VARIAVEL"), sem precisar de nenhuma configuração adicional no código.


O sistema operacional do container já terá essas variáveis disponíveis no ambiente, porque o docker-compose cuidou disso ao fazer o build ou subir os serviços. No entanto, se o código Python estiver sendo executado fora do container, por exemplo, diretamente na sua máquina local, o Python não vai ter acesso automático ao .env, porque variáveis de ambiente não são mágicas, elas só existem dentro do processo que as carrega. Nesse caso, o Python precisa de uma biblioteca externa para ler o arquivo .env e injetar os valores no ambiente de execução.


Para isso, você deve instalar o pacote python-dotenv com pip3 install python-dotenv, e então carregar o .env manualmente no seu código usando:

from dotenv import load_dotenv
import os

load_dotenv() # Carrega as variáveis do .env para o os.environ
senha = os.getenv("MYSQL_PASSWORD")

Essa abordagem faz com que o Python, fora de um container, consiga simular o mesmo comportamento que teria dentro do ambiente gerenciado pelo docker-compose. Então, quando o código está dentro do container, basta usar os.getenv() normalmente. Quando está fora, você precisa carregar o .env com python-dotenv.



Subir o container e criar as tabelas


A melhor maneira de subir um container de banco de dados já com as tabelas criadas depende do banco que você está usando (MySQL, PostgreSQL, etc.), mas o conceito geral é o mesmo, executar scripts de criação no momento em que o container é iniciado pela primeira vez.


A seguir, vou te explicar como fazer isso corretamente com base no comportamento padrão das imagens oficiais do Docker.


As imagens oficiais do MySQL e MariaDB suportam automaticamente a execução de arquivos .sql e .sh que você coloca em um volume montado em /docker-entrypoint-initdb.d dentro do container. Ou seja, você pode criar um diretório chamado, por exemplo, ./initdb (na sua máquina, no mesmo diretório do docker-compose.yaml), e colocar dentro dele um ou mais arquivos .sql com os comandos de CREATE DATABASE, CREATE TABLE, INSERT, etc.


Estrutura do projeto:

.
├── docker-compose.yaml
├── .env
└── initdb/
└── schema.sql

Arquivo schema.sql de exemplo:

CREATE TABLE IF NOT EXISTS usuarios (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100),
email VARCHAR(100)
);

E no docker-compose.yaml:

version: '3.8'

services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./initdb:/docker-entrypoint-initdb.d:ro

volumes:
mysql_data:

Esse mecanismo de execução dos scripts só roda na primeira vez que o volume de dados está vazio. Se o volume mysql_data já tiver o banco inicializado, os scripts não serão executados novamente.


Você pode ter quantos arquivos .sql (ou .sh, .sql.gz) quiser dentro do diretório montado em /docker-entrypoint-initdb.d. O Docker executa todos eles automaticamente, mas há um detalhe importante, a ordem de execução segue a ordem alfabética dos nomes dos arquivos.


Ou seja, os arquivos são processados como se você rodasse um ls -1 /docker-entrypoint-initdb.d | sort e executasse cada um na sequência. Se você tiver os seguintes arquivos no seu diretório initdb:

01-create-database.sql
02-create-tables.sql
03-insert-initial-data.sql

Eles serão executados exatamente nessa ordem, o que garante que você pode controlar a sequência de criação do schema, tabelas e dados iniciais apenas nomeando os arquivos corretamente.


Se você não controlar a ordem com prefixos numéricos, como 01-, 02-, etc. o Docker ainda vai executar os arquivos, mas na ordem alfabética natural dos nomes, o que pode causar erro se um script depender de algo definido em outro que ainda não foi executado.



PostgreSQL

O PostgreSQL funciona da mesma forma, a imagem oficial também lê os arquivos .sql, .sql.gz e .sh em /docker-entrypoint-initdb.d na inicialização do container.



Mudando ambiente


É possível controlar qual arquivo .env será carregado com base em uma variável que define o ambiente, como ENV=prod, ENV=dev, ENV=test, mas o docker-compose não faz isso automaticamente. Por padrão, ele sempre tenta carregar um arquivo chamado .env, fixo.


No entanto, você pode contornar essa limitação de forma simples e eficiente com um script de inicialização ou uso direto da flag --env-file, que está disponível nas versões recentes do Compose.



Solução usando --env-file

Você pode criar diferentes arquivos .env:

  • .env.dev
  • .env.prod
  • .env.test

E ao rodar o docker-compose, você escolhe qual usar:

docker-compose --env-file .env.dev up

ou

docker compose --env-file .env.prod up -d

Você também pode automatizar com um script shell:

#!/bin/bash

ENV=${1:-dev}
ENV_FILE=".env.${ENV}"

if [ ! -f "$ENV_FILE" ]; then
echo "Arquivo $ENV_FILE não encontrado"
exit 1
fi

docker-compose --env-file "$ENV_FILE" up -d

Agora, ao rodar:

./up.sh prod

Ele vai usar .env.prod. E se você rodar sem argumentos:

./up.sh

Ele usa .env.dev.