ÚNETE en bases de datos NoSQL

Esta publicación discutirá cómo conectar colecciones en bases de datos NoSQL mongodb, arangodb, orientdb y rethinkdb (además de ser bases de datos NoSQL, también están unidas por una versión gratuita con una licencia bastante leal). En bases de datos relacionales, se implementa una funcionalidad similar usando SQL JOIN. A pesar de que las operaciones CRUD en las bases de datos NoSQL son muy similares y solo difieren en detalles, por ejemplo, en una base de datos, la función crear ({...}) se usa para crear un objeto, insertar ({...}) en la otra, y en el tercero, guardar ({...}), - la implementación de una selección de dos o más colecciones en cada una de las bases de datos se implementa de maneras completamente diferentes. Por lo tanto, será interesante realizar la misma selección en todas las bases de datos. Para todas las bases de datos, se considerará el muestreo (una relación de muchos a muchos) para dos tablas.

Por ejemplo, se usarán tablas.

El autor

  • primer nombre

El libro

  • Titulo

LibrosAutor

  • El autor
  • El libro

Para mongodb, la selección de las tablas se implementará así:

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' }}],{ }) }); 

A diferencia de la selección SQL JOIN, la selección resultante no será una tabla plana en la que el Autor repetirá tantos libros como haya escrito, sino un objeto jerárquico en el que cada Autor estará representado por un objeto que tendrá la propiedad de libros que contiene una matriz de objetos Libro . Desde mi punto de vista, esta es una gran ventaja en las bases de datos NoSQL. Pero también puede necesitar una versión "plana", similar a SQL JOIN. Para recibirlo en la solicitud, debe agregar la "expansión" de las matrices: { $unwind: '$books' } .

La muestra presentada en el ejemplo es un análogo de SQL LEFT JOIN, es decir, todos los autores serán incluidos en la muestra, incluso si no tienen libros. Para hacer un análogo de SQL [INNER] JOIN, debe agregar la condición { $match: { books: { $ne: [ ] } } } , o si se usa $ unwind:

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

Entonces, pasemos a arangodb, que es una base de datos híbrida. Además de trabajar con documentos, implementa el trabajo con gráficos. Primero, veamos cómo en arangodb puede hacer una selección utilizando solo documentos (no 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 } 

Arangodb usa la palabra clave FOR para unir colecciones y FILTER para especificar la condición de unión. La muestra obtenida en este caso será similar a SQL [INNER] JOIN (es decir, será un objeto "plano" y no contendrá valores vacíos)

Pero es mucho más conveniente en arangodb usar funciones de gráficos para las selecciones de varios 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} `); }(); 

Ahora estamos utilizando para la comunicación no un documento, sino una colección de bordes del gráfico (borde) del autor de libros. La IN OUTBOUND a bookauthor selecciona para un Autor dado a colección de documentos relacionados, que coloca en la respuesta bajo el nombre book_vertex . La instrucción COLLECT a1 = a INTO b1 es un análogo de SQL GROUP: acumula un valor en una matriz que estará disponible en la respuesta con el nombre b1 para cada valor de Autor , que en la respuesta estará disponible con el nombre a1 . La construcción b1[*].book_vertex permite eliminar niveles innecesarios de anidamiento del objeto para que el resultado sea conveniente para un trabajo posterior.

Implementar consultas de tipo SQL LEFT JOIN en arangodb es más difícil porque la instrucción FOR - FILTER establece restricciones similares a SQL [INNER] JOIN. Para implementar las "conexiones izquierdas", se utiliza el oprerator LET y la subconsulta:

  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} `); 

En este caso, la agrupación de datos no es necesaria ya que Se realiza una subconsulta para cada autor y la respuesta ya contiene una matriz gótica de objetos de libro.

Vaya a la base de datos orientdb. También es una base de datos híbrida que le permite trabajar con documentos y gráficos. La ideología de trabajar con gráficos es similar al ejemplo anterior en arangodb. Es decir, para vincular colecciones, se utiliza una colección de bordes del gráfico del autor del libro.

 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() } () 

Quizás orientdb tenga la implementación más exitosa, como es más cercano a la sintaxis SQL y conciso en términos de trabajo con gráficos. La expresión out('BookAuthor').title as books from Author significa seleccionar para la colección Author todos los bordes salientes de la colección BookAuthor que vinculan la colección Author a la colección Book. En este caso, el objeto resultante será jerárquico (un objeto para cada Autor con una matriz de objetos Libro). Si necesita "expandir" la matriz en un objeto plano, se utiliza el operador UNWIND.

Y finalmente, considere repensar db. No hace mucho tiempo, el equipo que desarrolló esta base de datos dejó de existir y transfirió el desarrollo a la comunidad abierta. Lo digo de inmediato, porque alguien podría haberse saltado esta noticia. Antes de un conocimiento más detallado, me pareció que la implementación de JOIN en repensar lo más conveniente. Quizás porque esa oportunidad se incorporó de inmediato a la API de la base de datos, e incluso se llamó join (). Pero luego resultó que no todo es tan optimista, y no todas las funciones que implementan JOIN funcionan de manera igualmente eficiente y tienen suficiente flexibilidad para generar las consultas correctas. Nuestro mismo ejemplo de extremo a extremo ahora se implementa en 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); }(); 

Debes prestar atención a tales puntos. En este ejemplo, se implementó una comunicación de índice secundario utilizando la función eqJoin (), que puede usar pares al conectar objetos: una clave primaria con una clave primaria o una clave primaria con una clave secundaria (pero no una clave secundaria con una clave secundaria). Para condiciones más complejas, se utiliza la función map (), que es un orden de magnitud más difícil de entender. Otras funciones que implementan JOIN no están optimizadas (presumiblemente, se implementa una enumeración completa de valores).

El texto de los ejemplos se encuentra en el repositorio .

apapacy@gmail.com
4 de junio de 2018

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


All Articles