Статьи

Массовый импорт файла CSV в MongoDB с использованием Mongoose с Node.js

Конечный продукт
Что вы будете создавать

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

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

В этой статье предполагается, что у вас есть базовое понимание Mongoose и того, как он взаимодействует с MongoDB. Если вы этого не сделаете, я бы посоветовал сначала прочитать мою статью « Введение в Mongoose для MongoDB» и «Node.js» . В этой статье описывается, как Mongoose взаимодействует с MongoDB путем создания строго типизированных схем, из которых создается модель. Если у вас уже есть хорошее понимание Mongoose, давайте продолжим.

Для начала давайте создадим новое приложение Node.js. В командной строке перейдите туда, где вы хотите разместить свои приложения Node.js, и выполните следующие команды:

1
2
3
mkdir csvimport
cd csvimport
npm init

Я оставил все настройки по умолчанию, поэтому мое приложение будет запускаться с index.js . Перед созданием и анализом CSV-файлов сначала необходимо выполнить некоторые начальные настройки. Я хочу сделать это веб-приложением; чтобы сделать это, я собираюсь использовать пакет Express, чтобы обработать все настройки сервера. В командной строке установите Express, выполнив следующую команду:

1
npm install express —save

Поскольку это веб-приложение будет принимать файлы через веб-форму, я также собираюсь использовать подпакет Express Express File Upload. Давайте установим это и сейчас:

1
npm install express-fileupload —save

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

Вот мой файл index.js который устанавливает мой веб-сервер:

01
02
03
04
05
06
07
08
09
10
11
var app = require(‘express’)();
var fileUpload = require(‘express-fileupload’);
var server = require(‘http’).Server(app);
 
app.use(fileUpload());
 
server.listen(80);
 
app.get(‘/’, function (req, res) {
  res.sendFile(__dirname + ‘/index.html’);
});

В этом примере импортируются библиотеки Express и Express File Upload, настраивается мое веб-приложение для использования загрузки файлов и прослушивается порт 80. В этом примере также был создан маршрут с использованием Express в «/», который будет целевой страницей по умолчанию для моего веб-сайта. заявление. Этот маршрут возвращает файл index.html который содержит веб-форму, которая позволит пользователю загрузить файл CSV. В моем случае я работаю на своем локальном компьютере, поэтому при посещении http: // localhost я увижу форму, которую я создаю в следующем примере.

Вот моя страница index.html которая создает мою форму для загрузки файла CSV:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!DOCTYPE html>
<html lang=»en»>
<head>
    <title>Upload Authors</title>
</head>
<body>
    <p>Use the form below to upload a list of authors.
        Click <a href=»/template»>here</a> for an example template.</p>
    <form action=»/» method=»POST» encType=»multipart/form-data»>
        <input type=»file» name=»file» accept=»*.csv» /><br/><br/>
        <input type=»submit» value=»Upload Authors» />
    </form>
</body>
</html>

Этот HTML-файл содержит две важные вещи:

  1. Ссылка на «/ template», при нажатии на которую загружается шаблон CSV, в который можно ввести информацию для импорта.
  2. Форма с параметром encType установленным как multipart/form-data и полем ввода с типом file который принимает файлы с расширением «csv».

Как вы, возможно, заметили, в HTML содержится ссылка на шаблон Author. Если вы прочитали мою статью «Введение в Mongoose», я создал схему автора. В этой статье я собираюсь воссоздать эту схему и позволить пользователю массово импортировать коллекцию авторов в мою базу данных MongoDB. Давайте посмотрим на схему автора. Однако, прежде чем мы это сделаем, вы, наверное, догадались — нам нужно установить пакет Mongoose:

1
npm install mongoose —save

С установленным Mongoose, давайте создадим новый файл author.js , который определит схему и модель автора:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var mongoose = require(‘mongoose’);
 
