Configurando o MongoDB como banco de dados
Nesse livro o banco de dados escolhido foi o MongdoDB, principalmente pela simplicidade de ingração e vasto suporte opensource.
Introdução ao MongoDB
Até o ano de 2009 o mundo dos banco de dados era dominado por bancos RDMS (Relational Database Management System) baseados em SQL (Structured Query Language). Esse tipo de banco de dados baseado em um modelo relacional foi introduzido em 1970 e é utilizado até hoje. Com o surgimento da Cloud e Big Data os bancos relacionais começaram a enfrentar os desafios de escalar devido ao design relacional. Há 50 anos, quando os bancos de dados relacionais foram criados, o desafio que era armazenar dados, armazenamento era caro e lento.
Nos dias de hoje o armazenamento de dados é extremamente barato e não é mais visto como um problema, o desafio agora é está no processamento de dados. Empresas como a NASA com o projeto de monitoramento climático e de aquecimento global precisam processar e armazenar petabytes de dados. Pela natureza relacional, bancos RDMS não suportam bem esses casos, é difícil escalá-los horizontalmente já que os dados ficam em tabelas diferentes e são ligados por chaves.
Em 2009 o MongoDB surgiu, trazendo um design não relacional focado em prover escala e performance. O MongoDB armazena os dados de forma isolada possibilitando escalar horizontalmente de forma simples e com alta performance.
O MongoDB segue um padrão chamado NoSQL. NoSQL não utilizada linguagem SQL e é livre de dados relacionais. Não existe um padrão fixo de NoSQL mas a maioria dos bancos de dados possui características similares como:
- Não possuem um schema fixo
- Tendem a serem escaláveis e distribuídos por padrão
- Utilizam diferentes estratégias para armazenar os dados de forma otimizada para escala e performance diferente da conhecida tabela, colunas e linhas dos bancos de dados RDMS.
Alguns paradigmas NoSQL são:
- Bancos em gráfico (Graph Databases)
- Armazenamento chave/valor (Key/Value store)
- Armazenamento por documentos (Document store)
O MongoDB segue o paradigma de armazenamento de documentos e conta com três itens chave: Document, Field e Collection
- Document: Similar a linha (row) de um banco de dados relacional
- Field: Similar a coluna (column) de um banco de dados relacional
- Collection: Similar a tabela (table) de um banco de dados relacional
Configurando o banco de dados com Mongoose
Para integrar nossa aplicação com o MongoDB vamos utilizar o Mongoose que é um ODM (Object Document Mapper). O Mongoose irá abstrair o acesso ao banco de dados e ainda irá se responsabilizar por transformar os dados do banco em Models, facilitando a estruturação de nossa aplicação com o padrão MVC.
Para instalar o Mongoose basta executar o seguinte comando npm:
1 $ npm install mongoose@^5.7.13
Após a instalação o Mongoose estará disponível para ser utilizado. O próximo passo será configurar a aplicação para conectar com o banco de dados. Para isso vamos criar um diretório chamado config dentro de src e dentro dele um arquivo chamado database.js que será responsável por toda configuração do banco de dados.
A estrutura de diretórios deve estar assim:
1 ├── src
2 │ ├── app.js
3 │ ├── server.js
4 │ ├── config
5 │ │ └── database.js
6 │ ├── controllers
7 │ │ └── products.js
8 │ └── routes
9 │ ├── index.js
10 │ └── products.js
A primeira coisa que deve ser feita no database.js é importar o módulo do Mongoose, como no código abaixo:
1 import mongoose from 'mongoose';
Seguindo a configuração do banco de dados é necessário informar a url onde está o MongoDB. No meu caso está no meu computador então a url será “localhost” seguido do nome que daremos ao banco de dados:
1 const mongodbUrl = process.env.MONGODB_URL || 'mongodb://localhost/test';
Note que primeiro verificamos se não existe uma variável de ambiente, caso não exista é usado o valor padrão que irá se referir ao localhost e ao banco de dados test. Dessa maneira, poderemos utilizar o MongoDB tanto para testes quanto para rodar o banco da aplicação, sem precisar alterar o código.
No próximo passo vamos criar uma função para conectar com o banco de dados:
1 const connect = () => mongoose.connect(mongodbUrl);
Acima, criamos uma função que retorna uma conexão com o MongoDB, esse retorno é uma Promise, ou seja, somente quando a conexão for estabelecida a Promise será resolvida. Isso é importante pois precisamos garantir que nossa aplicação só vai estar disponível depois que o banco de dados estiver conectado e acessível.
Logo abaixo vamos adicionar um método para fechar a conexão com o banco de dados, essa é uma boa prática que facilita muito nos casos de teste, depois de executar os testes fechamos o banco de dados e garantimos que nada esta executando em nossa aplicação.
1 const close = () => mongoose.connection.close();
O último passo é exportar o módulo de configuração do banco de dados:
1 export default {
2 connect,
3 connection: mongoose.connection
4 }
O código do database.js deve estar similar ao que segue:
1 import mongoose from "mongoose";
2
3 const mongodbUrl = process.env.MONGODB_URL || "mongodb://localhost/test";
4
5 const connect = () =>
6 mongoose.connect(mongodbUrl, {
7 useNewUrlParser: true,
8 useUnifiedTopology: true
9 });
10
11 export default {
12 connect,
13 close
14 };
Pronto, o banco de dados está configurado. Nosso próximo passo será integrar o banco de dados com a aplicação, para que ela inicialize o banco sempre que for iniciada.
Integrando o Mongoose com a aplicação
O módulo responsável por inicializar a aplicação é o app.js, então, ele que vai garantir que o banco estará disponível para que a aplicação possa consumi-lo. Vamos alterar o app.js para que ele integre com o banco de dados, atualmente ele está assim:
1 import express from 'express';
2 import bodyParser from 'body-parser';
3 import routes from './routes';
4
5 const app = express();
6 app.use(bodyParser.json());
7 app.use('/', routes);
8
9 export default app;
O primeiro passo é importar o módulo responsável pelo banco de dados, o database.js, que fica dentro do diretório config. Os imports devem ficar assim:
1 import express from 'express';
2 import bodyParser from 'body-parser';
3 import routes from './routes';
4 + import database from './config/database';
A seguir vamos alterar um pouco o código anterior que utiliza o express e as rotas movendo o seguinte trecho:
1 - app.use(bodyParser.json());
2 - app.use('/', routes);
3
4 - export default app;
Os trechos em vermelho serão movidos para dentro de uma nova função, como no código abaixo:
1 + const configureExpress = () => {
2 + app.use(bodyParser.json());
3 + app.use('/', routes);
4 + app.database = database;
5 +
6 + return app;
7 +};
Acima criamos uma função nomeada configureExpress que terá a tarefa de configurar o express e retornar uma nova instância de aplicação configurada. Também adicionamos a instância do banco de dados, database, para ser parte da aplicação express dessa maneira e possivel abrir e fechar o banco de dados utilizando a app do express.
A última etapa da nossa alteração é inicializar o banco antes da aplicação. Como o moongose retorna uma Promise, vamos esperar ela ser resolvida para então retornar a aplicação configurada para ser utilizada.
1 + export default async() => {
2 + const app = configureExpress();
3 + await app.database.connect();
4 +
5 + return app;
6 +};
No bloco acima exportamos uma função que retorna uma Promise esse passo é necesario pois a função connect do database, que criamos na etapa anterior, assim que essa Promise for resolvida, significa que o banco de dados estará disponível, então retornamos a instância da aplicação.
O app.js depois de alterado deve estar assim:
1 import express from "express";
2 import bodyParser from "body-parser";
3 import routes from "./routes";
4 import database from "./config/database";
5
6 const app = express();
7
8 const configureExpress = () => {
9 app.use(bodyParser.json());
10 app.use("/", routes);
11 app.database = database;
12
13 return app;
14 };
15
16 export default async () => {
17 const app = configureExpress();
18 await app.database.connect();
19
20 return app;
21 };
Como alteramos o app para retornar uma função, que por sua vez retorna uma Promise, será necessário alterar o server.js para fazer a inicialização de maneira correta.
Alterando a inicilização
O server.js é o arquivo responsável por inicializar a aplicação, chamando o app. Como alteramos algumas coisas na etapa anterior precisamos atualizá-lo. Vamos começar alterando o nome do módulo na importação:
1 - import app from './app';
2 + import setupApp from './app';
O módulo foi alterado de app para setupApp, por quê? Porque agora ele é uma função e esse nome reflete mais a sua responsabilidade.
O próximo passo é alterar a maneira como o app é chamado:
1 -app.listen(port, () => {
2 - console.log(`app running on port ${port}`);
3 -});
4 +(async () => {
5 + try {
6 + const app = await setupApp();
7 + const server = app.listen(port, () =>
8 + console.info(`app running on port ${port}`)
9 + );
10 +
11 + const exitSignals = ["SIGINT", "SIGTERM", "SIGQUIT"];
12 + exitSignals.map(sig =>
13 + process.on(sig, () =>
14 + server.close(err => {
15 + if (err) {
16 + console.error(err);
17 + process.exit(1);
18 + }
19 + app.database.connection.close(function() {
20 + console.info("Database connection closed!");
21 + process.exit(0);
22 + });
23 + })
24 + )
25 + );
26 + } catch (error) {
27 + console.error(error);
28 + process.exit(1);
29 + }
30 +})();
Como o código anterior devolvia uma instância da aplicação diretamente, era apenas necessário chamar o método listen do express para inicializar a aplicação. Agora temos uma função que retorna uma promise devemos chamá-la e ela vai inicializar o app, inicializando o banco, configurando o express e retornando uma nova instância da aplicação, só então será possível inicializar a aplicação chamando o listen.
Até esse momento espero que vocês já tenham lido a especificação de Promises mais de 10 vezes e já sejam mestres na implementação. Quando um problema ocorre a Promise é rejeitada. Esse erro pode ser tratado usando um catch como no código acima. Acima, recebemos o erro e o mostramos no console.error, em seguida encerramos o processo do Node.js com o código 1. Dessa maneira o processo é finalizado informando que houve um erro em sua inicialização. Informar o código de saída, é uma boa prática finalizar o processo com código de erro e conhecido como “graceful shutdowns” e faz parte da lista do 12 factor app de boas práticas para desenvolvimento de software moderno.
Graceful shutdown
O graceful shutdown é muito importante principalmente no momento em que a aplicação precisa escalar e rodar na nuvem onde normalmente ela sera executada dentro de um container. Orquestradores de containers como Kubernetes mandam comandos para a aplicação. A aplicação é responsavel por receber esse comando e administrar sua finalização antes de desligar, isso siginifica que a aplicação deve finalizar as conexões abertas, fechar a conexão com o banco de dados e então desligar. No código acima isso é feito utilizando adicionando event listeners a os sinais SIGINT, SIGTERM, SIGQUIT utilizando o process.on(sig). Dessa maneira se a aplicação receber um dos sinais listados ela vai primeiro fechar o servidor express chamando o método close, server.close(), nesse momento o express vai fechar as conexões abertas e logo após fechar a conxão com o banco de dados, connection.close() e finalizar a aplicação com sucesso. Caso um erro ocorra para fechar a conexão do express o processo vai finalizar com 1, ou seja, falha. Desta maneira quem está finalizando o processo vai poder checar a saida (0) para sucesso ou (1) para falha para saber se a aplicação foi desligada com sucesso sem impactar nenhum cliente ou o banco de dados.
As alterações necessárias para integrar com o banco de dados estão finalizadas, vamos executar os testes de integração para garantir:
1 $ npm run test:integration
A saida será:
1 Routes: Products
2 GET /products
3 1) should return a list of products
4
5
6 0 passing (152ms)
7 1 failing
8
9 1) Routes: Products GET /products should return a list of products:
10 TypeError: Cannot read property 'get' of undefined
11 at Context.done (test/integration/routes/products_spec.js:21:7)
O teste quebrou! Calma, isso era esperado. Assim como o server.js o teste de integração inicia a aplicação usando o módulo app, então ele também deve ser alterado para lidar com a Promise.
Vamos começar alterando o helpers.js dos testes de integração, como no código abaixo:
1 -import app from '../../src/app.js';
2 +import setupApp from '../../src/app.js';
3
4 -global.app = app;
5 -global.request = supertest(app);
6 +global.setupApp = setupApp;
7 +global.supertest = supertest;
Assim como no server.js, alteramos o nome do módulo de app para setupApp e o exportamos globalmente. Também removemos o request do conceito global que era uma instância do supertest com o app configurado, deixaremos para fazer isso no próximo passo.
Agora é necessário alterar o products_spec.js para inicializar a aplicação antes de começar a executar os casos de teste usando o callback before do Mocha:
1 describe('Routes: Products', () => {
2 + let request;
3 + let app;
4 +
5 + before(async () => {
6 + app = await setupApp();
7 + request = supertest(app);
8 + });
No bloco acima, criamos um let para o request do supertest e no before a aplicação é inicializada. Assim que o setupApp retornar uma instância da aplicação é possível inicializar o supertest e atribuir a let request que definimos anteriormente. E também definimos o let app para armazenar a instancia da app do express, isso sera necessario apos executar todos os testes para fechar o banco de dados.
Executando os testes novamente, a saída deve ser a seguinte:
1 Routes: Products
2 GET /products
3 ✓ should return a list of products
4
5
6 1 passing (336ms)
Caso ocorra um erro como: “MongoError: failed to connect to server [localhost:27017] on first connect”:
1 Routes: Products
2 1) "before all" hook
3
4
5 0 passing (168ms)
6 1 failing
7
8 1) Routes: Products "before all" hook:
9 MongoError: failed to connect to server [localhost:27017] on first connect
10 at Pool.<anonymous> (node_modules/mongodb-core/lib/topologies/server.js:326:35)
11 at Connection.<anonymous> (node_modules/mongodb-core/lib/connection/pool.js:27\
12 0:12)
13 at Socket.<anonymous> (node_modules/mongodb-core/lib/connection/connection.js:\
14 175:49)
15 at emitErrorNT (net.js:1272:8)
16 at _combinedTickCallback (internal/process/next_tick.js:74:11)
17 at process._tickCallback (internal/process/next_tick.js:98:9)
A mensagem de erro explica que o MongoDB não está executando em localhost na porta 7000, verifique e tente novamente.
O ultimo passo é adicionar o metódo after do Mocha que é chamado após todos os testes:
1 after(async () => await app.database.connection.close());
Aqui garantimos que estamos fechando a conexão com o banco de dados após todos os testes, dessa maneira o Mocha vai executar os testes e logo após a execução vai fechar o banco e fechar o processo no terminal.
O código desta etapa esta disponível aqui.