JOIN nos bancos de dados NoSQL

Este post discutirá como conectar coleções nos bancos de dados NoSQL mongodb, arangodb, orientdb e rethinkdb (além de serem bancos de dados NoSQL, eles também são unidos por uma versão gratuita com uma licença bastante leal). Nos bancos de dados relacionais, funcionalidades semelhantes são implementadas usando o SQL JOIN. Apesar de as operações CRUD nos bancos de dados NoSQL serem muito semelhantes e diferirem apenas em detalhes, por exemplo, em um banco de dados, a função create ({...}) é usada para criar um objeto, inserir ({...}) no outro e em o terceiro - salvar ({...}), - a implementação de uma seleção de duas ou mais coleções em cada um dos bancos de dados é implementada de maneiras completamente diferentes. Portanto, será interessante executar a mesma seleção em todos os bancos de dados. Para todos os bancos de dados, a amostragem (um relacionamento muitos-para-muitos) para duas tabelas será considerada.

Por exemplo, tabelas serão usadas.

O autor

  • primeiro nome

O livro

  • Título

BooksAuthor

  • O autor
  • O livro

Para o mongodb, a seleção das tabelas será implementada assim:

const mongo = require('mongodb-bluebird'); mongo.connect("mongodb://localhost:27017/test").then(async function(db) { const author = db.collection('author'); const book = db.collection('book'); const bookauthor = db.collection('bookauthor'); ['Joe', 'John', 'Jack', 'Jeremy'].map(async (name) => await author.insert({name}) ); ['Art', 'Paint'].map(async (title) => await book.insert({title}) ); let Author = await author.findOne({ name: 'Joe' }); let Book = await book.findOne({ title: 'Paint' }); await bookauthor.insert({author: Author._id, book: Book._id}) Author = await author.findOne({ name: 'John' }); await bookauthor.insert({author: Author._id, book: Book._id}) Book = await book.findOne({ title: 'Art' }); await bookauthor.insert({author: Author._id, book: Book._id}) const result = await author.aggregate([{ $lookup:{ from: 'bookauthor', localField: '_id', foreignField: 'author', as: 'ba' }}, { $lookup: { from: 'book', localField: 'ba.book', foreignField: '_id', as: 'books' }}],{ }) }); 

Diferentemente da seleção SQL JOIN, a seleção resultante não será uma tabela plana na qual o Autor será repetido quantos livros ele tiver escrito, mas um objeto hierárquico no qual cada Autor será representado por um objeto que terá a propriedade books contendo uma matriz de objetos Book . Do meu ponto de vista, essa é uma vantagem muito grande nos bancos de dados NoSQL. Mas você também pode precisar de uma versão "plana", semelhante ao SQL JOIN. Para recebê-lo na solicitação, você precisa adicionar a "expansão" de matrizes: { $unwind: '$books' } .

A amostra apresentada no exemplo é um análogo do SQL LEFT JOIN, ou seja, todos os autores serão incluídos na amostra, mesmo que não possuam livros. Para fazer um análogo do SQL [INNER] JOIN, você deve adicionar a condição { $match: { books: { $ne: [ ] } } } ou se $ unindw for usado:

 { $unwind: { path: "$role", preserveNullAndEmptyArrays: false } } 

Então, vamos para o arangodb, que é um banco de dados híbrido. Além de trabalhar com documentos, implementa o trabalho com gráficos. Primeiro, vamos ver como no arangodb você pode fazer uma seleção usando apenas documentos (não gráficos):

  FOR a IN author FOR ba IN bookauthor FILTER a._id == ba.author FOR b IN book FILTER b._id == ba.book SORT a.name, b.title RETURN { author: a, book: b } 

O Arangodb usa a palavra-chave FOR para ingressar em coleções e FILTER para especificar a condição de ingresso. A amostra obtida neste caso será semelhante ao SQL [INNER] JOIN (ou seja, será um objeto "plano" e não conterá valores vazios)

