1. Code
  2. JavaScript
  3. Node

Пакетный импорт CSV - файла в MongoDB при помощи Mongoose и Node.js

Рад, что могу обсудить эту тему. Во многих веб-приложениях довольно часто происходит получение пользовательских данных и сохранение единственной записи в вашу базу данных. Но что если ваши пользователи (или вы сами) хотят выполнить массовую вставку одной командой?
Scroll to top
This post is part of a series called An Introduction to Mongoose for MongoDB and Node.js.
An Introduction to Mongoose for MongoDB and Node.js

Russian (Pусский) translation by AlexBioJS (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

Рад, что могу обсудить эту тему. Во многих веб-приложениях довольно часто происходит получение пользовательских данных и сохранение единственной записи в вашу базу данных. Но что если ваши пользователи (или вы сами) хотят выполнить массовую вставку одной командой?

Читайте статью далее, чтобы узнать, как создавать шаблон 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-файле есть два важных момента:

  1. Ссылка на "/template", при клике которой будет скачан шаблон CSV - файла, который можно заполнить информацией, которую необходимо отправить.
  2. Форма, свойству 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, для сохранения конечных записей.

Наслаждайтесь!