sexta-feira, 25 de julho de 2014

Curso de MEAN - Sessão 2: Banco de dados No SQL com MongoDB

Sessão 2: Banco de dados No SQL com MongoDB

Vamos aprender a usar um banco de dados No SQL, o "MongoDB", um dos mais rápidos e populares do Mercado. Totalmente Open Source e fácil de usar.


O trabalho Workshop de Desenvolvimento com Stack MEAN de Cleuton Sampaio de Melo Jr está licenciado com uma Licença Creative Commons - Atribuição-CompartilhaIgual 4.0 Internacional.


Isso inclui: Textos, páginas, gráficos e código-fonte.

Por que MongoDB? Isso nós já discutimos... Mas por que estudar o MongoDB agora, na segunda sessão? Simples: Sem Banco de Dados, nada de produtivo pode ser feito!



MongoDB é um banco que armazena documentos. Se você preferir, pode dizer que armazena Objetos. Esses Objetos estão no formado BSON ou “Binary JSON” (http://bsonspec.org/), um formato binário, derivado do JSON (http://json.org/). É muito simples converter de um formato para outro, e o JSON é o formato nativo de objetos do Javascript. Logo, faz todo sentido usar um banco desse tipo com o “Stack” MEAN.



Você conhece JSON, certo? Se não conhece, dê uma olhada nisso:



http://www.obomprogramador.com/2014/03/implementando-uma-api-rest.html



Antes de mais nada, é preciso deixar claro que não vamos ver TUDO sobre o MongoDB, afinal de contas, esse é um “Workshop” de desenvolvimento com Stack MEAN. Se você quiser estudar MongoDB mais detalhadamente, sugiro um livro.

Primeiros passos




Então, o MongoDB armazena coleções de documentos JSON, em formato BSON. Não há SQL, e os relacionamentos são feitos por referência. Existem índices, como em qualquer banco. Abra uma janela “terminal” e inicie o cliente MongoDB digitando “mongo”. Depois, digite o comando “use banco”, que serve para criar um banco de dados chamado “banco”.


Agora, vamos criar uma coleção de documentos de artistas, com esse layout:
{ nome : “Lenny Kravitz” }



Para criarmos uma coleção, basta inserirmos o primeiro documento nela. Usamos o objeto “db”, que representa o nosso banco de dados, informamos o nome da nossa coleção e usamos o método “insert()”. Digite isso no “terminal”, no aplicativo “mongo”:



db.artista.insert( { nome : “Lenny Kravitz”})
e depois:
db.artista.find()



Esse é o resultado que você deve ver:


O método “db.<collection>.find()” executa uma “query” no banco, retornando a coleção de documentos encontrados. Note que, além da propriedade “nome”, foi inserida uma propriedade “_id”, que é conhecido como “ObjectID”, no jargão do MongoDB. A propriedade “_id” é a chave primária do documento, e é indexada automaticamente.



Podemos criar consultas com seleção de documentos usando o método “find()”, bastando passar um Objeto JSON que especifique quais são as condições. Vamos ver alguns exemplos (execute-os no MongoDB):
  • db.artista.find()
    • Retorna todos os documentos da coleção;
  • db.artista.find({ "_id" : ObjectId("53c650ea0db0a2029c4ad773")})
    • Retorna o objeto da coleção cujo “_id” seja igual ao especificado. Estamos informando uma propriedade, e podemos usar qualquer uma delas. A função “ObjectId()” transforma um String em um campo do tipo “ObjectId”, para compararmos;
  • db.artista.find({ "nome" : "Lenny Kravitz" })
    • Semelhante ao exemplo anterior, pesquisamos os documentos da Coleção, cuja propriedade “nome” seja EXATAMENTE IGUAL ao String informado;
  • db.artista.find({ "nome" : { $not : { $eq : "Lenny Kravitz" }}})
    • Agora, usamos um operador lógico e um operador de comparação, para retornarmos os documentos cujo “nome” seja diferente de “Lenny Kravitz”;

Se quiséssemos pesquisar pelo valor exato de uma propriedade, poderíamos usar:
db.artista.find({ "nome" : { $eq : "Lenny Kravitz" }})


Mas isso é a mesma coisa que especificar apenas o valor.



E se quiséssemos pesquisar por algum artista cujo nome contenha “kra”, mas não sabemos o resto e nem se está em maiúsculas ou minúsculas? A solução é fazer uma consulta com Regular Expressions (http://www.regular-expressions.info/):



db.artista.find({ "nome" : { $regex : "kra", $options : "i"}})



A propriedade “$options” informa quais opções queremos para executar a comparação. Neste caso, “i” significa “Case Insensitive”, ou seja, desconsiderar maiúsculas e minúsculas.



Assim como o método “find()” retorna todos os documentos que atendem à seleção, o método “findOne()” retorna apenas um registro.



O MongoDB também trabalha com índices adicionais, para agilizar as consultas. Podemos criar índices com chaves simples ou compostas. Vamos criar um índice no campo: “nome”:



db.artista.ensureIndex( { "nome" : 1 } )



Este método cria um índice baseado na propriedade “nome”, em ordem ascendente (“1”). quiséssemos em ordem descendente, bastaria especificar: “{'nome' : -1}”.



Para deletar um índice, basta usar o método “dropIndex()” repetindo o mesmo Objeto JSON. Agora, delete o índice e crie outro, com essa especificação:



db.artista.ensureIndex( { "nome" : 1 }, { unique: true } )



E tente inserir um novo documento, com os dados: { “nome” : “Lenny Kravitz”}


A opção “unique” indica que só pode haver um único documento com aquele valor na propriedade “nome”. E podemos criar índices compostos também. Por exemplo, vamos modificar o documento para incluir mais uma propriedade:



db.artista.update({ "_id" : ObjectId("....")}, {$set :{"pais" : "Estados Unidos"})



O método “update()”, usado com o modificador “$set”, adiciona uma propriedade (caso não exista), ou modifica o valor de uma já existente. Neste caso, estamos ascrescentando uma propriedade “pais”, para incluir o país de origem do artista. Poderíamos também modificar o valor de uma propriedade já existente.



Para criar um índice com as propriedades “nome” e “pais”, ambos em ordem ascendente, basta usar o método:
db.artista.ensureIndex( { “nome” : 1, “pais” : 1} )



Finalmente, podemos apagar documentos ou coleções inteiras:
  • db.artista.remove({}) : remove TODOS os documentos da coleção;
  • db.artista.remove({ "_id" : ObjectId("53c650ea0db0a2029c4ad773")}) : remove TODOS os documentos que atendam à condição especificada;
  • db.artista.drop() : Temove a coleção e todos os documentos.



Relacionamentos

Para criar duas coleções relacionadas no MongoDB, podemos criar dois tipos de relacionamentos: Embutidos e por Referência.



Os relacionamentos embutidos funcionam com um documento “embutido” dentro de outro. Por exemplo, vamos modificar nossa coleção “artista” para incluir suas músicas:



db.artista.update({ "_id" : ObjectId("xxx")},
{$set : { "musicas" : [
{titulo : "Are you gonna go my way"},
{titulo : "It Ain't over 'Till it's over"},
{titulo : "American woman"}]
}})




Se fizermos um “find()” depois disso:



E podemos criar relacionamentos por Referência, por exemplo, vamos criar uma coleção de “estilos”:

db.estilo.insert({"nome" : "Rock"})



Depois, podemos incluir uma propriedade na coleção “artista”, que referencia o “estilo”:



db.artista.update({ "_id" : ObjectId("xxxx")}, {$set :{"estilo_id" : ObjectId("yyyyyy")}})



Usando o MongoDB no Node.js




Nós já usamos o MongoDB no Node.js, ainda na primeira aula. Veja o exemplo (“acessoBanco.js”):



function mensagem(request,response) {
   MongoClient.connect("mongodb://localhost:27017/meudb", function(err, db) {
   if(!err) {
      var usuario = db.collection('usuario');
      usuario.findOne({cpf : 12345}, 
         function(err, item) {
            if(!err) {
               response.end('OK: ' + JSON.stringify(item));
            }
         });
   }
   else {
      response.end('Erro ao conectar ao banco');
   }
   });
}

var http = require('http');
var MongoClient = require('mongodb').MongoClient;
server = http.createServer(mensagem);
server.listen(8080, function() {
     console.log('Aguardando conexoes na porta 8080');
});

Crie uma pasta, copie o script para ele e rode o comando “npm install mongodb” e rode esse Script para lembrar como funciona (se você acompanhou a aula anterior, então tem tudo o que precisa, caso contrário, volte e refaça).



Todos os métodos que usamos diretamente no “Mongo Client”, estão disponíveis, então não precisamos nos deter muito nisso.



Exercício




Faça uma aplicação que pesquise um artista no Banco, usando um request “GET”, com um parâmetro “artista”. O documento retornado deve pode ser em JSON mesmo.



Correção em “sessao2/respostas/exercicio1”.



Eis a saída esperada (usando o wget):


Como você vai fazer isso? Você vai usar um “find()” e um “each()”. O “find()” retorna um Cursor MongoDB, e o “each()” nos permite navegar em cada documento do Cursor. Um exemplo:


db.usuario.find().each(function(erro, doc) {
 if (!erro) {
     if(doc) {
  // quando acabam os documentos, retorna NULL
     }
     }
});

Use o método “JSON.stringify()” para obter um String a partir do documento obtido.



Agora, um desafio para você: Formate o HTML bonitinho com o “lith”. A priopriedade “musicas” é um Vetor!



Mapeamento de Esquema com o Mongoose




Usar a interface do MongoDB no Node.js é relativamente simples, mas as coisas tendem a ficar muito complexas, quando temos um Banco grande, com muitas coleções e relacionamentos. Além disso, não existe documentação de esquema do Banco.



O Mongoose resolve isso. Ele se parece um pouco com os frameworks de mapeamento O/R do Java, como o JPA e o Hibernate, porém, é imensamente mais simples.



Para usar o Mongoose, temos que adicionar a dependência em nosso “package.json”, ou então rodar o comando “npm install mongoose”.



Vamos ver um exemplo bem simples, modelando a coleção “estilo” (sessao2/modelo.js):


http = require('http')
mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/banco', function(erro) {
    if(erro) {
 console.log('erro ao conectar com o banco: ' + erro);
    }
    else {
 console.log('Conexão com o banco OK');
    }
});
schemaEstilo = new mongoose.Schema(
 { 
  nome : String
 }
);
Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

http.createServer(function(req,res) {
 Estilo.find({ nome: 'Rock' }, function(erro,Rock) {
  if (!erro) {
   if(Rock) {
      res.end(JSON.stringify(Rock));
   }
   else {
      res.end('Nao encontrado');
   }
  }
  else {
      res.end(erro);
  }
 });           
      
   }).listen(8080, function() {
       console.log('Aguardando conexoes na porta 8080');
   });

O Mongoose nos isola de um monte de trabalho. Para começar, abrimos a conexão default do Mongoose:
mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/banco', function(erro) {
if(erro) {
console.log('erro ao conectar com o banco: ' + erro);
}
else {
console.log('Conexão com o banco OK');
}

});

