JOIN dans les bases de données NoSQL

Cet article expliquera comment connecter des collections dans les bases de données NoSQL mongodb, arangodb, orientdb et rethinkdb (en plus d'être des bases de données NoSQL, elles ont également une version gratuite avec une licence assez fidèle). Dans les bases de données relationnelles, des fonctionnalités similaires sont implémentées à l'aide de SQL JOIN. Malgré le fait que les opérations CRUD dans les bases de données NoSQL sont très similaires et ne diffèrent que dans les détails, par exemple, dans une base de données, la fonction create ({...}) est utilisée pour créer un objet, insérer ({...}) dans l'autre et dans la troisième - save ({...}), - l'implémentation d'une sélection de deux ou plusieurs collections dans chacune des bases de données est implémentée de manières complètement différentes. Il sera donc intéressant d'effectuer la même sélection sur toutes les bases de données. Pour toutes les bases de données, l'échantillonnage (une relation plusieurs-à-plusieurs) pour deux tables sera considéré.

Par exemple, des tableaux seront utilisés.

L'auteur

  • prénom

Le livre

  • Le titre

LivresAuteur

  • L'auteur
  • Le livre

Pour mongodb, la sélection dans les tables sera implémentée comme ceci:

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

Contrairement à la sélection SQL JOIN, la sélection résultante ne sera pas une table plate dans laquelle l'auteur sera répété autant de livres qu'il a écrit, mais un objet hiérarchique dans lequel chaque auteur sera représenté par un objet qui aura la propriété books contenant un tableau d'objets Book . De mon point de vue, c'est un très gros avantage dans les bases de données NoSQL. Mais vous pouvez également avoir besoin d'une version «plate», similaire à SQL JOIN. Pour le recevoir dans la demande, vous devez ajouter l '"expansion" des tableaux: { $unwind: '$books' } .

L'exemple présenté dans l'exemple est un analogue de SQL LEFT JOIN, c'est-à-dire que tous les auteurs seront inclus dans l'échantillon, même s'ils n'ont pas de livres. Pour créer un analogue de SQL [INNER] JOIN, vous devez ajouter la condition { $match: { books: { $ne: [ ] } } } , ou si $ unwind est utilisé:

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

Passons donc à arangodb, qui est une base de données hybride. En plus de travailler avec des documents, il implémente un travail avec des graphiques. Tout d'abord, voyons comment dans arangodb vous pouvez faire une sélection en utilisant uniquement des documents (pas des graphiques):

  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 utilise le mot clé FOR pour joindre des collections et FILTER pour spécifier la condition de jointure. L'exemple obtenu dans ce cas sera similaire à SQL [INNER] JOIN (c'est-à-dire qu'il s'agira d'un objet «plat» et ne contiendra pas de valeurs vides)

Mais il est beaucoup plus pratique dans arangodb d'utiliser des fonctionnalités de graphique pour les sélections à partir de plusieurs objets:

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

Nous utilisons maintenant pour la communication non pas un document, mais une collection de bords du graphique (bord) de l'auteur du livre. L' IN OUTBOUND a bookauthor sélectionne pour un auteur donné a collection de documents associés, qu'elle place dans la réponse sous le nom book_vertex . L'instruction COLLECT a1 = a INTO b1 est un analogue de SQL GROUP - elle accumule une valeur dans un tableau qui sera disponible dans la réponse sous le nom b1 pour chaque valeur de Author , qui dans la réponse sera disponible sous le nom a1 . La construction b1[*].book_vertex permet de supprimer des niveaux d'imbrication inutiles de l'objet afin que le résultat soit pratique pour un travail ultérieur.

L'implémentation de requêtes de type SQL LEFT JOIN dans arangodb est plus difficile car l'instruction FOR-FILTER définit des restrictions similaires à SQL [INNER] JOIN. Pour implémenter les «connexions gauches», l'opérateur LET et la sous-requête sont utilisés:

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

Dans ce cas, le regroupement des données n'est pas nécessaire car une sous-requête est effectuée pour chaque auteur et la réponse contient déjà un tableau gothique d'objets Book.

Accédez à la base de données orientdb. Il s'agit également d'une base de données hybride qui vous permet de travailler à la fois avec des documents et des graphiques. L'idéologie du travail avec les graphiques est similaire à l'exemple précédent dans arangodb. Autrement dit, pour lier des collections, une collection d'arêtes du graphe d'auteur de livres est utilisée.

 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 a peut-être la mise en œuvre la plus réussie, comme il est le plus proche de la syntaxe SQL et concis en termes de travail avec les graphiques. L'expression out('BookAuthor').title as books from Author signifie sélectionner pour la collection Author tous les bords sortants de la collection BookAuthor qui lient la collection Author à la collection Book. Dans ce cas, l'objet résultant sera hiérarchique (un objet pour chaque auteur avec un tableau d'objets Book). Si vous devez "étendre" le tableau en un objet plat, l'opérateur UNWIND est utilisé.

Et enfin, pensez à repenser. Il n'y a pas si longtemps, l'équipe développant cette base de données a cessé d'exister et a transféré le développement à la communauté ouverte. Je le dis tout de suite, car quelqu'un aurait pu ignorer cette nouvelle. Avant une connaissance plus détaillée, l'implémentation de JOIN dans rethinkdb me semblait la plus pratique. Peut-être parce qu'une telle opportunité a été immédiatement incorporée dans l'API de la base de données, et même appelée join (). Mais il s'est avéré que tout n'était pas si rose, et toutes les fonctions qui implémentent JOIN fonctionnent de manière aussi efficace et ont une flexibilité suffisante pour créer les bonnes requêtes. Notre même exemple de bout en bout est maintenant implémenté sur 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); }(); 

Vous devez faire attention à ces points. Dans cet exemple, la communication d'index secondaire a été implémentée à l'aide de la fonction eqJoin (), qui peut utiliser des paires lors de la connexion d'objets: une clé primaire avec une clé primaire ou une clé primaire avec une clé secondaire (mais pas une clé secondaire avec une clé secondaire). Pour des conditions plus complexes, la fonction map () est utilisée, ce qui est un ordre de grandeur plus difficile à comprendre. Les autres fonctions qui implémentent JOIN ne sont pas optimisées (vraisemblablement, une énumération complète des valeurs est implémentée).

Le texte des exemples se trouve dans le référentiel .

apapacy@gmail.com
4 juin 2018

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


All Articles