加入NoSQL数据库

这篇文章将讨论如何在NoSQL数据库mongodb,arangodb,orientdb和rethinkdb中连接集合(除了作为NoSQL数据库之外,它们还具有免费版本并具有相当忠诚的许可证)。 在关系数据库中,使用SQL JOIN实现了类似的功能。 尽管NoSQL数据库中的CRUD操作非常相似且仅在细节上有所不同,例如,在一个数据库中,create({...})函数用于创建对象,在另一个数据库中插入({...}),并在第三个-保存({...})-在每个数据库中对两个或多个集合的选择的实现是以完全不同的方式实现的。 因此,对所有数据库执行相同的选择将很有趣。 对于所有数据库,将考虑两个表的采样(多对多关系)。

例如,将使用表。

作者


这本书

  • 职称

图书作者

  • 作者
  • 这本书

对于mongodb,将从表中进行选择的方式如下:

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

与SQL JOIN选择不同,结果选择将不是一个平面表,在该表中Author将重复他所写的书,而是一个分层对象,其中每个Author将由一个对象表示,该对象的books属性包含一个Book对象数组。 从我的角度来看,这在NoSQL数据库中是一个很大的优势。 但是您可能还需要类似于SQL JOIN的“扁平”版本。 为了在请求中接收它,您需要添加数组的“扩展”: { $unwind: '$books' }

该示例中提供的示例是SQL LEFT JOIN的类似物,也就是说,即使没有书籍,所有作者也都将包括在示例中。 为了模拟SQL [INNER] JOIN,必须添加条件{ $match: { books: { $ne: [ ] } } } ,或者如果使用$ unwind:

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

因此,让我们继续到arangodb,这是一个混合数据库。 除了处理文档之外,它还实现了对图形的处理。 首先,让我们看看如何在arangodb中仅使用文档(而不是图形)进行选择:

  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使用FOR关键字连接集合,并使用FILTER指定连接条件。 在这种情况下获得的示例将类似于SQL [INNER] JOIN(也就是说,它将是一个“平面”对象,并且不包含空值)

但是在arangodb中使用图形功能从多个对象中进行选择要方便得多:

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

现在,我们用于交流的不是文档,而是书作者的图的边缘(边缘)的集合。 IN OUTBOUND a bookauthorIN OUTBOUND a bookauthor ,为给定​​的Author选择一个相关文档a集合,它将其放置在响应中的名称为book_vertexCOLLECT a1 = a INTO b1语句COLLECT a1 = a INTO b1是SQL GROUP的类似物-它在数组中累积一个值,该值将在Author中每个值a1的名称b1下在响应中可用,而在响应中将以a1名称提供。 构造b1[*].book_vertex允许b1[*].book_vertex从对象中删除不必要的嵌套层次,以便为进一步的工作提供方便。

在arangodb中实现SQL LEFT JOIN类型查询更加困难,因为 FOR-FILTER语句设置与SQL [INNER] JOIN相似的限制。 为了实现“左连接”,使用了LET操作器和子查询:

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

在这种情况下,不需要数据分组,因为 对每个作者执行一个子查询,并且响应已经包含Book对象的哥特式数组。

转到orientdb数据库。 它也是一个混合数据库,可让您使用文档和图形。 使用图的思想与arangodb中的先前示例相似。 即,为了链接集合,使用书作者图的边缘的集合。

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

也许orientdb具有最成功的实现,因为 它最接近SQL语法,并且在使用图形方面简洁明了。 表达式out('BookAuthor').title as books from Author表示为Author集合选择BookAuthor集合中所有将Author集合链接到Book集合的输出边。 在这种情况下,结果对象将是分层的(每个作者一个对象,带有一个Book对象数组)。 如果需要将数组“扩展”为平面对象,则使用UNWIND运算符。

最后,考虑rethinkdb。 不久之前,开发此数据库的团队不复存在,并将开发转移到开放社区。 我马上说,因为 可能有人跳过了此新闻。 在更详细地了解之前,在我看来,在rethinkdb中实现JOIN最方便。 也许是因为这样的机会被立即合并到数据库API中,甚至被称为join()。 但是后来发现,并不是所有事情都那么乐观,并且并非所有实现JOIN的函数都能同样有效地工作,并且没有足够的灵活性来构建正确的查询。 现在,我们在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); }(); 

您应该注意这些要点。 在此示例中,辅助索引通信是使用eqJoin()函数实现的,该函数在连接对象时可以使用对:主键和主键或主键和辅助键(但不带主键的辅助键)。 对于更复杂的条件,使用map()函数,这是一个较难理解的数量级。 实现JOIN的其他函数未优化(大概实现了值的完整枚举)。

示例的文本位于资源库中

apapacy@gmail.com
六月4,2018

Source: https://habr.com/ru/post/zh-CN413123/


All Articles