¿Cómo escuchar los cambios en una colección MongoDB?

200

Estoy creando una especie de sistema de cola de trabajos en segundo plano con MongoDB como el almacén de datos. ¿Cómo puedo "escuchar" las inserciones de una colección MongoDB antes de generar trabajadores para procesar el trabajo? ¿Tengo que sondear cada pocos segundos para ver si hay algún cambio desde la última vez, o hay alguna forma en que mi script pueda esperar a que ocurran las inserciones? Este es un proyecto PHP en el que estoy trabajando, pero no dudes en responder en Ruby o en lenguaje agnóstico.

Andrés
fuente
1
Change Streams se agregó en MongoDB 3.6 para abordar su escenario. docs.mongodb.com/manual/changeStreams Además, si está utilizando MongoDB Atlas, puede aprovechar Stitch Triggers que le permiten ejecutar funciones en respuesta a insertar / actualizar / eliminar / etc. docs.mongodb.com/stitch/triggers/overview Ya no es necesario analizar el oplog.
Robert Walters

Respuestas:

111

Lo que estás pensando suena mucho a disparadores. MongoDB no tiene ningún soporte para los desencadenantes, sin embargo, algunas personas han "hecho lo suyo" usando algunos trucos. La clave aquí es el oplog.

Cuando ejecuta MongoDB en un conjunto de réplicas, todas las acciones de MongoDB se registran en un registro de operaciones (conocido como oplog). El oplog es básicamente una lista de las modificaciones realizadas a los datos. Los conjuntos de réplicas funcionan escuchando los cambios en este registro y luego aplicando los cambios localmente.

¿Te suena familiar?

No puedo detallar todo el proceso aquí, son varias páginas de documentación, pero las herramientas que necesita están disponibles.

Primero algunos relatos sobre el oplog - Descripción breve - Diseño de la localcolección (que contiene el oplog)

También querrás aprovechar los cursores disponibles . Estos le proporcionarán una forma de escuchar los cambios en lugar de sondearlos. Tenga en cuenta que la replicación utiliza cursores disponibles, por lo que esta es una característica compatible.

Vicepresidente de Gates
fuente
1
hmm ... no es exactamente lo que tenía en mente. Solo estoy ejecutando una instancia en este momento (sin esclavos). Entonces, ¿tal vez una solución más básica?
Andrew
17
Puede iniciar el servidor con la --replSetopción y creará / completará el oplog. Incluso sin la secundaria. Esta es definitivamente la única forma de "escuchar" los cambios en la base de datos.
Gates VP
2
Esta es una buena descripción de cómo configurar el registro de operaciones para registrar cambios en DB localmente: loosexaml.wordpress.com/2012/09/03/…
johndodo
Cooooool! Eso es realmente lo que quiero. Y encontré una biblioteca llamada 'mongo-oplog' en npm. Tan feliz ~
pjincz
Estoy de acuerdo en el momento de escribir esta respuesta, los desencadenantes pueden no estar disponibles, pero para todos los que aterrizan aquí, hay una opción disponible ahora, consulte MongoDB Stitch ( docs.mongodb.com/stitch/#stitch ) y Stitch triggers ( docs. mongodb.com/stitch/triggers ) ..
whoami
102

MongoDB tiene lo que se llama capped collectionsy tailable cursorseso le permite a MongoDB enviar datos a los oyentes.

A capped collectiones esencialmente una colección que tiene un tamaño fijo y solo permite inserciones. Así es como se vería crear uno:

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

Cursores Tago MongoDB ( publicación original de Jonathan H. Wage )

Rubí

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 (por 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 (por 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;
    }
}

Recursos adicionales:

Tutorial Ruby / Node.js que lo guía a través de la creación de una aplicación que escucha inserciones en una colección con tapa MongoDB.

Un artículo que habla sobre los cursores tailable con más detalle.

Ejemplos de PHP, Ruby, Python y Perl del uso de cursores disponibles.

Andrés
fuente
70
dormir 1? ¿De Verdad? para el código de producción? ¿Cómo es que no sondeo?
rbp
2
@rbp jaja, nunca dije que era un código de producción, pero tienes razón, dormir por un segundo no es una buena práctica. Estoy bastante seguro de que obtuve ese ejemplo de otro lugar. Sin embargo, no estoy seguro de cómo refactorizarlo.
Andrew
14
@kroe porque esos detalles irrelevantes serán puestos en código de producción por programadores más nuevos que pueden no entender por qué es malo.
Catfish
3
Entiendo tu punto, ¡pero esperar que algunos nuevos programadores agreguen "dormir 1" a la producción es casi ofensivo! Quiero decir, no me sorprendería ... Pero si alguien pone esto en producción, al menos aprenderá de la manera difícil y para siempre ... jajaja
kroe
19
¿Qué hay de malo en hacer time.sleep (1) en producción?
Al Johri
44

Desde MongoDB 3.6 habrá una nueva API de notificaciones llamada Change Streams que puede usar para esto. Vea esta publicación de blog para un ejemplo . Ejemplo de ello:

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'])
Mitar
fuente
44
¿Por qué? ¿Puedes elaborar? Esta es la forma estándar ahora?
Mitar
1
¿cómo? no use encuestas: necesita un enfoque uniforme en lugar de bucles while, etc.
Alexander Mills
3
¿Dónde ves encuestas aquí?
Mitar
Creo que él / ella se está refiriendo al último bucle. Pero creo que PyMongo solo es compatible con eso. Motor podría tener una implementación de estilo de escucha asíncrono / evento.
Shane Hsu
41

Mira esto: Cambiar transmisiones

10 de enero de 2018 - Versión 3.6

* EDITAR: escribí un artículo sobre cómo hacer esto https://medium.com/riow/mongodb-data-collection-change-85b63d96ff76

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


Es nuevo en mongodb 3.6 https://docs.mongodb.com/manual/release-notes/3.6/ 2018/01/10

$ mongod --version
db version v3.6.2

Para usar changeStreams, la base de datos debe ser un conjunto de replicación

Más acerca de los conjuntos de replicación: https://docs.mongodb.com/manual/replication/

Su base de datos será un " Standalone " por defecto.

Cómo convertir un conjunto independiente a un conjunto de réplicas: https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/


El siguiente ejemplo es una aplicación práctica de cómo puede usar esto.
* Específicamente para Nodo.

/* 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 */

Enlaces útiles:
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

Rio Weber
fuente
perdón por todas las ediciones, SO no me gustó mis "Enlaces" (dijo que tenían un formato incorrecto.)
Rio Weber
1
no debería tener que consultar la base de datos, creo que con watch () o similar, los nuevos datos se pueden enviar al servidor que está escuchando
Alexander Mills
22

La versión 3.6 de MongoDB ahora incluye flujos de cambio que es esencialmente una API en la parte superior del OpLog que permite casos de uso de tipo disparador / notificación.

Aquí hay un enlace a un ejemplo de Java: http://mongodb.github.io/mongo-java-driver/3.6/driver/tutorials/change-streams/

Un ejemplo de NodeJS podría verse así:

 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));
          });
      });