Mas é muito mais conveniente no arangodb usar recursos de gráfico para seleções de vários objetos:

 const { Database, aql } = require('arangojs'); const db = new Database({ url: "http://localhost:8529" }); db.useDatabase("test"); db.useBasicAuth("test", "test"); const author = db.collection('author') const book = db.collection('book') const bookauthor = db.edgeCollection('bookauthor') void async function() { ['Joe', 'John', 'Jack', 'Jeremy'].map(async (name) => await author.save({name}) ); ['Art', 'Paint'].map(async (title) => await book.save({title}) ); let Author = await author.firstExample({ name: 'Joe' }); let Book = await book.firstExample({ title: 'Paint' }); await bookauthor.save({date: 'Some data'}, Author._id, Book._id) Author = await author.firstExample({ name: 'John' }); await bookauthor.save({date: 'Some data'}, Author._id, Book._id) Book = await book.firstExample({ title: 'Art' }); await bookauthor.save({date: 'Some data'}, Author._id, Book._id) const cursor = await db.query(aql` FOR a IN author FOR book_vertex, book_edge IN OUTBOUND a bookauthor COLLECT a1 = a INTO b1 RETURN {author: a1, books: b1[*].book_vertex} `); }(); 

Agora, estamos usando para comunicação não um documento, mas uma coleção de arestas do gráfico (aresta) do autor do livro. A IN OUTBOUND a bookauthor seleciona para um determinado autor a coleção de documentos relacionados, que ele coloca na resposta sob o nome book_vertex . A instrução COLLECT a1 = a INTO b1 é um análogo do SQL GROUP - acumula um valor em uma matriz que estará disponível na resposta sob o nome b1 para cada valor do Autor , que na resposta estará disponível sob o nome a1 . A construção b1[*].book_vertex permite remover níveis desnecessários de aninhamento do objeto, para que o resultado seja conveniente para trabalhos futuros.

A implementação de consultas do tipo SQL LEFT JOIN no arangodb é mais difícil porque a instrução FOR - FILTER define restrições semelhantes ao SQL [INNER] JOIN. Para implementar as "conexões esquerdas", o operador LET e a subconsulta são usados:

  const cursor = await db.query(aql` FOR a IN author LET books = ( FOR book_vertex, book_edge IN OUTBOUND a bookauthor RETURN book_vertex ) RETURN {author: a, books} `); 

Nesse caso, o agrupamento de dados não é necessário, pois uma subconsulta é realizada para cada autor e a resposta já contém uma matriz gótica de objetos Book.

Vá para o banco de dados orientdb. É também um banco de dados híbrido que permite trabalhar com documentos e gráficos. A ideologia de trabalhar com gráficos é semelhante ao exemplo anterior no arangodb. Ou seja, para vincular coleções, é usada uma coleção de arestas do gráfico do autor do livro.

 const OrientDB = require('orientjs'); const server = OrientDB({ host: 'localhost', port: 2424, }); void async function() { const db = server.use({ name:'test', username: 'test', password: 'test' }); await db.open(); try { await db.class.drop('Author UNSAFE'); } catch(ex) { console.log(ex) } try { await db.class.drop('Book UNSAFE'); } catch(ex) { console.log(ex) } try { await db.class.drop('BookAuthor UNSAFE'); } catch(ex) { console.log(ex) } const author = await db.class.create('Author', 'V'); const book = await db.class.create('Book', 'V'); const bookauthor = await db.class.create('BookAuthor', 'E'); ['Joe', 'John', 'Jack', 'Jeremy'].map(async (name) => await author.create({name}) ); ['Art', 'Paint'].map(async (title) => await book.create({title}) ); await author.list(); await book.list(); let Author = await db.select().from('Author').where({name: 'Joe'}).one(); let Book = await db.select().from('book').where({ title: 'Paint' }).one(); await db.create('EDGE', 'BookAuthor').from(Author['@rid']).to(Book['@rid']).set({date: 'Some data'}).one(); Author = await db.select().from('Author').where({name: 'John'}).one(); await db.create('EDGE', 'BookAuthor').from(Author['@rid']).to(Book['@rid']).set({date: 'Some data'}).one(); Book = await db.select().from('book').where({ title: 'Art' }).one(); await db.create('EDGE', 'BookAuthor').from(Author['@rid']).to(Book['@rid']).set({date: 'Some data'}).one(); const cursor = await db.query(`select name, out('BookAuthor').title as books from Author`).all() } () 

