ExpressJS и Mongoose используют create несколько раз с функцией for

#javascript #node.js #mongodb #express #mongoose

#javascript #node.js #mongodb #выразить #mongoose

Вопрос:

На сайте, который я создаю, пользователи могут вводить разные теги и разделять их запятыми. Затем ExpressJS должен выполнить поиск, существуют ли они или нет. Если они не существуют, то он должен создать объект для каждого из них. У меня есть массив, и я перебираю его с помощью функции for, однако благодаря обратному вызову создается только один объект… Есть ли какой-либо возможный способ создать несколько объектов одновременно в зависимости от длины массива?

 for (i=0;i<postTopics.length;i  ) {
    var postTopic = postTopics[i],
        postTopicUrl = postTopic.toString().toLowerCase().replace(' ', '-');

    Topic.findOne({ "title": postTopics[i] }, function (err, topic) {

        if (err) throw err;

        if (!topic) {

            Topic.create({
                title: postTopic,
                url: postTopicUrl

            }, function (err, topic) {

                if (err) throw err;

                res.redirect('/');
            });
        }
    });
}
  

Комментарии:

1. Вместо того, чтобы находить одну тему за раз, почему бы не запросить их все и не просмотреть этот результат?

2. Можете ли вы показать мне пример? В любом случае я бы получил тот же результат, поскольку при создании тем есть функция обратного вызова.

3. Во-первых, есть ли у вас уникальный индекс по вашим темам? Если это так, вы могли бы просто выполнить один вызов DB для создания и игнорировать повторяющуюся ошибку, сохраняя вызов findOne. Во-вторых, вы отвечаете перенаправлением на первый ответ от вызова create, вы должны ждать, пока не ответят все асинхронные запросы mongo.

Ответ №1:

Попробуйте async.parallel .

$ npm установить асинхронно

 // Get the async module so we can do our parallel asynchronous queries much easier.
var async = require('async');

// Create a hash to store your query functions on.
var topicQueries = {};

// Loop through your postTopics once to create a query function for each one.
postTopics.forEach(function (postTopic) {
    // Use postTopic as the key for the query function so we can grab it later.
    topicQueries[postTopic] = function (cb) {
        // cb is the callback function passed in by async.parallel. It accepts err as the first argument and the result as the second.
        Topic.findOne({ title: postTopic }, cb);
    };
});

// Call async.parallel and pass in our topicQueries object.
// If any of the queries passed an error to cb then the rest of the queries will be aborted and this result function will be called with an err argument.
async.parallel(topicQueries, function (err, results) {
    if (err) throw err;

    // Create an array to store our Topic.create query functions. We don't need a hash because we don't need to tie the results back to anything else like we had to do with postTopics in order to check if a topic existed or not.
    var createQueries = [];

    // All our parallel queries have completed.
    // Loop through postTopics again, using postTopic to retrieve the resulting document from the results object, which has postTopic as the key.
    postTopics.forEach(function (postTopic) {
        // If there is no document at results[postTopic] then none was returned from the DB.
        if (results[postTopic]) return;

        // I changed .replace to use a regular expression. Passing a string only replaces the first space in the string whereas my regex searches the whole string.
        var postTopicUrl = postTopic.toString().toLowerCase().replace( g, '-');

        // Since this code is executing, we know there is no topic in the DB with the title you searched for, so create a new query to create a new topic and add it to the createQueries array.
        createQueries.push(function (cb) {
            Topic.create({
                title: postTopic,
                url: postTopicUrl
            }, cb);
        });
    });

    // Pass our createQueries array to async.parallel so it can run them all simultaneously (so to speak).
    async.parallel(createQueries, function (err, results) {
        // If any one of the parallel create queries passes an error to the callback, this function will be immediately invoked with that err argument.
        if (err) throw err;

        // If we made it this far, no errors were made during topic creation, so redirect.
        res.redirect('/');
    });
});
  

Сначала мы создаем объект с именем topicQueries и прикрепляем к нему функцию запроса для каждого postTopic заголовка в вашем postTopics массиве. Затем мы передаем завершенный topicQueries объект async.parallel , который будет выполнять каждый запрос и собирать результаты в results объект.

results Объект в конечном итоге становится простым объектным хэшем с каждым из ваших postTopic заголовков в качестве ключа, а значение является результатом из базы данных. if (results[postTopic]) return; Строка возвращает, если results под этим postTopic ключом нет документа. Это означает, что приведенный ниже код выполняется только в том случае, если из базы данных не была возвращена тема с таким названием. Если не было подходящей темы, мы добавляем функцию запроса в наш createQueries массив.

Мы не хотим, чтобы ваша страница перенаправлялась после завершения сохранения только одной из этих новых тем. Мы хотим дождаться завершения всех ваших запросов create, поэтому мы используем async.parallel еще раз, но на этот раз мы используем массив вместо хэша объекта, потому что нам не нужно ни к чему привязывать результаты. Когда вы передаете массив в async.parallel аргумент results также будет массивом, содержащим результаты каждого запроса, хотя в этом примере результаты нас не особо волнуют, только то, что ошибок не было выдано. Если parallel функция завершается и нет err аргумента, то все разделы успешно завершены, и мы можем, наконец, перенаправить пользователя на новую страницу.

PS — Если вы когда-нибудь столкнетесь с подобной ситуацией, за исключением того, что для каждого последующего запроса требуются данные из запроса перед ним, тогда оформите заказ async.waterfall 🙂

Комментарии:

1. Мой ответ предполагает, что title в вашей модели нет уникального индекса. Как указал адамК, если у вас есть уникальный индекс в поле title для ваших тематических документов, вы можете просто вызывать Topic.create для каждого postTopic и игнорировать ошибку, когда он жалуется на дублирующиеся записи. Затем вы можете использовать async.parallel только один раз, чтобы дождаться завершения всех запросов create. Просто имейте в виду, что вам, вероятно, придется вызывать cb себя, если вы пытаетесь игнорировать ошибку, потому что, если err будет передано в async, оно прервется и немедленно передаст ошибку в функцию результата.

Ответ №2:

Если вы действительно хотите увидеть, существуют ли вещи уже и избежать ошибок при дубликатах, то .create() метод уже принимает список. Похоже, вы не заботитесь о создании документа в ответ, поэтому просто проверьте наличие документов и отправьте новые.

Итак, с помощью «сначала найти» выполняйте задачи последовательно. async.waterfall просто для того, чтобы уменьшить ползучесть отступа:

 // Just a placeholder for your input
var topics = ["A Topic","B Topic","C Topic","D Topic"];

async.waterfall(
  [
    function(callback) {
      Topic.find(
        { "title": { "$in": topics } },
        function(err,found) {

          // assume ["Topic B", "Topic D"] are found
          found = found.map(function(x) {
            return x.title;
          });

          var newList = topics.filter(function(x) {
            return found.indexOf(x) == -1;
          });

          callback(err,newList);

        }
      );

    },
    function(newList,callback) {

      Topic.create(
        newList.map(function(x) {
          return {
            "title": x,
            "url": x.toString().toLowerCase().replace(' ','-')
          };
        }),
        function(err) {
          if (err) throw err;
          console.log("done");
          callback();
        }
      );
    }
  ]

);
  

Вы могли бы переместить генерацию «URL» в «предварительно» сохранить схему. Но опять же, если вам действительно не нужны правила проверки, перейдите к операциям «bulk API» при условии, что ваша целевая версия MongoDB и mongoose достаточно новая, чтобы поддерживать это, что действительно означает получение дескриптора базового драйвера:

 // Just a placeholder for your input
var topics = ["A Topic","B Topic","C Topic","D Topic"];

async.waterfall(
  [
    function(callback) {
      Topic.find(
        { "title": { "$in": topics } },
        function(err,found) {

          // assume ["Topic B", "Topic D"] are found
          found = found.map(function(x) {
            return x.title;
          });

          var newList = topics.filter(function(x) {
            return found.indexOf(x) == -1;
          });

          callback(err,newList);

        }
      );

    },
    function(newList,callback) {

      var bulk = Topic.collection.initializeOrderedBulkOp();
      newList.forEach(function(x) {
        bullk.insert({
          "title": x,
          "url": x.toString().toLowerCase().replace(' ','-')
        });
      });
      bulk.execute(function(err,results) {
        console.log("done");
        callback();
      });

    }
  ]
);
  

Это единственная операция записи на сервер, хотя, конечно, все вставки фактически выполняются по порядку и проверяются на наличие ошибок.

В противном случае просто удалите ошибки из дубликатов и вставьте как «неупорядоченную операцию», проверьте наличие ошибок «не дублирующихся» после, если хотите:

 // Just a placeholder for your input
var topics = ["A Topic","B Topic","C Topic","D Topic"];

var bulk = Topic.collection.initializeUnorderedBulkOp();
topics.forEach(function(x) {
  bullk.insert({
    "title": x,
    "url": x.toString().toLowerCase().replace(' ','-')
  });
});
bulk.execute(function(err,results) {
  if (err) throw err;
  console.log(JSON.stringify(results,undefined,4));
});
  

Вывод в результатах выглядит примерно следующим образом, указывая на «повторяющиеся» ошибки, но не «выдает» ошибку, поскольку в данном случае это не установлено:

 {
    "ok": 1,
    "writeErrors": [
        {
            "code": 11000,
            "index": 1,
            "errmsg": "insertDocument :: caused by :: 11000 E11000 duplicate key error index: test.topic.$title_1  dup key: { : "B Topic" }",
            "op": {
                "title": "B Topic",
                "url": "b-topic",
                "_id": "53b396d70fd421057200e610"
            }
        },
        {
            "code": 11000,
            "index": 3,
            "errmsg": "insertDocument :: caused by :: 11000 E11000 duplicate key error index: test.topic.$title_1  dup key: { : "D Topic" }",
            "op": {
                "title": "D Topic",
                "url": "d-topic",
                "_id": "53b396d70fd421057200e612"
            }
        }
    ],
    "writeConcernErrors": [],
    "nInserted": 2,
    "nUpserted": 0,
    "nMatched": 0,
    "nModified": 0,
    "nRemoved": 0,
    "upserted": []
}
  

Обратите внимание, что при использовании собственных методов сбора данных вам необходимо убедиться, что соединение уже установлено. Методы mongoose будут «стоять в очереди» до тех пор, пока не будет установлено соединение, но они этого не сделают. Скорее проблема с тестированием, если нет вероятности, что это будет первый выполненный код.

Надеемся, что версии этих массовых операций скоро будут представлены в mongoose API, но общая функциональность серверной части зависит от наличия MongoDB 2.6 или более поздней версии на сервере. Как правило, это будет лучший способ обработки.

Конечно, во всех примерах, кроме последнего, который в этом не нуждается, вы можете пойти абсолютно «асинхронно», вызывая версии «filter», «map» и «forEach», которые существуют в этой библиотеке. Вероятно, это не будет реальной проблемой, если вы не предоставляете действительно длинные списки для ввода.

Методы .initializeOrderedBulkOP() и .initializeUnorderedBulkOP() описаны в руководстве по встроенному драйверу узла. Также смотрите основное руководство для общего описания массовых операций.