TL; DR: в этом сообщении подробно описывается мой метод использования сценариев Node для поиска по каждому полю в каждом документе MongoDB в нескольких коллекциях в базе данных и обновления полей, которые соответствуют определенным критериям (в этом примере: удаление новой строки символы из строк, например \r и \n). Предоставляется множество примеров кода, включая ссылку на пример репозитория.

Итак, вот сценарий, который мне недавно представили в рамках клиентского проекта: я унаследовал старую базу данных, которая за годы накопила много интересного. Одна из интересных проблем, с которыми он столкнулся, заключалась в том, что многие поля таинственным образом приобрели многочисленные символы новой строки где-то в конце строки: \r и \n.

Откуда они пришли, сказать было нечего. В контексте приложения это действительно не вызывало никаких проблем с самого начала, хотя в последнее время эти символы новой строки вызывали некоторые проблемы для клиента, и, следовательно, их пришлось, наконец, очистить от всех данных.

Возник вопрос, как именно это сделать?

1. Наивный подход

Запрос самих данных для поиска полей в документах, содержащих эти символы новой строки, не представляет большого труда, команда оболочки MongoDB будет выглядеть примерно так:

db.collection.find({fieldName: /\r\n/})

С fieldName, представляющим данное поле в документе, и /\r\n/, представляющим поиск по регулярному выражению для этих символов новой строки. Поиск в документах и ​​понимание того, где присутствуют эти символы новой строки и сколько их было, было быстрой работой. Было много.

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

db.collection.find({ fieldName: /\r\n/ })
 .forEach(function(document) {
   document.fieldName = document.fieldName.replace(‘\r\n’, ‘’);
   db.collection.save(document);
});

Подводя итог вышеупомянутой команде оболочки: мы 1) выполняем поиск документов с символами новой строки в их поле fieldName, 2) перебираем эти документы и выполняем замену строки в этих полях, заменяя все символы новой строки пустыми строка и 3) снова записать их в БД.

Однако сложность заключается в том, что мы не знаем точно, где находятся эти персонажи. Они могут быть в одних полях в одних документах, но отсутствовать в других. А учитывая, что в рассматриваемых документах есть около дюжины + полей, становится довольно сложно копировать и вставлять эту команду оболочки снова и снова для каждого поля. Умножьте все это ручное копирование на несколько коллекций баз данных, и внезапно это превратится в довольно трудоемкую, хрупкую и скучную задачу. Множество возможностей для человеческой ошибки, и, кроме того, у нас есть компьютеры, которые сделают за нас всю эту ручную работу!

2: сценарийный подход

Давайте напишем сценарий, чтобы добиться всего, что нам нужно делать с этими данными. Обобщая цели, нам необходимо:

  • Запросить каждый документ в нескольких коллекциях
  • Искать в каждом поле в этих документах символы новой строки
  • Замените эти символы новой строки пустыми строками, если и когда мы их найдем

По мере написания кода мы собираемся писать наши функции как методы для объекта, а затем вызывать эти методы позже, чтобы все было аккуратно, модульно и тестируемым. Мы также собираемся использовать Promises, чтобы упростить управление потоком, особенно с учетом того, что подключение к MongoDB является асинхронным (если вы не знакомы с обещаниями, я настоятельно рекомендую этот эпизод Fun Fun Function по этой теме).

Итак, во-первых, давайте подключимся к базе данных:

const MongoUpdater = {
  ... 
  getConnection: function(url, dbName){
    return new Promise((resolve, reject) => {
      MongoClient.connect(url, (err, connection) => {
        if (err) {
          reject(err);
        }
        resolve(connection.db(dbName))
      })
    })
  },
  ...
}

Для подключения все, что вам нужно сделать, это:

MongoUpdater.getConnection('mongodb://localhost:27017', 'test')
  .then(db => console.log(db) ) // -> MongoDB database

Затем нам нужно получить коллекцию, в которой мы будем выполнять поиск и обновления. Написание отдельного метода для получения коллекции на данном этапе может показаться немного глупым, но помните, что мы работаем с несколькими коллекциями, поэтому выделение этой отдельной функции упрощает работу с несколькими коллекциями:

const MongoUpdater = {
  ...
  getCollection: function(collectionName, db) {
    return db.collection(collectionName)
  },
  ...
}

Расширяя наш предыдущий пример, вот вызовы функций, которые вы должны выполнить для получения коллекции:

