Eficiencia em consultas relacionais com Knex.js

Olá pessoal,

Já faz um tempo que não escrevemos por aqui, mas nossa equipe está empenhada e internamente montamos uma agenda de posts para compartilhar um pouco do que aprendemos e lidamos no nosso dia a dia.

Hoje eu vou falar de um framework que há algum tempo começamos a estudar e rapidamente adotamos como nossa principal estratégia de acesso a banco de dados relacionais.
Trata-se do Knex.js.
Knex.js é um “SQL Query Builder” criado para ser uma opção mais flexivel aos tradicionais ORMs que nos ajudam bastante a manter a integridade e o relacionamento dos dados mas por um outro lado nos limita na criação de queries performáticas e na persistência de entidades complexas. Como o javascript (linguagem usada pelo Node.js) não é propriamente uma linguagem orientada a objetos adotar soluções ORM pode ser uma escolha ruim para determinados tipos de projetos.
Bom vamos lá! #showmethecode

Para iniciarmos, vamos criar um projeto para demonstrar uma consulta a um cadastro de usuários que possui um mais perfis (1-N), chamado “my-first-knex-app“.

	mkdir my-first-knex-app && cd my-first-knex-app
	npm init 	# para criar o package.json e as informações do seu projetola

Instalação

Seguindo o padrão (regra) a instalação é feita através do NPM

npm install knex --save

De acordo com o banco de dados de sua escolha, instale os conectores (confira a lista de banco de dados suportados clicando aqui).
Aqui na Ornito, quando fazemos projetos que necessitam de integridade de dados relacionais, utilizamos o PostgreSQL, vamos utilizá-lo para esse exemplo.

npm install pg --save

Nós vamos precisar instalar também uma lib chamada JoinJs, ela vai nos ajudar a fazer o repetitivo trabalho de montar a entidade com os resultados das queries, vamos ver mais a frente no post como isso funciona na prática.

npm install join-js --save

Setup

Knex.js conta também com uma incrivel interface para ser utilizado via console, esse “cli” contem uma ferramenta de migrations.
Para fazermos uso desse utilitário, vamos criar um arquivo chamado “knexfile.js” (ao executar o cli, o Knex.js procura por esse arquivo para obter as informações necessárias para a execução dos arquivos de migration, vou deixar uma explicação mais detalhada e alguns exemplos de uso para um próximo post =) )

//knexfile.js
module.exports = {

 	 development: {
	    client: 'postgresql',
	    connection: {
	      database: 'SEU_BD_DEV',
	      user: 'SEU_USUARIO_BD',
	      password: 'SENHA_BD'
	    },
	    migrations: {
	      tableName: 'migrations'
	    },
	    seeds: {
	      directory: './seeds/'
	    }
	  }
}

Repare que nesse arquivo podemos determinar os environments (development, homolog e prod), para esse exemplo vamos utilizar apenas em development.
Agora crie um arquivo chamado “db.js” e dentro dele adicione as informações de acesso ao banco de dados:

//db.js

'use strict';

let knex = require('knex')({
	client: 'pg',
	connection: 'postgres://SEU_USUARIO_BD:SENHA_BD@localhost:5432/SEU_BD_DEV'
});

module.exports = {
	knex: knex
};

Adicionando o atributo “debug: true” nas configurações do client, as queries serão logadas no terminal conforme sua execução.
Uma excelente opção nos tristes momentos de debug.

Como o Knex.js foi desenvolvido para funcionar como um query builder e não uma ferramenta para acesso transacional ao banco de dados em si, você pode usá-lo apenas como um construtor de queries, outra funcionalidade que esse framework disponibiliza, para isso apenas importe o módulo e saia escrevendo suas queries:

let knex = require('knex')({});
knex.select('*').from('users').toString();
// Output: select * from users;

Voltando ao projeto “my-first-knex-app”, agora vamos criar um arquivo chamado “users.js” e importar o módulo que criamos acima, depois criar uma função que vai servir para obtermos detalhes de um usuário especifico, reparem que a API do Knex.js é totalmente “Promised“:

//users.js
'use strict';

const knex = require('./db').knex;

let getUserById = (id) => {

	return knex
	.select('usr.login as login',
		'usr.name as name',
		'usr.created_on as created_on'
	)
	.from('users as usr')
	.innerJoin('profiles as pro', 'usr.profile_id', 'pro.id')
	.where('id', id)
	.then(resultSet => {
		return joinjs.mapOne(resultSet, resultMaps, 'userMap');
	});
}

Estamos fazendo um join da tabela “users” com a tabela “profiles” e logo em seguida estamos chamando a function “mapOne” do JoinJs setando qual mapa deve ser usado (‘userMap’) como referencia para que as propriedades sejam mapeadas.
Crie um arquivo chamado ‘resultmaps.js‘ e especifique os campos que devem ser mapeados:

//resultmaps.js
let resultMaps = [{
    mapId: 'userMap',
    idProperty: 'id',
    properties: ['name', 'login', 'created_on'],
    collections: [{
    	name: 'profiles',
        mapId: 'profileMap',
        columnPrefix: 'profile_'
    }]
}, {
    mapId: 'profileMap',
    idProperty: 'id',
    properties: ['name']
}];

module.exports = resultMaps;

Ao executar essa função teremos um objeto montado de acordo com o mapeamento:

{
	id: 1,
	name: 'My First Knex App',
	login: 'my-first-knex@ornito.com',
	created_on: '2016-03-03T19:14:00',
	profiles: [{
		id: 1,
		name: 'Developer'
	}, {
		id: 2,
		name: 'SysAdmin'
	}]
}

Como puderam ver nesse post, consultas entre entidades que possuem relacionamentos variados podem ficar cada vez mais complexas, se você tiver usando uma estratégia ORM tarefas que são facilmente resolvidas com uma simples query podem se tornar grandes pontos de refactoring.

Espero que essa pequena demonstração possa motivá-los a conhecer alternativas e buscar outros mindsets para resolver esses tais problemas que já possuem uma “solução” e apenas para registro nos autos, eu não tenho nada contra frameworks ORM, pelo contrário, utilizo e sempre utilizei frameworks como HibernateNHibernate e Sequelize em alguns projetos da Ornito.

Não deixe de nos acompanhar, um grande abraço!

 

 

Source: Ornito

Recent Posts

Leave a Comment