Comment écouter les modifications apportées à une collection MongoDB?


200

Je crée une sorte de système de file d'attente de travaux en arrière-plan avec MongoDB comme magasin de données. Comment puis-je «écouter» les insertions d'une collection MongoDB avant de générer des travailleurs pour traiter le travail? Dois-je interroger toutes les quelques secondes pour voir s'il y a des changements par rapport à la dernière fois, ou existe-t-il un moyen pour mon script d'attendre les insertions? Il s'agit d'un projet PHP sur lequel je travaille, mais n'hésitez pas à répondre en Ruby ou en langage agnostique.


1
Change Streams a été ajouté dans MongoDB 3.6 pour répondre à votre scénario. docs.mongodb.com/manual/changeStreams De plus, si vous utilisez MongoDB Atlas, vous pouvez utiliser les déclencheurs de points qui vous permettent d'exécuter des fonctions en réponse à l'insertion / la mise à jour / la suppression / etc. docs.mongodb.com/stitch/triggers/overview Plus besoin d'analyser l'oplog.
Robert Walters

Réponses:


111

Ce à quoi vous pensez ressemble beaucoup à des déclencheurs. MongoDB ne prend pas en charge les déclencheurs, cependant certaines personnes ont "roulé les leurs" en utilisant quelques astuces. La clé ici est l'oplog.

Lorsque vous exécutez MongoDB dans un jeu de réplicas, toutes les actions MongoDB sont enregistrées dans un journal des opérations (appelé oplog). L'oplog est simplement une liste en cours d'exécution des modifications apportées aux données. Les jeux de répliques fonctionnent en écoutant les modifications sur cet oplog, puis en appliquant les modifications localement.

Cela vous semble-t-il familier?

Je ne peux pas détailler l'ensemble du processus ici, il s'agit de plusieurs pages de documentation, mais les outils dont vous avez besoin sont disponibles.

D'abord quelques articles sur l'oplog - Brève description - Présentation de la localcollection (qui contient l'oplog)

Vous voudrez également utiliser les curseurs disponibles . Ceux-ci vous fourniront un moyen d'écouter les changements au lieu de les interroger. Notez que la réplication utilise des curseurs disponibles, c'est donc une fonctionnalité prise en charge.


1
hmm ... pas exactement ce que j'avais en tête. Je n'exécute qu'une seule instance à ce stade (pas d'esclaves). Alors peut-être une solution plus basique?
Andrew

17
Vous pouvez démarrer le serveur avec l' --replSetoption et il créera / remplira le oplog. Même sans secondaire. C'est certainement la seule façon «d'écouter» les changements dans la base de données.
Gates VP

2
Voici une belle description de la façon de configurer oplog pour la journalisation locale des modifications de la base de données: loosexaml.wordpress.com/2012/09/03/…
johndodo

Cooooool! C'est vraiment ce que je veux. Et j'ai trouvé une bibliothèque nommée 'mongo-oplog' sur npm. So happy ~
pjincz

