Conheça o Rocketseat Para Empresas
Oferecemos soluções personalizadas para empresas de todos os portes.
Você já se deparou com a necessidade de conectar sua aplicação Node.js a um banco de dados Oracle? Se a resposta for sim, você sabe que o desafio pode ser real. O Oracle Database é um gigante no mundo corporativo, conhecido por sua robustez e segurança. Ele está no coração de sistemas financeiros, governamentais e de grandes empresas.
Por outro lado, temos o Node.js: leve, rápido e perfeito para construir APIs modernas e microsserviços. A combinação dessas duas tecnologias pode parecer inusitada à primeira vista, especialmente se você vem de ecossistemas como PostgreSQL ou MySQL.
Muitos desenvolvedores Node.js sentem um frio na espinha ao ouvir "Oracle". Mas essa integração não precisa ser um bicho de sete cabeças.
Aqui e agora, vamos desmistificar o uso do Oracle com Node.js. Usaremos o
node-oracledb, o driver oficial mantido pela Oracle, para construir uma API REST eficiente e escalável. Do setup inicial ao connection pooling e gerenciamento de transações.Preparado para adicionar essa skill valiosa ao seu acervo? Vamos começar.
Pré-requisitos e configuração do ambiente
Antes de mergulharmos no código, precisamos garantir que nosso ambiente esteja pronto. Você precisará de:
- Node.js e NPM instalados: recomendamos usar a versão LTS mais recente (v18+).
- Conhecimento básico de Node.js e Express: você deve estar confortável criando rotas e entendendo middlewares.
- Acesso a uma instância Oracle Database: pode ser local (via Docker), Oracle Cloud (Free Tier) ou um servidor da sua empresa.
Estrutura inicial do projeto
Vamos criar a estrutura do nosso projeto. Abra seu terminal e execute:
mkdir node-oracle-api cd node-oracle-api npm init -y npm install express dotenv oracledb
Instalamos as dependências necessárias:
express: nosso framework web.
dotenv: para gerenciar variáveis de ambiente (essencial para segurança).
oracledb: o driver que fará a mágica acontecer.
Variáveis de ambiente
Nunca coloque credenciais de banco de dados diretamente no seu código. Crie um arquivo
.env na raiz do projeto para armazená-las:# .env DB_USER=seu_usuario DB_PASSWORD=sua_senha_super_secreta DB_CONNECT_STRING=localhost:1521/XE # Exemplo para Oracle Cloud pode envolver wallet_location
O
DB_CONNECT_STRING define onde seu banco está localizado. O formato comum é host:porta/nome_do_servico.Entendendo o driver node-oracledb
O
node-oracledb é o driver oficial para conectar Node.js ao Oracle Database, mantido ativamente pela Oracle. Ele oferece uma API JavaScript limpa, baseada em Promises e async/await.A parte mais importante para entender como o driver funciona é a diferença entre os modos Thin e Thick.
Modo Thin (padrão atual)
A partir da versão 6 do
node-oracledb, o modo Thin é o padrão.- Características: 100% JavaScript. Implementa o protocolo de rede do Oracle diretamente.
- Vantagens:
- Instalação extremamente simples (apenas
npm install oracledb). - Não requer bibliotecas externas (como o Oracle Instant Client).
- Facilita o deploy em ambientes containerizados e serverless.
- Limitações: não suporta alguns recursos avançados do Oracle, como Application Continuity ou algumas configurações de autenticação avançada.
Modo Thick
O modo Thick era o padrão anterior e utiliza as bibliotecas do Oracle Client.
- Características: depende de bibliotecas C (Oracle Instant Client).
- Vantagens:
- Suporte total aos recursos avançados do Oracle Database.
- Desvantagens:
- Setup mais complexo, exigindo a instalação e configuração do Oracle Client no ambiente de execução.
Quando usar cada abordagem?
A recomendação oficial é: sempre comece com o modo Thin. Ele cobre a vasta maioria dos casos de uso de uma API Node.js e simplifica drasticamente a configuração. Use o modo Thick apenas se precisar explicitamente de um recurso avançado que o Thin não suporta.
Neste material, focaremos no modo Thin.
Primeira conexão com Oracle Database
Vamos criar um script simples para testar a conectividade.
Crie um arquivo chamado
testConnection.js:// testConnection.js require('dotenv').config(); const oracledb = require('oracledb'); const dbConfig = { user: process.env.DB_USER, password: process.env.DB_PASSWORD, connectString: process.env.DB_CONNECT_STRING }; async function runTest() { let connection; try { console.log('Tentando conectar ao Oracle Database...'); // Estabelecendo a conexão direta connection = await oracledb.getConnection(dbConfig); console.log('Conexão estabelecida com sucesso!'); // Executando uma query simples const result = await connection.execute( `SELECT 'Hello from Oracle, ' || TO_CHAR(SYSDATE, 'DD-MM-YYYY HH24:MI:SS') AS message FROM dual` ); console.log('Resultado da Query:', result.rows[0]); } catch (err) { console.error('Erro ao conectar ou executar a query:', err.message); } finally { if (connection) { try { // Sempre feche a conexão quando terminar de usá-la await connection.close(); console.log('Conexão fechada.'); } catch (err) { console.error('Erro ao fechar a conexão:', err.message); } } } } runTest();
Execute com
node testConnection.js.🚨 Atenção: o bloco
finally é crucial. Conexões de banco de dados são recursos caros. Se você não as fechar, sua aplicação rapidamente esgotará os recursos disponíveis.🔍 Debug: problemas comuns de conexão
- ORA-12154: verifique seu
DB_CONNECT_STRING. O host ou o nome do serviço está incorreto.
- ORA-01017: credenciais incorretas. Verifique
DB_USEReDB_PASSWORD.
- ORA-12541: o banco de dados não está rodando ou a porta está bloqueada por um firewall.
Configurando Connection Pooling
Embora a conexão direta funcione, ela é ineficiente para uma API real. Criar uma nova conexão para cada requisição HTTP é lento e consome muitos recursos.
A solução é o Connection Pooling.
Um Connection Pool mantém um "estoque" de conexões abertas e prontas para uso. Quando sua aplicação precisa fazer uma query, ela pega emprestada uma conexão do pool, usa e devolve. Isso é drasticamente mais rápido.
Implementação de Pool com node-oracledb
Vamos criar um módulo dedicado para gerenciar nosso pool. Crie um arquivo
src/config/database.js:// src/config/database.js require('dotenv').config(); const oracledb = require('oracledb'); // Configurações recomendadas oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT; // Retorna resultados como objetos JS const poolConfig = { user: process.env.DB_USER, password: process.env.DB_PASSWORD, connectString: process.env.DB_CONNECT_STRING, // Configurações de otimização do pool poolMin: 10, // Mínimo de conexões mantidas abertas poolMax: 50, // Máximo de conexões permitidas poolIncrement: 5, // Quantidade de novas conexões abertas quando necessário poolTimeout: 60 // Tempo (segundos) que conexões inativas podem ficar abertas }; async function initialize() { console.log('Inicializando o Connection Pool do Oracle...'); try { await oracledb.createPool(poolConfig); console.log('Connection Pool criado com sucesso.'); } catch (err) { console.error('Erro ao inicializar o pool:', err); throw err; } } async function close() { console.log('Encerrando o Connection Pool...'); try { // Fecha o pool padrão, dando 10 segundos para conexões em uso terminarem await oracledb.getPool().close(10); console.log('Connection Pool encerrado.'); } catch (err) { console.error('Erro ao encerrar o pool:', err); throw err; } } // Função auxiliar para executar queries simplificadas async function simpleExecute(statement, binds = [], opts = {}) { let connection; let result; // Define opções padrão se não forem fornecidas opts.autoCommit = opts.autoCommit !== undefined ? opts.autoCommit : true; // Padrão: autoCommit para operações simples try { // Pega uma conexão do pool connection = await oracledb.getConnection(); // Executa a query result = await connection.execute(statement, binds, opts); } catch (err) { console.error('Erro na execução da query:', err); throw err; } finally { if (connection) { try { // Libera a conexão de volta para o pool await connection.close(); } catch (err) { console.error('Erro ao liberar conexão:', err); } } } return result; } module.exports = { initialize, close, simpleExecute, oracledb // Exportamos o oracledb para acesso a constantes (como BIND_OUT) };
Gerenciamento do ciclo de vida do Pool
É crucial inicializar o pool quando a aplicação inicia e fechá-lo graciosamente quando ela termina.
Crie o arquivo principal
src/server.js:// src/server.js const express = require('express'); const db = require('./config/database'); const app = express(); const port = process.env.PORT || 3000; app.use(express.json()); // ... Rotas serão adicionadas aqui ... async function startup() { try { await db.initialize(); // Inicializa o pool antes de aceitar conexões app.listen(port, () => { console.log(`Servidor rodando na porta ${port}`); }); } catch (err) { console.error('Falha ao iniciar a aplicação:', err); process.exit(1); } } // Gerenciamento de desligamento async function shutdown(e) { let err = e; console.log('Desligando a aplicação...'); try { await db.close(); } catch (e) { console.error('Erro no desligamento do DB:', e); err = err || e; } process.exit(err ? 1 : 0); } process.on('SIGTERM', () => { console.log('Recebido SIGTERM'); shutdown(); }); process.on('SIGINT', () => { console.log('Recebido SIGINT (Ctrl+C)'); shutdown(); }); startup();
📊 Performance: a configuração do pool (poolMin, poolMax) é vital. Monitore o uso do pool em produção para ajustar esses valores. Um pool muito pequeno causa espera; um pool muito grande pode sobrecarregar o banco de dados.
Implementando operações CRUD
Vamos construir as operações CRUD para uma entidade de exemplo,
Funcionarios.Preparando a tabela
Assumindo que você tenha uma tabela similar a esta:
CREATE TABLE Funcionarios ( id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, nome VARCHAR2(100) NOT NULL, cargo VARCHAR2(50), salario NUMBER(10, 2), data_admissao DATE DEFAULT SYSDATE );
Vamos criar um módulo
src/models/funcionarioModel.js.Foco na segurança: Bind Variables
🚨 Atenção: nunca concatene strings para montar suas queries SQL com dados vindos do usuário. Isso abre portas para ataques de SQL Injection. Sempre use Bind Variables (indicados por dois pontos, como
:id).// src/models/funcionarioModel.js const db = require('../config/database'); // READ (Listar e Buscar por ID) async function find(context) { let query = `SELECT id, nome, cargo, salario, data_admissao "dataAdmissao" FROM Funcionarios`; const binds = {}; if (context && context.id) { binds.id = context.id; query += ` WHERE id = :id`; } else { query += ` ORDER BY data_admissao DESC`; } const result = await db.simpleExecute(query, binds); return result.rows; } module.exports.find = find;
CREATE (inserção de dados)
Para inserções, precisamos obter o ID gerado. O Oracle facilita isso com a cláusula
RETURNING INTO.// src/models/funcionarioModel.js (Continuação) const createSql = ` INSERT INTO Funcionarios (nome, cargo, salario, data_admissao) VALUES (:nome, :cargo, :salario, :data_admissao) RETURNING id INTO :id_out`; async function create(func) { const funcionario = { ...func }; // Definindo o bind de saída para o ID const binds = { nome: funcionario.nome, cargo: funcionario.cargo, salario: funcionario.salario, data_admissao: funcionario.data_admissao ? new Date(funcionario.data_admissao) : null, id_out: { dir: db.oracledb.BIND_OUT, type: db.oracledb.NUMBER } }; // Passamos { autoCommit: true } como padrão na simpleExecute const result = await db.simpleExecute(createSql, binds); // Recuperando o ID gerado funcionario.id = result.outBinds.id_out[0]; return funcionario; } module.exports.create = create;
⚡ Dica pro: ao trabalhar com datas no Oracle, o node-oracledb converte automaticamente objetos Date do JavaScript para tipos de data/hora do Oracle. Certifique-se de que os dados de entrada sejam convertidos corretamente para Date.
UPDATE e DELETE
// src/models/funcionarioModel.js (Continuação) // UPDATE const updateSql = ` UPDATE Funcionarios SET nome = :nome, cargo = :cargo, salario = :salario WHERE id = :id`; async function update(id, func) { const funcionario = { ...func, id: id }; const result = await db.simpleExecute(updateSql, funcionario); if (result.rowsAffected && result.rowsAffected === 1) { return funcionario; } else { return null; } } module.exports.update = update; // DELETE const deleteSql = `DELETE FROM Funcionarios WHERE id = :id`; async function del(id) { const binds = { id: id }; const result = await db.simpleExecute(deleteSql, binds); return result.rowsAffected === 1; } module.exports.del = del;
Construindo uma API REST completa
Agora vamos conectar a lógica de CRUD com o Express, mantendo uma arquitetura limpa com separação de responsabilidades (Rotas, Controllers, Models).
Controllers
O controller lida com a requisição HTTP, chama o modelo e formata a resposta.
Crie
src/controllers/funcionarioController.js:// src/controllers/funcionarioController.js const FuncionarioModel = require('../models/funcionarioModel'); // GET /funcionarios e GET /funcionarios/:id async function get(req, res, next) { try { const context = {}; if (req.params.id) { context.id = parseInt(req.params.id, 10); } const rows = await FuncionarioModel.find(context); if (context.id) { if (rows.length === 1) { res.status(200).json(rows[0]); } else { res.status(404).json({ message: 'Funcionário não encontrado' }); } } else { res.status(200).json(rows); } } catch (err) { next(err); } } // POST /funcionarios async function post(req, res, next) { try { // Em um projeto real, valide os dados de entrada aqui (ex: Joi ou Zod) const funcionario = { nome: req.body.nome, cargo: req.body.cargo, salario: req.body.salario, data_admissao: req.body.data_admissao }; const resultado = await FuncionarioModel.create(funcionario); res.status(201).json(resultado); } catch (err) { next(err); } } // PUT /funcionarios/:id async function put(req, res, next) { try { const id = parseInt(req.params.id, 10); const funcionario = { nome: req.body.nome, cargo: req.body.cargo, salario: req.body.salario }; const sucesso = await FuncionarioModel.update(id, funcionario); if (sucesso) { res.status(200).json(sucesso); } else { res.status(404).end(); } } catch (err) { next(err); } } // DELETE /funcionarios/:id async function del(req, res, next) { try { const id = parseInt(req.params.id, 10); const sucesso = await FuncionarioModel.del(id); if (sucesso) { res.status(204).end(); // No Content } else { res.status(404).end(); } } catch (err) { next(err); } } module.exports = { get, post, put, del };
Rotas e Integração
Crie
src/routes/funcionarioRoutes.js:// src/routes/funcionarioRoutes.js const express = require('express'); const router = express.Router(); const funcionarioController = require('../controllers/funcionarioController'); router.get('/:id?', funcionarioController.get); router.post('/', funcionarioController.post); router.put('/:id', funcionarioController.put); router.delete('/:id', funcionarioController.del); module.exports = router;
Finalmente, registre as rotas e um middleware de erro no
src/server.js:// src/server.js (Atualizado) const express = require('express'); const db = require('./config/database'); const funcionarioRoutes = require('./routes/funcionarioRoutes'); // Importar as rotas const app = express(); // ... (setup inicial) app.use(express.json()); // Registrar as rotas da API app.use('/api/funcionarios', funcionarioRoutes); // Middleware de tratamento de erros genérico app.use((err, req, res, next) => { console.error(err.stack); // Verificar se é um erro do Oracle (ex: violação de constraint) if (err.errorNum) { res.status(500).json({ message: "Erro no banco de dados", oracleError: err.message, code: err.errorNum }); } else { res.status(500).json({ message: "Erro interno do servidor" }); } }); // ... (startup e shutdown permanecem o mesmo)
Sua API REST completa integrada com Oracle está pronta!
Gerenciamento de transações
Em aplicações reais, muitas operações envolvem modificações em múltiplas tabelas. Se uma dessas operações falhar, precisamos garantir que todas as outras sejam desfeitas para manter a integridade dos dados (ACID).
Nossa função
simpleExecute usa autoCommit: true por padrão, o que é ótimo para operações únicas. Para transações complexas, precisamos de controle manual sobre COMMIT e ROLLBACK.Implementando transações manuais
Para operações transacionais, não podemos usar
simpleExecute, pois precisamos manter a mesma conexão durante todas as operações.Vamos simular um cenário de transferência de fundos entre duas contas (assumindo uma tabela
Contas).// Exemplo de função de serviço com transação manual const oracledb = require('oracledb'); // Usamos o oracledb direto aqui async function transferenciaBancaria(contaOrigemId, contaDestinoId, valor) { let connection; try { // 1. Obter conexão do pool (sem usar simpleExecute) connection = await oracledb.getConnection(); // Opções para execução sem autoCommit const options = { autoCommit: false }; // 2. Débito na origem const sqlDebito = ` UPDATE Contas SET saldo = saldo - :valor WHERE id = :idOrigem AND saldo >= :valor`; const resultDebito = await connection.execute(sqlDebito, { valor, idOrigem: contaOrigemId }, options); if (resultDebito.rowsAffected === 0) { throw new Error('Saldo insuficiente ou conta de origem não encontrada.'); } // 3. Crédito no destino const sqlCredito = ` UPDATE Contas SET saldo = saldo + :valor WHERE id = :idDestino`; await connection.execute(sqlCredito, { valor, idDestino: contaDestinoId }, options); // 4. Commit da Transação // Se tudo deu certo até aqui, confirmamos as mudanças await connection.commit(); return { success: true }; } catch (err) { console.error('Erro na transação:', err.message); // 5. Rollback da Transação if (connection) { try { await connection.rollback(); } catch (e) { console.error('Erro ao executar rollback:', e); } } throw err; // Propaga o erro original } finally { // 6. Liberar Conexão (Crucial!) if (connection) { try { await connection.close(); } catch (err) { console.error('Erro ao fechar conexão após transação:', err); } } } }
O fluxo
try -> commit, catch -> rollback, finally -> close é o padrão ouro para gerenciar transações.Performance e otimização
A integração Node.js e Oracle pode ser extremamente rápida se configurada corretamente.
Tuning de parâmetros do driver (fetch tuning)
Quando você executa uma query que retorna muitas linhas, o driver não busca todas de uma vez. Ele faz viagens de ida e volta (round-trips) para o banco de dados. Reduzir essas viagens melhora a performance.
O
node-oracledb oferece dois parâmetros principais:prefetchRows: quantas linhas o driver deve buscar antecipadamente e armazenar em um buffer interno.
fetchArraySize: quantas linhas são retornadas do buffer para a aplicação JavaScript em cada lote.
Você pode configurá-los globalmente ou por consulta:
// Globalmente (no database.js) oracledb.prefetchRows = 500; oracledb.fetchArraySize = 500; // Por consulta await connection.execute( `SELECT * FROM tabela_grande`, [], { fetchArraySize: 1000 } );
Conheça o Rocketseat Para Empresas
Oferecemos soluções personalizadas para empresas de todos os portes.
Artigos_
Explore conteúdos relacionados
Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.
NewsletterReceba conteúdos inéditos e novidades gratuitamente