Robert Walters
fuente
JSON.stringify es muy importante para recibir estos datos en Android Studio (aplicación de Android) ..
DragonFire
3

Alternativamente, puede utilizar el método estándar Mongo FindAndUpdate y, dentro de la devolución de llamada, activar un evento EventEmitter (en Nodo) cuando se ejecuta la devolución de llamada.

Cualquier otra parte de la aplicación o arquitectura que escuche este evento será notificada de la actualización, y cualquier información relevante enviada allí también. Esta es una forma realmente simple de lograr notificaciones de Mongo.

Alex
fuente
esto es muy ineficiente ... ¡estás bloqueando la base de datos para cada FindAndUpdate!
Yash Gupta
1
Mi conjetura es que Alex estaba respondiendo una pregunta ligeramente diferente (no abordando específicamente las inserciones) pero relacionada con cómo enviar algún tipo de notificación a los clientes cuando cambia el estado de un trabajo en cola que suponemos que tendrá que suceder a medida que se generan los trabajos , completar con éxito o fallar. Con los clientes conectados mediante WebSockets al nodo, todos pueden ser notificados de los cambios con un evento de difusión en la devolución de llamada FIndAndUpdate que se puede llamar cuando se reciben mensajes de cambio de estado. Yo diría que esto no es ineficiente ya que las actualizaciones deben hacerse.
Peter Scott
3

Muchas de estas respuestas solo le darán nuevos registros y no actualizaciones y / o son extremadamente ineficientes

La única forma confiable y eficaz de hacer esto es crear un cursor en la colección local db: oplog.rs para obtener TODOS los cambios en MongoDB y hacer lo que quiera. (¡MongoDB incluso hace esto internamente más o menos para admitir la replicación!)

Explicación de lo que contiene el oplog: https://www.compose.com/articles/the-mongodb-oplog-and-node-js/

Ejemplo de una biblioteca Node.js que proporciona una API sobre lo que está disponible para hacer con el oplog: https://github.com/cayasso/mongo-oplog

John Culviner
fuente
2

Hay un increíble conjunto de servicios disponibles llamado MongoDB Stitch . Mira en funciones de puntos / triggers . Tenga en cuenta que este es un servicio pago basado en la nube (AWS). En su caso, en una inserción, puede llamar a una función personalizada escrita en javascript.

ingrese la descripción de la imagen aquí

Manish Jain
fuente
stackoverflow.com/users/486867/manish-jain : ¿tiene un ejemplo de cómo se puede usar stitch para notificar a una aplicación REACT que los datos se insertaron en una tabla?
MLissCetrus
1

Hay un ejemplo de Java en funcionamiento que se puede encontrar aquí .

 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 clave es OPCIONES DE CONSULTA aquí.

También puede cambiar la consulta de búsqueda, si no necesita cargar todos los datos cada vez.

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);
Maleen Abewardana
fuente
1

En realidad, en lugar de mirar la salida, ¿por qué no se nota cuando se inserta algo nuevo utilizando el middleware proporcionado por el esquema de mangosta?

Puede detectar el evento de insertar un nuevo documento y hacer algo después de esta inserción

Duong Nguyen
fuente
Culpa mía. Lo siento, señor.
Duong Nguyen
0

Después de 3.6 se permite usar la base de datos, los siguientes tipos de desencadenantes de la base de datos:

  • activadores controlados por eventos: útiles para actualizar documentos relacionados automáticamente, notificar servicios posteriores, propagar datos para admitir cargas de trabajo mixtas, integridad de datos y auditoría
  • disparadores programados: útiles para las cargas de trabajo de recuperación, propagación, archivo y análisis de datos programados

Inicie sesión en su cuenta Atlas, seleccione la Triggersinterfaz y agregue un nuevo disparador:

ingrese la descripción de la imagen aquí

Expanda cada sección para más configuraciones o detalles.

gotqn
fuente