Como ouvir alterações em uma coleção do MongoDB?

200

Estou criando um tipo de sistema de fila de tarefas em segundo plano com o MongoDB como armazenamento de dados. Como posso "escutar" inserções em uma coleção do MongoDB antes de gerar trabalhadores para processar o trabalho? Preciso pesquisar a cada poucos segundos para ver se há alguma alteração da última vez, ou existe uma maneira de meu script aguardar a inserção de inserções? Este é um projeto PHP em que estou trabalhando, mas sinta-se à vontade para responder em Ruby ou em linguagem independente.

Andrew
fonte
1
O Change Streams foi adicionado no MongoDB 3.6 para abordar seu cenário. docs.mongodb.com/manual/changeStreams Além disso, se você estiver usando o MongoDB Atlas, poderá usar os Triggers de ponto que permitem executar funções em resposta a inserir / atualizar / excluir / etc. docs.mongodb.com/stitch/triggers/overview Não é mais necessário analisar o oplog.
Robert Walters

Respostas:

111

O que você está pensando soa muito como gatilhos. O MongoDB não tem suporte para gatilhos, no entanto, algumas pessoas "criaram seus próprios" usando alguns truques. A chave aqui é o oplog.

Quando você executa o MongoDB em um conjunto de réplicas, todas as ações do MongoDB são registradas em um log de operações (conhecido como oplog). O oplog é basicamente apenas uma lista em execução das modificações feitas nos dados. Réplicas Define a função ouvindo as alterações neste oplog e aplicando as alterações localmente.

Isso soa familiar?

Não posso detalhar todo o processo aqui, são várias páginas de documentação, mas as ferramentas necessárias estão disponíveis.

Primeiro, alguns artigos escritos no oplog - Breve descrição - Layout da localcoleção (que contém o oplog)

Você também deseja aproveitar os cursores disponíveis . Isso fornecerá uma maneira de ouvir as alterações, em vez de pesquisar por elas. Observe que a replicação usa cursores disponíveis, portanto, esse é um recurso suportado.