Je suis d'accord au moment d'écrire cette réponse, les déclencheurs pourraient ne pas être disponibles, mais pour tous ceux qui atterrissent ici, il existe une option disponible maintenant, consultez MongoDB Stitch ( docs.mongodb.com/stitch/#stitch ) & Stitch triggers ( docs. mongodb.com/stitch/triggers ) ..
whoami

102

MongoDB a ce qu'on appelle capped collectionset tailable cursorsqui permet à MongoDB de pousser les données vers les écouteurs.

A capped collectionest essentiellement une collection de taille fixe et n'autorise que les insertions. Voici à quoi cela ressemblerait pour en créer un:

db.createCollection("messages", { capped: true, size: 100000000 })

Curseurs MongoDB Tailable ( message original de Jonathan H. Wage )

Rubis

coll = db.collection('my_collection')
cursor = Mongo::Cursor.new(coll, :tailable => true)
loop do
  if doc = cursor.next_document
    puts doc
  else
    sleep 1
  end
end

PHP

$mongo = new Mongo();
$db = $mongo->selectDB('my_db')
$coll = $db->selectCollection('my_collection');
$cursor = $coll->find()->tailable(true);
while (true) {
    if ($cursor->hasNext()) {
        $doc = $cursor->getNext();
        print_r($doc);
    } else {
        sleep(1);
    }
}

Python (par Robert Stewart)

from pymongo import Connection
import time

db = Connection().my_db
coll = db.my_collection
cursor = coll.find(tailable=True)
while cursor.alive:
    try:
        doc = cursor.next()
        print doc
    except StopIteration:
        time.sleep(1)

Perl (par Max )

use 5.010;

use strict;
use warnings;
use MongoDB;

my $db = MongoDB::Connection->new;
my $coll = $db->my_db->my_collection;
my $cursor = $coll->find->tailable(1);
for (;;)
{
    if (defined(my $doc = $cursor->next))
    {
        say $doc;
    }
    else
    {
        sleep 1;
    }
}

Ressources supplémentaires:

Ruby / Node.js Tutorial qui vous guide à travers la création d'une application qui écoute les insertions dans une collection plafonnée MongoDB.

Un article qui parle plus en détail des curseurs disponibles.

Exemples PHP, Ruby, Python et Perl d'utilisation de curseurs disponibles.


70
dormir 1? vraiment? pour le code de production? comment est-ce que ce n'est pas un sondage?
rbp

2
@rbp haha, je n'ai jamais dit que c'était du code de production, mais vous avez raison, dormir une seconde n'est pas une bonne pratique. Je suis presque sûr d'avoir obtenu cet exemple ailleurs. Je ne sais pas comment le refactoriser.
Andrew

14
@kroe parce que ces détails non pertinents seront mis dans le code de production par de nouveaux programmeurs qui pourraient ne pas comprendre pourquoi c'est mauvais.
Catfish

3
Je comprends votre point, mais il est presque offensant d'attendre de nouveaux programmeurs qu'ils ajoutent "sleep 1" à la production! Je veux dire, je ne serais pas surpris ... Mais si quelqu'un met cela en production, au moins apprendra à la dure et pour toujours .. hahaha
kroe

19
qu'est-ce qui ne va pas avec time.sleep (1) en production?
Al Johri

44

Depuis MongoDB 3.6, il y aura une nouvelle API de notifications appelée Change Streams que vous pouvez utiliser pour cela. Voir cet article de blog pour un exemple . Exemple:

cursor = client.my_db.my_collection.changes([
    {'$match': {
        'operationType': {'$in': ['insert', 'replace']}
    }},
    {'$match': {
        'newDocument.n': {'$gte': 1}
    }}
])

# Loops forever.
for change in cursor:
    print(change['newDocument'])

4
Pourquoi? Peux-tu élaborer? C'est la voie standard maintenant?
Mitar

1
Comment? n'utilisez pas l'interrogation - vous avez besoin d'une approche événementielle au lieu de boucles while, etc.
Alexander Mills

3
Où voyez-vous le sondage ici?
Mitar

Je pense qu'il fait référence à la dernière boucle. Mais je pense que PyMongo ne supporte que cela. Le moteur peut avoir une implémentation de type écouteur asynchrone / événement.
Shane Hsu

41

Découvrez ceci: Modifier les flux

10 janvier 2018 - Version 3.6

* EDIT: j'ai écrit un article sur la façon de procéder https://medium.com/riow/mongodb-data-collection-change-85b63d96ff76

https://docs.mongodb.com/v3.6/changeStreams/


C'est nouveau dans mongodb 3.6 https://docs.mongodb.com/manual/release-notes/3.6/ 2018/01/10

$ mongod --version
db version v3.6.2

Pour utiliser changeStreams, la base de données doit être un jeu de réplication

En savoir plus sur les jeux de réplication: https://docs.mongodb.com/manual/replication/

Votre base de données sera un " autonome " par défaut.

Comment convertir un autonome en un jeu de répliques: https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/


L' exemple suivant est une application pratique de la façon dont vous pouvez l'utiliser.
* Spécifiquement pour Node.

/* file.js */
'use strict'


module.exports = function (
    app,
    io,
    User // Collection Name
) {
    // SET WATCH ON COLLECTION 
    const changeStream = User.watch();  

    // Socket Connection  
    io.on('connection', function (socket) {
        console.log('Connection!');

        // USERS - Change
        changeStream.on('change', function(change) {
            console.log('COLLECTION CHANGED');

            User.find({}, (err, data) => {
                if (err) throw err;

                if (data) {
                    // RESEND ALL USERS
                    socket.emit('users', data);
                }
            });
        });
    });
};
/* END - file.js */

Liens utiles:
https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set
https://docs.mongodb.com/manual/tutorial/change-streams-example

https://docs.mongodb.com/v3.6/tutorial/change-streams-example
http://plusnconsulting.com/post/MongoDB-Change-Streams


désolé pour toutes les modifications, SO n'aimait pas mes "Liens" (a déclaré qu'ils étaient du code mal formaté.)
Rio Weber

1
vous ne devriez pas avoir à interroger la base de données, je pense qu'avec watch () ou similaire, les nouvelles données peuvent être envoyées au serveur qui écoute
Alexander Mills

22

MongoDB version 3.6 inclut désormais des flux de modifications qui sont essentiellement une API au-dessus de l'OpLog permettant des cas d'utilisation de type déclencheur / notification.

Voici un lien vers un exemple Java: http://mongodb.github.io/mongo-java-driver/3.6/driver/tutorials/change-streams/

Un exemple NodeJS pourrait ressembler à quelque chose comme:

 var MongoClient = require('mongodb').MongoClient;
    MongoClient.connect("mongodb://localhost:22000/MyStore?readConcern=majority")
     .then(function(client){
       let db = client.db('MyStore')

       let change_streams = db.collection('products').watch()
          change_streams.on('change', function(change){
            console.log(JSON.stringify(change));
          });
      });

