Seedery w node.js

Czym tak w ogóle są seedery?

Tworząc aplikację bardzo często zachodzi potrzeba pracy na danych. Oczywiście nic nie stoi na przeszkodzie, abyśmy uruchomili pokłady swojej wyobraźni i wpisywali do bazy danych testowe rekordy. Niemniej jednak jest to mało produktywne zajęcie. Nie wspominam nawet o tym, że takich rekordów często potrzeba dziesiątki, setki a nawet i czasem tysiące.

W tem miejscu z pomoca przychodzą nam seedery, czyli specjalne funkcje pozwalające nam na generowanie testowych rekordów.

Po raz pierwszy z seederami zetknąłem się podczas pracy z Laravelem. Tam cały proces generowania oraz uruchamiania seedera jest mocno zautomatyzowany. Do utworzenia pliku oraz samego seedowania mamy gotowe polecenia i w praktyce jedyne co musimy podać to schemat danych, na którego podstawie zostaną one wygenerowane.

A jak jest w node?

Moje próby znalezienia podobnego rozwiązania w node.js spełzły na niczym. Postanowiłem więc napisać własne rozwiązanie i przedstawić je w tym artykule, tak aby jednocześnie pochwalić się tym, co stworzyłem, ale także poddać to ocenie i weryfikacji.

Do dobrego przyswojenia treści zawartych w tym wpisie zalecana jest znajomość działania modeli, biblioteki mongoose, oraz bardzo podstawowej znajomości MongoDB i node.js.

Kod przedstawiony w tym wpisie wykorzystuje składnię ES6, domyślnie niewspieraną przez node.js. Do poprawnego uruchomienia kodu potrzebny będzie Babel.

Krok po kroku

Pierwszym krokiem będzie stworzenie modelu. Na jego podstawie będziemy tworzyli testowe dane i umieszczali je w bazie danych.

Drugim krokiem jest stworzenie wcześniej omawianego seedera. Na początku importujemy bibliotekę mongoose oraz wcześniej stworzony model. Następnie musimy wykonać połączenie z bazą danych. Sam seeder w naszym przypadku nie jest integralną częścią aplikacji, dlatego też musimy wykonać osobne połączenie.

Następnie tworzymy obiekt będący reprezentacją naszych danych testowych.

Kolejnym krokiem jest użycie na naszym obiekcie z danymi metody save oraz zamknięcie połączenia z bazą danych.

Tak oto rysuje sie nasz bardzo podstawowy seeder:

import mongoose from 'mongoose';
import Message from '../models/Message';

mongoose.connect('mongodb://localhost:27017/FindMates', { useNewUrlParser: true });

const message = new Message({
    message: 'Hello World!'
});

message.save()
    .then(() => mongoose.disconnect());

Oczywiście jest to rozwiązanie działające i jak najbardziej poprawne jednakże można dodać w nim kilka ułatwiających pracę usprawnień.

Seedery - ziarna 2

Optymalizacja

Przede wszystkim warto dodać funkcję, która przed seedowaniem wyczyści nam kolekcję z pozostałości po poprzednim seedowaniu.

Kolejnym bardzo ułatwiającym życie elementem jest skorzystanie z tzw. fakera. Faker to biblioteka służąca do generowania losowych, fałszywych danych. Za pomocą tego rozszerzenia możemy wygenerować takie dane jak imiona, nazwiska, numery telefonu, adresy email i wiele więcej. Dzięki tej bibliotece możemy generować w praktyce unikalne rekordy. Aby skorzystać z fakera musimy go najpierw oczywiście dodać do naszego projektu i zaimportować w pliku seedera. Całą, obszerną listę możliwych do wygenerowania danych oraz sposób użycia samej biblioteki znajdziesz na końcu artykułu w źródłach.

Oprócz tego po wykonaniu seedowania powinniśmy otrzymać komunikat o powodzeniu lub o błędzie.

Dodajmy do naszego seedera wcześniej omówione usprawnienia:

import faker from 'faker';
import mongoose from 'mongoose';
import Message from '../models/Message';

mongoose.connect('mongodb://localhost:27017/FindMates', { useNewUrlParser: true });
Message.deleteMany({}, err=> console.log(err));

const message = new Message({
    message: faker.lorem.sentence()
});
message.save()
    .then(() => mongoose.disconnect())
    .then(() => console.log('Message seed succesfully'))
    .catch(err => console.log(err));

Skalowalność

Ze skalowaniem ilości seedowanych danych nie ma żadnego problemu. Wystarczy stworzyć pętlę, która wykona się żądaną ilość razy.

Schody zaczynają się w momencie tworzenia kolejnych seederów. Nie ma problemu w ręcznym wykonaniu polecenia dla jednego bądź kilku seederów. Problem zaczyna się pojawiać w momencie, w którym tych seederów pojawi się znaczna ilość. Warto byłoby zoptymalizować również i ten proces.

Do tego zadania posłuży nam nowy plik, w którym zamieścimy skrypt odpowiadający za uruchomienie wszystkich plików w zadanym katalogu. Przyznam się bez bicia,jest to gotowy skrypt znaleziony na Stack Overflow, niemniej jednak na moje potrzeby jest on idealny.

import fs from 'fs';
import async from 'async';
const exec = require('child_process').exec

const scriptsFolder = './src/seed/';

const files = fs.readdirSync(scriptsFolder);
const funcs = files.map(function(file) {
    return exec.bind(null, `babel-node ${scriptsFolder}${file}`);
});

function getResults(err, data) {
    if (err) {
        return console.log(err);
    }
    const results = data.map(function(lines){
        return lines.join('');
    });
    console.log(results);
}

async.parallel(funcs, getResults);

Teraz jedyne co nam pozostaje to stworzyć odpowiednie polecenie w pliku package.json, które uruchomi nasz plik startujący nasze seedery. Jak wcześniej pisałem do transpilowania kodu wykorzystuję Babel, więc w moim przypadku polecenie będzie wyglądało następująco: babel-node src/seedRunner.js.

Po krótkiej chwili, jeżeli wszystko zrobiliśmy jak należy, w naszej bazie danych powinny widnieć nowe rekordy.

Podsumowanie

Rozwiązanie przedstawione w tym wpisie nie jest w 100% idealne. Występuje chociaż problem z referencja do zasobów. W repozytorium, do którego link znajdziesz w źródłach zobaczysz, że istnieje problem z referencją do zasobów (której de facto w naszym seederze nie ma – jest to losowe ID użytkownika, którego nawet nie ma w bazie danych). Jest to problem, nad którym muszę się bardziej pochylić, ale zachęcam też do sprawdzenia się samemu z tym problemem.

Serdecznie zachęcam do pozostawienia opinii, pytań oraz propozycji na kolejne wpisy w komentarzu.

Źródła i materiały dodatkowe:

Repozytorium z kodem z przykładów:
https://github.com/elszczepano/FindMates-API/
https://mongoosejs.com/docs/models.html
https://stackoverflow.com/questions/50558532/how-to-run-all-javascript-files-in-a-folder-with-nodejs
https://www.youtube.com/watch?v=V30Rpqi6kYE
https://github.com/marak/Faker.js/