Пакетный импорт CSV - файла в MongoDB при помощи Mongoose и Node.js
Russian (Pусский) translation by AlexBioJS (you can also view the original English article)



Рад, что могу обсудить эту тему. Во многих веб-приложениях довольно часто происходит получение пользовательских данных и сохранение единственной записи в вашу базу данных. Но что если ваши пользователи (или вы сами) хотят выполнить массовую вставку одной командой?
Читайте статью далее, чтобы узнать, как создавать шаблон CSV (comma-separated values - значения, разделённые запятыми) - файла и форму для загрузки CSV - файла и как осуществить парсинг CSV - файла для создания модели Mongoose, которую сохраним в базу данных MongoDB.
Предполагается, что у вас есть базовые представления о Mongoose и ее взаимодействии с MongoDB. Если же их нет, то я предлагаю вам для начала прочитать мою статью Введение в Mongoose для MongoDB и Node.js. В ней рассказывается, как Mongoose взаимодействует с MongoDB путем создания строго-типизированных схем, из которых создается модель. Если вы уже хорошо понимаете Mongoose, поехали дальше.
Начало работы
Для начала давайте создадим новое приложение Node.js. Перейдите в командной строке в директорию, куда вы хотите установить ваше приложение Node.js, и выполните следующие команды:
1 |
mkdir csvimport
|
2 |
cd csvimport
|
3 |
npm init |
Я оставил значения по умолчанию как есть, поэтому мое приложение запустится при помощи файла index.js. Сперва, до создания и парсинга CSV - файлов, нам необходимо осуществить первоначальные настройки. Я хочу сделать веб-приложение. Для этого я собираюсь поручить выполнение всех основных настроек сервера модулю Express. Для установки Express выполните в консоли следующую команду:
1 |
npm install express --save |
Так как наше веб-приложение будет принимать файлы при помощи веб-формы, я собираюсь также использовать подмодуль Express Express File Upload. Давайте теперь установим и его:
1 |
npm install express-fileupload --save |
Этой первоначальной конфигурации достаточно, чтобы установить моё веб-приложение и создать базовую веб-страницу с формой для загрузки файлов на сервер.
Ниже приведен мой index.js, в котором устанавливается мой веб-сервер:
1 |
var app = require('express')(); |
2 |
var fileUpload = require('express-fileupload'); |
3 |
var server = require('http').Server(app); |
4 |
|
5 |
app.use(fileUpload()); |
6 |
|
7 |
server.listen(80); |
8 |
|
9 |
app.get('/', function (req, res) { |
10 |
res.sendFile(__dirname + '/index.html'); |
11 |
});
|
В этом примере мы подключаем модули Express и Express File Upload, настраиваем веб-приложение на использование File Upload и прослушиваем подключения на 80 порту. Также в данном примере мы создали с помощью Express маршрут для обработки запросов к "/", по которому будет отсылаться целевая страница по умолчанию моего веб-приложения. В ответ на запросы по этому пути отправляется файл index.html, содержащий веб-форму, которая позволяет пользователю закачать CSV - файл. Поскольку приложение работает на моем локальном компьютере, то в моём случае при переходе на http://localhost я увижу форму, которую создам в следующем примере.
Ниже приведена моя страница index.htm с формой для закачивания CSV - файла:
1 |
<!DOCTYPE html>
|
2 |
<html lang="en"> |
3 |
<head>
|
4 |
<title>Upload Authors</title> |
5 |
</head>
|
6 |
<body>
|
7 |
<p>Use the form below to upload a list of authors.
|
8 |
Click <a href="/template">here</a> for an example template.</p> |
9 |
<form action="/" method="POST" encType="multipart/form-data"> |
10 |
<input type="file" name="file" accept="*.csv" /><br/><br/> |
11 |
<input type="submit" value="Upload Authors" /> |
12 |
</form>
|
13 |
</body>
|
14 |
</html>
|
В данном HTML-файле есть два важных момента:
- Ссылка на "/template", при клике которой будет скачан шаблон CSV - файла, который можно заполнить информацией, которую необходимо отправить.
- Форма, свойству
encTypeкоторой присвоено значениеmultipart/form-data, и поле для ввода типаfile, которое принимает файлы с расширением "csv".
Как вы могли заметить, HTML-файл ссылается на шаблон Author. Если вы читали мою статью 'Введение в Mongoose...', то помните, что я создал схему Author. В этой статье я собираюсь воссоздать эту схему и создать для пользователя возможность массового импорта коллекции авторов в мою базу данных MongoDB. Давайте посмотрим на схему Author. Однако, вы, вероятно, догадались, что сначала нам необходимо установить модуль Mongoose:
1 |
npm install mongoose --save |
Создание схемы и модели
Теперь, когда у нас установлен Mongoose, давайте создадим новый файл author.js, в котором определим схему и модель Author:
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
var authorSchema = mongoose.Schema({ |
4 |
_id: mongoose.Schema.Types.ObjectId, |
5 |
name: { |
6 |
firstName: { |
7 |
type: String, |
8 |
required: true |
9 |
},
|
10 |
lastName: String |
11 |
},
|
12 |
biography: String, |
13 |
twitter: { |
14 |
type: String, |
15 |
validate: { |
16 |
validator: function(text) { |
17 |
if (text !== null && text.length > 0) |
18 |
return text.indexOf('https://twitter.com/') === 0; |
19 |
|
20 |
return true; |
21 |
},
|
22 |
message: 'Twitter handle must start with https://twitter.com/' |
23 |
}
|
24 |
},
|
25 |
facebook: { |
26 |
type: String, |
27 |
validate: { |
28 |
validator: function(text) { |
29 |
if (text !== null && text.length > 0) |
30 |
return text.indexOf('https://www.facebook.com/') === 0; |
31 |
|
32 |
return true; |
33 |
},
|
34 |
message: 'Facebook Page must start with https://www.facebook.com/' |
35 |
}
|
36 |
},
|
37 |
linkedin: { |
38 |
type: String, |
39 |
validate: { |
40 |
validator: function(text) { |
41 |
if (text !== null && text.length > 0) |
42 |
return text.indexOf('https://www.linkedin.com/') === 0; |
43 |
|
44 |
return true; |
45 |
},
|
46 |
message: 'LinkedIn must start with https://www.linkedin.com/' |
47 |
}
|
48 |
},
|
49 |
profilePicture: Buffer, |
50 |
created: { |
51 |
type: Date, |
52 |
default: Date.now |
53 |
}
|
54 |
});
|
55 |
|
56 |
var Author = mongoose.model('Author', authorSchema); |
57 |
|
58 |
module.exports = Author; |
Теперь, когда у нас созданы схема и модель Author, давайте переключимся и сосредоточимся на создании шаблона CSV - файла, который можно скачать, кликнув по ссылке шаблона. Для упрощения генерации шаблона CSV - файла я собираюсь использовать модуль JSON (JavaScript Object Notation - текстовый формат обмена данными, основанный на JavaScript) to CSV. Давайте теперь установим его:
1 |
npm install json2csv --save |
Теперь я собираюсь обновить мой заранее созданный файл index.js, чтобы добавить новый маршрут для обработки запросов к "/template":
1 |
var template = require('./template.js'); |
2 |
app.get('/template', template.get); |
Я привел только новый код для маршрута шаблона, который добавляем в предыдущий файл index.js.
В коде происходит следующее: подключение нового файла template.js (будет создан следующим) и создание маршрута для обработки запросов к "/template". При запросах к пути этого маршрута будет вызвана функция get в файле template.js.
Теперь, когда у нас обновлен сервер Express добавлением нового маршрута, давайте создадим новый файл template.js:
1 |
var json2csv = require('json2csv'); |
2 |
|
3 |
exports.get = function(req, res) { |
4 |
|
5 |
var fields = [ |
6 |
'name.firstName', |
7 |
'name.lastName', |
8 |
'biography', |
9 |
'twitter', |
10 |
'facebook', |
11 |
'linkedin' |
12 |
];
|
13 |
|
14 |
var csv = json2csv({ data: '', fields: fields }); |
15 |
|
16 |
res.set("Content-Disposition", "attachment;filename=authors.csv"); |
17 |
res.set("Content-Type", "application/octet-stream"); |
18 |
|
19 |
res.send(csv); |
20 |
|
21 |
};
|
В этом файле я сначала подключаю предварительно установленный модуль json2csv. Затем создаю и экспортирую функцию get. Эта функция принимает в качестве аргументов объекты запроса и ответа от сервера Express.
Внутри функции я создал массив полей, которые хочу использовать в моем шаблоне CSV - файла. Создать список полей можно различными способами. Первый (которым я воспользовался в этом примере) - это создание статического списка полей, которые необходимо включить в шаблон. Второй - это динамическое создание списка полей путем извлечения свойств из схемы Author.
Второй способ можно было бы осуществить при помощи следующего кода:
1 |
var fields = Object.keys(Author.schema.obj); |
Я бы захотел использовать этот динамический метод, если бы не трудности, которые возникают, если я не хочу включать множество свойств из схемы в шаблон CSV - файла. В моём примере в шаблоне не используются свойства _id и created, так как их значения будут заданы с помощью кода. Однако, если у вас нет полей, которые вы хотите исключить, то динамический способ также подходит.
Создание шаблона CSV - файла
После определения массива полей я использую модуль json2csv для создания из объекта JavaScript шаблона CSV - файла. Этот объект csv будет результатом запросов к маршруту с путем '/template'.
И, наконец, используя свойство res сервера Express, я устанавливаю значения двух заголовков, что обеспечит скачивание файла authors.csv.
Если бы вы запустили ваше Node-приложение на данном этапе и перешли в своем веб-браузере по ссылке http://localhost, то на экране отобразилась бы веб-форма со ссылкой для скачивания шаблона. Кликнув по ссылке для скачивания шаблона, вы скачаете файл authors.csv для заполнения перед закачиванием.
Ниже приведен пример заполненного CSV - файла:
1 |
name.firstName,name.lastName,biography,twitter,facebook,linkedin |
2 |
Jamie,Munro,Jamie is a web developer and author,,, |
3 |
Mike,Wilson,Mike is a web developer and Node.js author,,, |
В результате закачивания этого шаблона будет создано два автора: я и мой друг, который написал книгу о Node.js несколько лет назад. Вы, наверное, заметили, что в конце каждой строки расположены три запятые ", , ,". Это сделано для сокращения примера. Я не заполнил свойства социальных сетей (twitter, facebook и linkedin).
Кусочки мозаики начинают собираться в единое целое и формировать общую картину. Давайте перейдем к самому важному нашего примера и осуществим парсинг CSV - файла. Нам необходимо немного обновить файл index.js для подключения к MongoDB и создания нового маршрута для обработки запросов POST, по которому на сервер загружается файл.
1 |
var app = require('express')(); |
2 |
var fileUpload = require('express-fileupload'); |
3 |
var mongoose = require('mongoose'); |
4 |
|
5 |
var server = require('http').Server(app); |
6 |
|
7 |
app.use(fileUpload()); |
8 |
|
9 |
server.listen(80); |
10 |
|
11 |
mongoose.connect('mongodb://localhost/csvimport'); |
12 |
|
13 |
app.get('/', function (req, res) { |
14 |
res.sendFile(__dirname + '/index.html'); |
15 |
});
|
16 |
|
17 |
var template = require('./template.js'); |
18 |
app.get('/template', template.get); |
19 |
|
20 |
var upload = require('./upload.js'); |
21 |
app.post('/', upload.post); |
После подключения к базе данных и настройки нового маршрута для обработки запросов POST пришло время осуществить парсинг CSV - файла. К счастью, для этого есть несколько отличных библиотек. Я решил использовать модуль fast-csv, который вы можете установить при помощи следующей команды:
1 |
npm install fast-csv --save |
Маршрут для обработки запросов POST был создан подобно маршруту шаблона. В нем вызывается функция post из файла upload.js. Нет необходимости помещать функции маршрутов в разные файлы.Однако, мне хочется сделать разные файлы для этих маршрутов, поскольку это помогает поддерживать читабельность и организованность кода.
Отправка данных
И, наконец, давайте создадим файл upload.js, в котором содержится функция post, которую вызываем после отправки заранее созданной формы:
1 |
var csv = require('fast-csv'); |
2 |
var mongoose = require('mongoose'); |
3 |
var Author = require('./author'); |
4 |
|
5 |
exports.post = function (req, res) { |
6 |
if (!req.files) |
7 |
return res.status(400).send('No files were uploaded.'); |
8 |
|
9 |
var authorFile = req.files.file; |
10 |
|
11 |
var authors = []; |
12 |
|
13 |
csv
|
14 |
.fromString(authorFile.data.toString(), { |
15 |
headers: true, |
16 |
ignoreEmpty: true |
17 |
})
|
18 |
.on("data", function(data){ |
19 |
data['_id'] = new mongoose.Types.ObjectId(); |
20 |
|
21 |
authors.push(data); |
22 |
})
|
23 |
.on("end", function(){ |
24 |
Author.create(authors, function(err, documents) { |
25 |
if (err) throw err; |
26 |
});
|
27 |
|
28 |
res.send(authors.length + ' authors have been successfully uploaded.'); |
29 |
});
|
30 |
};
|
Довольно много происходит в этом файле. В первых трех строках мы подключаем необходимые модули, которые понадобятся для осуществления парсинга и сохранения данных CSV - файла.
Далее определяется и экспортируется функция post для использования в файле index.js. Именно внутри этой функции происходит волшебство.
Сначала функция проверяет наличие файла в теле запроса. Если файл отсутствует, то возвращается ошибка, указывающая, что необходимо закачать файл.
Когда файл закачан, ссылка на файл сохраняется в переменную с именем authorFile благодаря обращению к массиву files и свойству file этого массива. Имя свойства file соответствует имени поля для отправки файла, которое я указал в примере index.html.
Также я создал массив authors, который будет наполняться по мере того как осуществляется парсинг CSV - файла. Этот массив будет использоваться для сохранения данных в базу данных.
Далее вызывается библиотека fast-csv при использовании ее функции fromString. Функция принимает CSV - файл в виде строки. Я извлек строку из свойства authorFile.data. В свойстве data находится содержимое моего закачанного CSV - файла.
Я передал две опции в функцию fromString модуля fast-csv: headers и ignoreEmpty. Значениями обоих является true. Это сообщает библиотеке, что в первой строке CSV - файла будут содержаться заголовки и что необходимо игнорировать пустые строки.
Теперь, когда опции настроены, я определил две функции слушателей событий, которые вызываются при генерировании событий data и end. Событие data генерируется один раз для каждой строки CSV - файла. В этом событии содержится объект JavaScript данных, подвергшихся парсингу.
Я обновляю этот объект для включения свойства _id схемы Author с новым значением типа ObjectId (уникальный идентификатор объекта, первичный ключ, _id). Далее этот объект добавляется в массив authors.
После того, как осуществился парсинг всего CSV - файла, генерируется событие end. Внутри функции обратного вызова для этого события я вызываю функцию create модели Author, передавая ей массив authors.
Если при сохранении массива происходит ошибка, то выкидывается исключение. В ином случае пользователю отображается сообщение о количестве авторов, закачанных и сохраненных в базу данных.
На случай, если бы вы хотели увидеть весь исходный код, я создал репозиторий GitHub с кодом.
Заключение
В моём примере я закачал только несколько записей. Если в вашем случае необходима закачка тысяч записей, то было бы неплохо сохранять записи небольшими частями.
Это можно осуществить различными способами. Если бы мне нужно было это реализовать, то я бы предложил обновить функцию обратного вызова, зарегистрированную на событие data, добавив проверку длины массива authors. При превышении длины массива установленной вами длины, например, 100, вызовите функцию Author.create для этого массива и затем сбросьте (присвойте значение 0) массив. Затем функция сохранит записи частями по 100. Убедитесь, что оставили последний вызов функции create в функции обратного вызова, зарегистрированной на событие end, для сохранения конечных записей.
Наслаждайтесь!