JSON.stringify est très important pour recevoir ces données dans Android Studio (application Android) ..
DragonFire

3

Vous pouvez également utiliser la méthode Mongo FindAndUpdate standard et, dans le rappel, déclencher un événement EventEmitter (dans Node) lorsque le rappel est exécuté.

Toute autre partie de l'application ou de l'architecture écoutant cet événement sera informée de la mise à jour, et toutes les données pertinentes y seront également envoyées. C'est un moyen très simple d'obtenir des notifications de Mongo.


c'est très inefficace .. vous verrouillez la base de données pour chaque FindAndUpdate!
Yash Gupta

1
Je suppose qu'Alex répondait à une question légèrement différente (ne traitant pas spécifiquement des insertions) mais liée à la façon de déclencher une sorte de notification aux clients lorsque l'état d'un travail en file d'attente change, ce qui, selon nous, doit se produire lorsque les travaux sont générés , terminer avec succès ou échouer. Avec les clients connectés à l'aide du Websockets au nœud, ils peuvent tous être informés des modifications avec un événement de diffusion sur le rappel FIndAndUpdate qui pourrait être appelé lors de la réception des messages de changement d'état. Je dirais que ce n'est pas inefficace car les mises à jour doivent être effectuées.
Peter Scott

3

Beaucoup de ces réponses ne vous donneront que de nouveaux enregistrements et non des mises à jour et / ou sont extrêmement inefficaces

La seule manière fiable et performante de le faire est de créer un curseur disponible sur la collection db: oplog.rs locale pour obtenir TOUTES les modifications de MongoDB et faire avec ce que vous voulez. (MongoDB le fait même plus ou moins en interne pour prendre en charge la réplication!)

Explication de ce que contient l'oplog: https://www.compose.com/articles/the-mongodb-oplog-and-node-js/

Exemple d'une bibliothèque Node.js qui fournit une API autour de ce qui est disponible pour être fait avec l'oplog: https://github.com/cayasso/mongo-oplog


2

Il existe un ensemble impressionnant de services disponibles appelé MongoDB Stitch . Examinez les fonctions / déclencheurs de points . Notez qu'il s'agit d'un service payant basé sur le cloud (AWS). Dans votre cas, sur un insert, vous pouvez appeler une fonction personnalisée écrite en javascript.

entrez la description de l'image ici


stackoverflow.com/users/486867/manish-jain - avez-vous un exemple de la façon dont le point peut être utilisé pour notifier à une application REACT que des données ont été insérées dans une table?
MLissCetrus

1

Il existe un exemple de java fonctionnel qui peut être trouvé ici .

 MongoClient mongoClient = new MongoClient();
    DBCollection coll = mongoClient.getDatabase("local").getCollection("oplog.rs");

    DBCursor cur = coll.find().sort(BasicDBObjectBuilder.start("$natural", 1).get())
            .addOption(Bytes.QUERYOPTION_TAILABLE | Bytes.QUERYOPTION_AWAITDATA);

    System.out.println("== open cursor ==");

    Runnable task = () -> {
        System.out.println("\tWaiting for events");
        while (cur.hasNext()) {
            DBObject obj = cur.next();
            System.out.println( obj );

        }
    };
    new Thread(task).start();

La clé est QUERY OPTIONS donnée ici.

Vous pouvez également modifier la requête de recherche, si vous n'avez pas besoin de charger toutes les données à chaque fois.

BasicDBObject query= new BasicDBObject();
query.put("ts", new BasicDBObject("$gt", new BsonTimestamp(1471952088, 1))); //timestamp is within some range
query.put("op", "i"); //Only insert operation

DBCursor cur = coll.find(query).sort(BasicDBObjectBuilder.start("$natural", 1).get())
.addOption(Bytes.QUERYOPTION_TAILABLE | Bytes.QUERYOPTION_AWAITDATA);

1

En fait, au lieu de regarder la sortie, pourquoi vous ne recevez pas d'avis quand quelque chose de nouveau est inséré en utilisant un middleware fourni par le schéma mangouste

Vous pouvez attraper l'événement d'insérer un nouveau document et faire quelque chose après cette insertion


Ma faute. Désolé monsieur.
Duong Nguyen

0

Après 3.6, on est autorisé à utiliser la base de données, les types de déclencheurs de base de données suivants:

  • déclencheurs déclenchés par les événements - utiles pour mettre à jour automatiquement les documents connexes, notifier les services en aval, propager les données pour prendre en charge des charges de travail mixtes, l'intégrité des données et l'audit
  • déclencheurs planifiés - utiles pour les charges de travail planifiées de récupération, de propagation, d'archivage et d'analyse de données

Connectez-vous à votre compte Atlas et sélectionnez l' Triggersinterface et ajoutez un nouveau déclencheur:

entrez la description de l'image ici

Développez chaque section pour plus de paramètres ou de détails.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.