Com a conexão default aberta, podemos modelar o “esquema” do nosso banco de dados, criando um ou mais “mongoose.schema”. Por exemplo, vamos criar um esquema para a coleção “estilo”:



schemaEstilo = new mongoose.Schema(
{
nome : String
}
);
Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');



Primeiro, criamos um esquema passando um objeto JSON para ele. Neste Objeto, definimos todas as propriedades da Coleção, e podemos colocar atributos também, por exemplo:
schemaEstilo = new mongoose.Schema(
{
nome : { type : String, required : true }
}
);



Uma vez que definimos o esquema, podemos criar um modelo e associá-lo a uma coleção de verdade no Banco de dados:



Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

O terceiro parâmetro é o nome da coleção. Se você estiver criando um banco novo, ele pode ser omitido. O Mongoose vai criar a coleção com o nome do modelo, no plural. Neste caso, seria: “Estilos”.



Pronto! Quer consultar um estilo? Simples:



Estilo.find({ nome: 'Rock' }, function(erro,Rock) {



O “find()” funciona igual ao do MongoDB Driver, e também tem um “callback”. O parâmetro “Rock” é o resultado da query, se nada existir, então virá vazio. O parâmetro “erro” funciona da mesma forma.



O que o “find()” retorna? Um vetor JSON contendo os registros encontrados, por exemplo, se inserirmos mais um estilo (“Samba”), e retirarmos o critério da seleção, isso é o que será retornado:



Estilo.find({ }, function(erro,Rock)



[{"_id":"53c667ff90edc0014dba9f9d","nome":"Rock"},{"_id":"53c7bc7c5ac13bff32917eda","nome":"Samba"}]



Relacionamentos




Bem, já temos o modelo do Estilo, agora, vamos modelar o artista também. Para começar, vamos rever um registro de artista:


> db.artista.find()
{ "_id" : ObjectId("53c6ba00860c9301ef45fdf3"),
"nome" : "Lenny Kravitz",
"pais" : "Estados Unidos",
"musicas" : [
{ "titulo" : "Are you gonna go my way" },
{ "titulo" : "It Ain't over 'Till it's over" },
{ "titulo" : "American woman" }
],
"estilo_id" : ObjectId("53c667ff90edc0014dba9f9d")
}



Temos um documento “musicas” embutido, e um relacionamento com a coleção “estilo”. Podemos modelar de maneira simples (script: “modeloartista.js”):



schemaEstilo = new mongoose.Schema(
 { 
  nome : String
 }
);


Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

schemaArtista = new mongoose.Schema(
 {
     "nome" : String, 
     "pais" : String, 
     "musicas" : [ { "titulo" : String }], 
     "estilo_id" : {type: mongoose.Schema.Types.ObjectId, ref: 'Estilo'}
 }
);
Artista = mongoose.model('Artista',schemaArtista, 'artista');

A modelagem é simples. O vetor de músicas é definido com um vetor de objetos que possuem a propriedade String “titulo”. A grande diferença é como modelamos o “estilo_id”, pois o definimos como um “ObjectId”, que referencia o modelo “Estilo”, criado anteriormente. Por enquanto, só estamos informando que existe uma referência de Artista para Estilo.



Bem, mudamos a query:



Artista.find({}, function(erro,Rock)...);



E ele retornou isso:



wget -qO - localhost:8080
[{"_id":"53c6ba00860c9301ef45fdf3","nome":"Lenny Kravitz","pais":"Estados Unidos","estilo_id":"53c667ff90edc0014dba9f9d","musicas":[{"titulo":"Are you gonna go my way"},{"titulo":"It Ain't over 'Till it's over"},{"titulo":"American woman"}]



Ok, ele retorno tudo certinho, incluindo as músicas. Mas, cadê o Estilo? Nós informamos que havia uma referência de artista para Estilo, logo, o esperado é que venha o nome do Estilo... “KEEP CALM AND” vamos estudar como popular um Modelo composto por referência. Para começar, abra o script: “modelo-artista-estilo.js”:


http = require('http')
mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/banco', function(erro) {
    if(erro) {
 console.log('erro ao conectar com o banco: ' + erro);
    }
    else {
 console.log('Conexão com o banco OK');
    }
});
schemaEstilo = new mongoose.Schema(
 { 
  nome : String
 }
);


Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

schemaArtista = new mongoose.Schema(
 {
     "nome" : String, 
     "pais" : String, 
     "musicas" : [ { "titulo" : String }], 
     "estilo_id" : {type: mongoose.Schema.Types.ObjectId, ref: 'Estilo'}
 }
);
Artista = mongoose.model('Artista',schemaArtista, 'artista');

http.createServer(function(req,res) {
 Artista
     .find({})
     .populate('estilo_id')
     .exec(function(erro,Artistas) {
  if (!erro) {
   if(Artistas) {
      res.end(JSON.stringify(Artistas));
   }
   else {
      res.end('Nao encontrado');
   }
  }
  else {
      res.end(erro);
  }
 });           
      
   }).listen(8080, function() {
       console.log('Aguardando conexoes na porta 8080');
   });

O Objeto retornado pelos métodos “find()” e “findOne()” é do tipo “query”, e ele possui alguns métodos interessantes, como “populate()” e “exec()”. O método “populate()” faz exatamente o que seu nome diz: Popula (ou “povoa”) referências a outros Documentos. Basta informar o nome da propriedade. E o método “exec()” executa a consulta, permitindo que criemos um “callback” para ler o resultado. E, por falar em Resultado:



wget -qO - localhost:8080
[{"_id":"53c6ba00860c9301ef45fdf3","nome":"Lenny Kravitz","pais":"Estados Unidos","estilo_id":{"_id":"53c667ff90edc0014dba9f9d","nome":"Rock"},"musicas":[{"titulo":"Are you gonna go my way"},{"titulo":"It Ain't over 'Till it's over"},{"titulo":"American woman"}]}]



Note que, a propriedade “estilo_id” agora é um Documento completo, com a propriedade “nome”. Isto foi criado pelo método “populate('estilo_id')”.



Alteraração de modelos



Podemos inserir novos Documentos usando os modelos. Por exemplo, vamos inserir um novo Estilo, usando o método “save()”:


mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/banco', function(erro) {
    if(erro) {
 console.log('erro ao conectar com o banco: ' + erro);
    }
    else {
 console.log('Conexão com o banco OK');
    }
});
schemaEstilo = new mongoose.Schema(
 { 
  nome : String
 }
);
Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');
pagode = new Estilo({nome : 'Pagode'});
pagode.save(function (erro,estiloSalvo) {
 if(erro) {
     console.log('Erro: ' + erro);
 }
 else {
     console.log('ID: ' + estiloSalvo._id);
 }
});

Após salvar o novo Estilo, podemos pegar (no “callback”) o seu “_id”, para usar como referência em outros documentos.



Para alterar um Documento podemos usar dois métodos: “update()” e “findByIdAndUpdate()”. Vamos ver dois exemplos simples (“update-estilo.js”):



Estilo.update({_id : estiloSalvo._id},
{$set : {nome : 'Sertanejo Universitário'}},
function(erro) {
});



É claro que você precisa ter lido um Estilo ANTES de atualizá-lo. Agora, se você quiser fazer as duas coisas, existe o método “findByIdAndUpdate()” (“find-update-estilo.js”):



Estilo.findByIdAndUpdate({ "_id" : mongoose.Types.ObjectId("53c7d62f69019e2a12289818")},
{$set : { nome : 'Partido Alto'}}, function(erro,estilo) {
if (erro) {
console.log('erro: ' + erro);
}
else {
console.log(estilo);
}
});



Para remover documentos, usamos o método “remove()”. Eis um exemplo: ('remove-estilo.js')



Estilo.remove({ "_id" : mongoose.Types.ObjectId("53c7d62f69019e2a12289818")},
function(erro) {
if (erro) {
console.log('erro: ' + erro);
}
else {
console.log('Removido');
}
});



Também podemos usar o “findByIdAndRemove()”:



Modelo.findByIdAndRemove(id, [options], function(erro,documento) {})



A vantagem é que ele retorna o documento removido no “callback”, logo, podemos mostrar o que estamos removendo, ou salvar em outro lugar.



Atualização de documentos compostos




Como diriam os Americanos: “piece of cake”, ou seja: “molezinha”... Vejam esse código ('cria-update-artista.js'):


mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/banco', function(erro) {
    if(erro) {
 console.log('erro ao conectar com o banco: ' + erro);
    }
    else {
 console.log('Conexão com o banco OK');
    }
});
schemaEstilo = new mongoose.Schema(
 { 
  nome : String
 }
);


Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