MongoUpdater.getConnection(‘mongodb://localhost:27017’, ‘test’)
.then(db => {
  let collection = MongoUpdater.getCollection(‘test_collection’, db)     
  // -> MongoDB collection

Теперь, когда у нас есть наша коллекция, нам нужно запросить все документы в этой коллекции, преобразовать их в массив и начать творить чудеса:

MongoUpdater.getConnection('mongodb://localhost:27017', 'test')
  .then(db => {
    MongoUpdater.getCollection('test_collection', db)
    .then(collection => {
      collection.find({}).toArray(docs => {
        // magic goes here!
      }
    })
  })
})

Использование reduce для обновления наших документов

Теперь, когда у нас есть наши документы, мы должны проверить каждое поле в данном документе, чтобы увидеть, содержат ли они символы новой строки, оставив все остальные поля неизменными. Все это требует циклического перебора свойств этого объекта документа, что, если вы спросите меня, немного затрудняет Javascript. Учитывая поставленную задачу, начинающий функциональный программист во мне просто кричит, чтобы использовать Array.prototype.reduce. Если вы не сталкивались с reduce в своих путешествиях по программированию, а) вы должны серьезно изучить его, и б) для этого есть Эпизод Fun Fun Function.

const MongoUpdater = {
  ...
  processDocFields: function(doc, regex) {
    // first, we get the all the keys of the document
    let docKeys = Object.keys(doc)
    // next, we build a new object based on those keys
    let processedDocument = docKeys.reduce((current, next) => {
      // we only want to process fields with strings
      // and definitely don't want to touch the _id field 
      if (typeof doc[next] != 'string' || next === '_id') {
        // if the field isn't a string or is the _id field,
        // we stick it into the new object we're building
        current[next] = doc[next]
        return current
      }
      // otherwise, we replace anything in the field that matches 
      // the regex pattern with an empty string, and stick it back 
      // in the object
      current[next] = doc[next].replace(regex, '')
      return current
    }, {})
    // function spits out our processed document
    return processedDocument
  },
  ...
}

И вот оно! После этого остается просто написать ваш свежий, чистый документ обратно в БД, и у вас есть тушеное мясо:

MongoUpdater.getConnection('mongodb://localhost:27017', 'test')
.then(db => {
  let collection = MongoUpdater.getCollection('test_collection', db)
  collection.find({}).toArray(docs => {
    // First, throw the docs array into a loop
    docs.forEach(doc => {
      // Within the loop, process the document object
      let processedDoc = MongoUpdater.processDocFields(doc, regex)
      // Now that it's processed, we save it back into the database
      collection.update(
        // We locate the existing document by its _id field
        { _id: ObjectID(processedDoc._id) },
        // then pass in your new, cleaned up document as the update
        processedDoc,
        (err, status) => {
          // -> document has been updated in the DB!
        })
      })
    }
  })
})

И теперь остается лишь добавить несколько циклов для выполнения одних и тех же операций с несколькими коллекциями. Вот как это выглядит, с некоторыми абстракциями для краткости:

let collections = ['test_collection1', 'test_collection2', 'test_collection3']
let dbUrl = 'mongodb://localhost:27017'
let dbName = 'testDB'
let regex = /\r\n/
MongoUpdater.getConnection(dbUrl, dbName)
  .then(db => {
    testCollections.forEach(coll => {
      let collection = MongoUpdater.getCollection(coll, db)
      collection.find().toArray(docs => {
        docs.forEach(doc => {
          let newDoc = MongoUpdater.processDocFields(doc, regex)
          MongoUpdater.save(newDoc, collection)
        })
      })
    })
  })

Вывод

С помощью некоторых хитроумных сценариев Node вы можете автоматизировать все виды операций обновления ваших баз данных MongoDB, что не только избавит вас от множества неприятных операций по копированию и вставке команд оболочки (и сопутствующей человеческой ошибки), но и упростит выполнение аналогичных обновлений. в будущем, если вы столкнетесь с той же проблемой. Кроме того, вы можете адаптировать сценарий для других подобных сценариев. Вам нужно объединить имена и фамилии пользователей в один регистр предложения? Заменить сокращение конкретного слова на полное слово или наоборот? Проверить орфографию и исправить часто повторяющееся слово? Вы действительно могли сделать все, что угодно.

Все вышеперечисленное доступно в этом репозитории Github вместе со сценариями для настройки тестовой базы данных MongoDB, чтобы вы могли запускать сценарии самостоятельно и сами видеть, как они работают, экспериментировать с изменениями или адаптировать код к вашему собственные потребности.

Спасибо за чтение! Удачного кодирования.