Gates VP
fonte
1
hmm ... não exatamente o que eu tinha em mente. Estou executando apenas uma instância neste momento (sem escravos). Então, talvez uma solução mais básica?
Andrew
17
Você pode iniciar o servidor com a --replSetopção e ele criará / preencherá o arquivo oplog. Mesmo sem o secundário. Essa é definitivamente a única maneira de "ouvir" as alterações no banco de dados.
Gates VP
2
Esta é uma boa descrição de como configurar o oplog para registrar alterações no banco de dados localmente: loosexaml.wordpress.com/2012/09/03/…
johndodo
Cooooool! É isso mesmo que eu quero. E eu encontrei uma biblioteca chamada 'mongo-oplog' no npm. Tão feliz ~
pjincz
Concordo que no momento em que escrevo esta resposta, os gatilhos de resposta podem não estar disponíveis, mas para todos os que chegam aqui, existe uma opção disponível agora: Confira o MongoDB Stitch ( docs.mongodb.com/stitch/#stitch ) e os gatilhos de Stitch ( docs. mongodb.com/stitch/triggers ) ..
whoami
102

O MongoDB possui o que é chamado capped collectionse tailable cursorsisso permite que o MongoDB envie dados para os ouvintes.

A capped collectioné essencialmente uma coleção de tamanho fixo e permite apenas inserções. Aqui está como seria criar um:

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

Cursores disponíveis do MongoDB ( postagem original de Jonathan H. Wage )

Rubi

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 (de 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 (de 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 adicionais:

Tutorial do Ruby / Node.js, que orienta você na criação de um aplicativo que ouve inserções em uma coleção limitada do MongoDB.

Um artigo falando sobre cursores disponíveis em mais detalhes.

Exemplos de PHP, Ruby, Python e Perl do uso de cursores disponíveis.

Andrew
fonte
70
dormir 1? realmente? para código de produção? como isso não está sendo pesquisado?
RBP
2
@ rbp haha, eu nunca disse que era código de produção, mas você está certo, dormir por um segundo não é uma boa prática. Tenho certeza de que recebi esse exemplo de outro lugar. Não sei como refatorá-lo embora.
Andrew
14
@kroe porque esses detalhes irrelevantes serão inseridos no código de produção por programadores mais novos que podem não entender por que é ruim.
Catfish
3
Entendo o seu ponto de vista, mas esperar que alguns novos programadores adicionem "suspensão 1" à produção é quase ofensivo! Quer dizer, eu não ficaria surpreso ... Mas se alguém coloca isso em produção, pelo menos vai aprender da maneira mais difícil e para sempre .. hahaha
kroe
19
o que há de errado em fazer time.sleep (1) na produção?
Al Johri
44

Desde o MongoDB 3.6, haverá uma nova API de notificações chamada Change Streams, que você pode usar para isso. Veja esta postagem no blog para obter um exemplo . Exemplo disso:

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
fonte
4
Por quê? Você pode elaborar? Esta é a maneira padrão agora?
Mitar
1
como? não use polling - você precisa de uma abordagem de evento em vez de while loops, etc. #
Alexander Mills
3
Onde você vê as pesquisas aqui?
Mitar
Eu acho que ele / ela está se referindo ao último loop. Mas acho que o PyMongo apenas apoia isso. O motor pode ter uma implementação no estilo de ouvinte assíncrono / evento.
Shane Hsu
41

Confira isto: Change Streams

10 de janeiro de 2018 - Versão 3.6

* EDIT: escrevi um artigo sobre como fazer isso https://medium.com/riow/mongodb-data-collection-change-85b63d96ff76

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


É novo no mongodb 3.6 https://docs.mongodb.com/manual/release-notes/3.6/ 2018/01/10

$ mongod --version
db version v3.6.2

Para usar o changeStreams, o banco de dados deve ser um Conjunto de Replicação

Mais sobre conjuntos de replicação: https://docs.mongodb.com/manual/replication/

Seu banco de dados será " autônomo " por padrão.

Como converter um autônomo em um conjunto de réplicas: https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/


O exemplo a seguir é um aplicativo prático de como você pode usar isso.
* Especificamente para Nó.

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

Links úteis:
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
fonte
desculpe por todas as edições, por isso não como meus "Links" (disseram que foram indevidamente formatado código.)
Rio Weber
1
você não deve ter para consultar o banco de dados, eu acho que com relógio () ou similar, os novos dados podem ser enviados para o servidor que está ouvindo
Alexander Mills
22

O MongoDB versão 3.6 agora inclui fluxos de alterações, que são essencialmente uma API no topo do OpLog, permitindo casos de uso semelhantes a acionamentos / notificações.

Aqui está um link para um exemplo de Java: http://mongodb.github.io/mongo-java-driver/3.6/driver/tutorials/change-streams/

Um exemplo do NodeJS pode ser algo como:

 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
fonte
JSON.stringify é muito importante para receber esses dados no Android Studio (Android App) ..
DragonFire
3

Como alternativa, você pode usar o método Mongo FindAndUpdate padrão e, dentro do retorno de chamada, acionar um evento EventEmitter (no Node) quando o retorno de chamada é executado.

Quaisquer outras partes do aplicativo ou arquitetura que estiverem ouvindo esse evento serão notificadas sobre a atualização e quaisquer dados relevantes enviados para lá também. Essa é uma maneira muito simples de obter notificações do Mongo.

Alex
fonte
isso é muito ineficiente .. você está bloqueando o banco de dados para cada FindAndUpdate!
Yash Gupta
1
Meu palpite é que Alex estava respondendo a uma pergunta um pouco diferente (não abordando especificamente as inserções), mas relacionada a como acionar algum tipo de notificação aos clientes quando o estado de um trabalho na fila mudar, que assumimos que precisará acontecer quando os trabalhos forem gerados , conclua com êxito ou falhe. Com os clientes conectados usando websockets ao nó, todos eles podem ser notificados de alterações com um evento de transmissão no retorno de chamada FIndAndUpdate, que pode ser chamado ao receber mensagens de alteração de estado. Eu diria que isso não é ineficiente, pois as atualizações precisam ser feitas.
Peter Scott
3

Muitas dessas respostas fornecerão apenas novos registros e não atualizações e / ou são extremamente ineficientes

A única maneira confiável e eficiente de fazer isso é criar um cursor disponível na coleção db: oplog.rs local para obter TODAS as alterações no MongoDB e fazer o que você desejar. (O MongoDB ainda faz isso internamente mais ou menos para oferecer suporte à replicação!)

Explicação do conteúdo do oplog: https://www.compose.com/articles/the-mongodb-oplog-and-node-js/

Exemplo de uma biblioteca Node.js que fornece uma API em torno do que está disponível para ser feito com o oplog: https://github.com/cayasso/mongo-oplog

John Culviner
fonte
2

Existe um impressionante conjunto de serviços disponíveis chamado MongoDB Stitch . Observe as funções / gatilhos do ponto . Observe que este é um serviço pago baseado na nuvem (AWS). No seu caso, em uma inserção, você pode chamar uma função personalizada escrita em javascript.

insira a descrição da imagem aqui

Manish Jain
fonte
stackoverflow.com/users/486867/manish-jain - você tem um exemplo de como o stitch pode ser usado para notificar um aplicativo REACT que dados foram inseridos em uma tabela?
MLissCetrus 10/04
1

Há um exemplo de java que pode ser encontrado aqui .

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

A chave é QUERY OPTIONS fornecida aqui.

Além disso, você pode alterar a consulta de busca, se não precisar carregar todos os dados todas as vezes.

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
fonte
1

Na verdade, em vez de assistir à saída, por que você não recebe aviso quando algo novo é inserido usando o middleware que foi fornecido pelo esquema de mangusto

Você pode capturar o evento de inserir um novo documento e fazer alguma coisa após essa inserção

Duong Nguyen
fonte
Foi mal. Desculpe senhor.
Duong Nguyen
0

Após o 3.6, é permitido usar o banco de dados, os seguintes tipos de gatilhos de banco de dados:

  • acionadores acionados por eventos - úteis para atualizar documentos relacionados automaticamente, notificar serviços posteriores, propagar dados para suportar cargas de trabalho mistas, integridade e auditoria de dados
  • gatilhos agendados - úteis para cargas de trabalho agendadas de recuperação, propagação, arquivamento e análise de dados

Entre na sua conta Atlas, selecione a Triggersinterface e adicione um novo gatilho:

insira a descrição da imagem aqui

Expanda cada seção para obter mais configurações ou detalhes.

gotqn
fonte