schemaArtista = new mongoose.Schema(
 {
     "nome" : String, 
     "pais" : String, 
     "musicas" : [ { "titulo" : String }], 
     "estilo_id" : {type: mongoose.Schema.Types.ObjectId, ref: 'Estilo'}
 }
);
Artista = mongoose.model('Artista',schemaArtista, 'artista');
fulano = new Artista({
 nome : 'Fulano de Tal',
 musicas : [
  { titulo : 'Ô vida de cão'},
  { titulo : 'Todo castigo é pouco'}]
 });
fulano.save(function(erro,artista) {
 Estilo.findOne({ nome : {$regex : 'sertanejo', $options : 'i'}},
  function(erro,sertanejo) {
     if(!erro) {
   Artista.update( {'_id' : artista._id},
    {$set : {estilo_id : sertanejo}}, 
    function(erro) {
       if(erro) {
     console.log('Erro ' + erro);
       }
       else {
     console.log('Ok');
       }
    });
     }
     else {
   console.log('Erro no find do Estilo');
     }
  });

});

É só salvar o objeto Estilo dentro da propriedade “estilo_id”. Veja o resultado de uma consulta logo após a execução desse script:



> db.artista.find()
{ "_id" : ObjectId("53c6ba00860c9301ef45fdf3"), "nome" : "Lenny Kravitz", "pais" : "Estados Unidos", "musicas" : [ { "titulo" : "Are you gonna go my way" }, { "titulo" : "It Ain't over 'Till it's over" }, { "titulo" : "American woman" } ], "estilo_id" : ObjectId("53c667ff90edc0014dba9f9d") }
{ "_id" : ObjectId("53c802c52b52f4ec1234a8f6"), "nome" : "Fulano de Tal", "musicas" : [ { "_id" : ObjectId("53c802c52b52f4ec1234a8f8") }, { "_id" : ObjectId("53c802c52b52f4ec1234a8f7") } ], "__v" : 0, "estilo_id" : ObjectId("53c7fa997da9327912281454") }



O artista foi criado e a propriedade “estilo_id” foi preenchida com o “_id” do estilo que indicamos.



KEEP CALM AND CRIE UMA APLICAÇÃO




Refaça o exercício dessa aula usando o Mongoose!



Não arranque os cabelos! A resposta está em : 'sessao2/respostas/s2exercicio2.js'.