Talvez o orientdb tenha a implementação mais bem-sucedida, como é o mais próximo da sintaxe SQL e conciso em termos de trabalho com gráficos. A expressão out('BookAuthor').title as books from Author significa selecionar para a coleção Author todas as arestas de saída da coleção BookAuthor que vinculam a coleção Author à coleção Book. Nesse caso, o objeto resultante será hierárquico (um objeto para cada autor com uma matriz de objetos do livro). Se você precisar "expandir" a matriz em um objeto plano, o operador UNWIND será usado.

E, finalmente, considere o repensar. Não faz muito tempo, a equipe que desenvolveu esse banco de dados deixou de existir e transferiu o desenvolvimento para a comunidade aberta. Eu digo isso imediatamente, porque alguém pode ter pulado esta notícia. Antes de um conhecimento mais detalhado, parecia-me a implementação do JOIN no rehinkdb a mais conveniente. Talvez porque essa oportunidade tenha sido incorporada imediatamente à API do banco de dados e chamada de join (). Mas, no fim, tudo não é tão otimista e nem todas as funções que implementam JOIN funcionam de maneira igualmente eficiente e têm flexibilidade suficiente para criar as consultas corretas. Nosso mesmo exemplo de ponta a ponta agora está implementado no rethinkdb:

 r = require('rethinkdb') void async function() { const conn = await r.connect({ host: 'localhost', port: 28015 }); try { await r.db('test').tableDrop('author').run(conn); await r.db('test').tableDrop('book').run(conn); await r.db('test').tableDrop('bookauthor').run(conn); } catch (ex) { console.log(ex) } await r.db('test').tableCreate('author').run(conn); await r.db('test').tableCreate('book').run(conn); await r.db('test').tableCreate('bookauthor').run(conn); await r.db('test').table('bookauthor').indexCreate('author').run(conn); await r.db('test').table('bookauthor').indexCreate('book').run(conn); await r.db('test').table('bookauthor').indexWait('author', 'book').run(conn); ['Joe', 'John', 'Jack', 'Jeremy'].map(async (name) => await r.db('test').table('author').insert({ name }).run(conn) ); ['Art', 'Paint'].map(async (title) => await r.db('test').table('book').insert({ title }).run(conn) ); let Author = await r.db('test').table('author').filter({ name: 'Joe' }).run(conn).then(authors => authors.next()); let Book = await r.db('test').table('book').filter({ title: 'Paint' }).run(conn).then(books => books.next()); await r.db('test').table('bookauthor').insert({author: Author.id, book: Book.id}).run(conn); Author = await r.db('test').table('author').filter({ name: 'John' }).run(conn).then(authors => authors.next()); await r.db('test').table('bookauthor').insert({author: Author.id, book: Book.id}).run(conn); Book = await r.db('test').table('book').filter({ title: 'Art' }).run(conn).then(books => books.next()); await r.db('test').table('bookauthor').insert({author: Author.id, book: Book.id}).run(conn); const cursor = await r.db('test').table('author') .eqJoin('id', r.db('test').table('bookauthor'), {index: 'author'}).zip() .eqJoin('book', r.db('test').table('book')).zip().run(conn); }(); 

Você deve prestar atenção a esses pontos. Neste exemplo, a comunicação do índice secundário foi implementada usando a função eqJoin (), que pode usar pares ao conectar objetos: uma chave primária com uma chave primária ou uma chave primária com uma chave secundária (mas não uma chave secundária com uma chave secundária). Para condições mais complexas, é usada a função map (), que é uma ordem de magnitude mais difícil de entender. Outras funções que implementam JOIN não são otimizadas (presumivelmente, uma enumeração completa de valores é implementada).

O texto dos exemplos está localizado no repositório .

apapacy@gmail.com
4 de junho de 2018

Source: https://habr.com/ru/post/pt413123/


All Articles