var authorSchema = mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    name: {
        firstName: {
            type: String,
            required: true
        },
        lastName: String
    },
    biography: String,
    twitter: {
        type: String,
        validate: {
            validator: function(text) {
                if (text !== null && text.length > 0)
                    return text.indexOf(‘https://twitter.com/’) === 0;
                 
                return true;
            },
            message: ‘Twitter handle must start with https://twitter.com/’
        }
    },
    facebook: {
        type: String,
        validate: {
            validator: function(text) {
                if (text !== null && text.length > 0)
                    return text.indexOf(‘https://www.facebook.com/’) === 0;
                 
                return true;
            },
            message: ‘Facebook Page must start with https://www.facebook.com/’
        }
    },
    linkedin: {
        type: String,
        validate: {
            validator: function(text) {
                if (text !== null && text.length > 0)
                    return text.indexOf(‘https://www.linkedin.com/’) === 0;
                 
                return true;
            },
            message: ‘LinkedIn must start with https://www.linkedin.com/’
        }
    },
    profilePicture: Buffer,
    created: {
        type: Date,
        default: Date.now
    }
});
 
var Author = mongoose.model(‘Author’, authorSchema);
 
module.exports = Author;

С созданной авторской схемой и моделью давайте переключимся и сосредоточимся на создании шаблона CSV, который можно скачать, нажав на ссылку шаблона. Чтобы помочь с генерацией шаблона CSV, я собираюсь использовать пакет JSON to CSV. Давайте установим это сейчас:

1
npm install json2csv —save

Сейчас я собираюсь обновить свой ранее созданный файл index.js чтобы включить новый маршрут для «/ template»:

1
2
var template = require(‘./template.js’);
app.get(‘/template’, template.get);

Я включил только новый код для шаблона маршрута, который добавлен к предыдущему файлу index.js .

Первое, что делает этот код, это включает новый файл template.js (который будет создан далее) и создает маршрут для «/ template». Этот маршрут вызовет функцию get в файле template.js .

Обновив сервер Express, добавив новый маршрут, давайте создадим новый файл template.js :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
var json2csv = require(‘json2csv’);
 
exports.get = function(req, res) {
 
    var fields = [
        ‘name.firstName’,
        ‘name.lastName’,
        ‘biography’,
        ‘twitter’,
        ‘facebook’,
        ‘linkedin’
    ];
 
    var csv = json2csv({ data: », fields: fields });
 
    res.set(«Content-Disposition», «attachment;filename=authors.csv»);
    res.set(«Content-Type», «application/octet-stream»);
 
    res.send(csv);
 
};

Этот файл сначала включает в себя ранее установленный пакет json2csv . Затем я создаю и экспортирую функцию get . Эта функция принимает объекты запроса и ответа от сервера Express.

Внутри функции я создал массив полей, которые я хочу включить в шаблон CSV. Это можно сделать одним из двух способов. Первый способ (это делается в этом примере) — создать статический список полей, которые будут включены в шаблон. Второй способ — это динамическое создание списка полей путем извлечения свойств из схемы автора.

Второй способ можно сделать с помощью следующего кода:

1
var fields = Object.keys(Author.schema.obj);

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

Определив массив полей, я использую пакет json2csv для создания моего шаблона CSV из моего объекта JavaScript. Этот объект csv будет результатом этого маршрута.

И наконец, используя свойство res с сервера Express, я установил два свойства заголовка, которые будут принудительно загружать файл authors.csv .

На этом этапе, если вы запустите приложение Node и перейдете по адресу http: // localhost в своем веб-браузере, появится веб-форма со ссылкой для загрузки шаблона. Нажав на ссылку для загрузки шаблона, вы сможете загрузить файл authors.csv который будет заполнен до его загрузки.

Вот пример заполненного файла CSV:

1
2
3
name.firstName,name.lastName,biography,twitter,facebook,linkedin
Jamie,Munro,Jamie is a web developer and author,,,
Mike,Wilson,Mike is a web developer and Node.js author,,,

В этом примере при загрузке будут созданы два автора: я и мой друг, который написал книгу на Node.js несколько лет назад. Вы можете заметить, что в конце каждой строки есть три запятые «,,,». Это сделано для сокращения примера. Я не заполнил свойства социальных сетей ( twitter , facebook и linkedin ).

Кусочки головоломки начинают собираться вместе и формировать картину. Давайте разберемся с мясом и картофелем этого примера и проанализируем этот файл CSV. Файл index.js требует некоторого обновления для подключения к MongoDB и создания нового маршрута POST, который будет принимать загрузку файла:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
var app = require(‘express’)();
var fileUpload = require(‘express-fileupload’);
var mongoose = require(‘mongoose’);
 
var server = require(‘http’).Server(app);
 
app.use(fileUpload());
 
server.listen(80);
 
mongoose.connect(‘mongodb://localhost/csvimport’);
 
app.get(‘/’, function (req, res) {
  res.sendFile(__dirname + ‘/index.html’);
});
 
var template = require(‘./template.js’);
app.get(‘/template’, template.get);
 
var upload = require(‘./upload.js’);
app.post(‘/’, upload.post);

После подключения к базе данных и нового маршрута POST настало время проанализировать файл CSV. К счастью, есть несколько замечательных библиотек, которые помогают с этой работой. Я решил использовать пакет fast-csv , который можно установить с помощью следующей команды:

1
npm install fast-csv —save

Маршрут POST был создан аналогично маршруту шаблона, который вызывает функцию post из файла upload.js . Нет необходимости размещать эти функции в отдельных файлах; Тем не менее, мне нравится создавать отдельные файлы для этих маршрутов, так как это помогает поддерживать красивый и организованный код

И наконец, давайте создадим файл upload.js который содержит функцию post которая вызывается при upload.js ранее созданной формы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var csv = require(‘fast-csv’);
var mongoose = require(‘mongoose’);
var Author = require(‘./author’);
 
exports.post = function (req, res) {
    if (!req.files)
        return res.status(400).send(‘No files were uploaded.’);
     
    var authorFile = req.files.file;
 
    var authors = [];
         
    csv
     .fromString(authorFile.data.toString(), {
         headers: true,
         ignoreEmpty: true
     })
     .on(«data», function(data){
         data[‘_id’] = new mongoose.Types.ObjectId();
          
         authors.push(data);
     })
     .on(«end», function(){
         Author.create(authors, function(err, documents) {
            if (err) throw err;
         });
          
         res.send(authors.length + ‘ authors have been successfully uploaded.’);
     });
};

Совсем немного происходит в этом файле. Первые три строки содержат необходимые пакеты, которые потребуются для анализа и сохранения данных CSV.

Затем функция post определяется и экспортируется для использования файлом index.js . Внутри этой функции происходит волшебство.

Функция сначала проверяет, есть ли файл, содержащийся в теле запроса. Если нет, возвращается ошибка, указывающая, что файл должен быть загружен.

Когда файл загружен, ссылка на файл сохраняется в переменной с именем authorFile . Это делается путем доступа к массиву files и свойству file в массиве. Свойство file совпадает с именем моего входного имени файла, которое я впервые определил в примере index.html .

Я также создал массив authors который будет заполняться при разборе файла CSV. Этот массив будет использоваться для сохранения данных в базе данных.

Библиотека fast-csv теперь fromString функции fromString . Эта функция принимает файл CSV в виде строки. Я извлек строку из свойства authorFile.data . Свойство data содержит содержимое моего загруженного файла CSV.

Я включил два параметра в функцию fast-csv : headers и ignoreEmpty . Они оба установлены в true . Это говорит библиотеке, что первая строка CSV-файла будет содержать заголовки и что пустые строки следует игнорировать.

С настроенными параметрами я настроил две функции прослушивателя, которые вызываются, когда происходит событие data событие end . Событие data вызывается один раз для каждой строки файла CSV. Это событие содержит объект JavaScript проанализированных данных.

Я обновляю этот объект, чтобы включить свойство ObjectId автора с новым ObjectId . Этот объект затем добавляется в массив authors .

Когда файл CSV был полностью проанализирован, запускается событие end . Внутри функции обратного вызова события я вызываю функцию create для модели Author, передавая ей массив authors .

Если при попытке сохранить массив возникает ошибка, генерируется исключение; в противном случае пользователю отображается сообщение об успехе, указывающее, сколько авторов было загружено и сохранено в базе данных.

Если вы хотите увидеть полный исходный код, я создал GitHub-репозиторий с кодом.

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

Это можно сделать несколькими способами. Если бы я это реализовал, я бы предложил обновить функцию обратного вызова data чтобы проверить длину массива авторов. Когда массив превышает заданную вами длину, например, 100, вызовите функцию Author.create для массива, а затем сбросьте массив на пустой. После этого записи будут сохраняться кусками по 100. Обязательно оставьте последний вызов create в функции обратного вызова end для сохранения окончательных записей.

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