Направете своите GraphQL заявки по-ефективни, като ги групирате

Ако изграждате API, захранван от GraphQL, в Node, не можете да го направите без DataLoader. Тази библиотека ви позволява да групирате вашите заявки и да поддържате своя API също толкова ефективен, колкото неговите предшественици REST. В тази статия просто ще разгледаме необработената механика на библиотеката, без да се затъваме твърде много в GraphQL.

Какво представляват DataLoaders?

Просто казано, DataLoaders решават „N+1 проблема“ на GraphQL. Ако не знаете какво е това, прочетете това бързо обяснение. Предполагам, че разбирате какво е това от този момент нататък, но за да обобщим, той гласи:

За всяка 1 заявка към база данни, която връща N резултата, ще трябва да направите N допълнителни заявки

Толкова много заявки е неефективно. Би било по-добре да групираме тези N заявки в 1, така че вместо N+1 винаги да имаме само 2. Тук идват DataLoaders.

DataLoaders: Общ преглед

На най-високо ниво DataLoader:

  1. Събира масив от ключове по време на един тик на цикъла на събития
  2. Посещава базата данни веднъж с всички тези ключове
  3. Връща обещание, което разрешава масив от стойности

Всичко, от което се нуждаете, за да направите DataLoader, е функция за пакетиране, която приема масив от ключове и разрешава масив от стойности. И двата масива трябва да са с еднаква дължина, в противен случай ще се счупи, когато се опита да ги превърне в хранилище за ключ/стойност. Нека работим със следния DataLoader и фалшива DB:

const DataLoader = require('dataloader');
const fakeDB = ['Tom', 'Bo', 'Kate', 'Sara', 'Gene', 'Noel'];
const batchGetUserById = async (ids) => {
  console.log(‘called once per tick:’, ids);
  return ids.map(id => fakeDB[id — 1]);
};
const userLoader = new DataLoader(batchGetUserById);

Имаме „асинхронна функция“, която обикаля даден масив от идентификатори и след това връща масив от потребителски имена. Това е подигравка как една истинска програма би await резултат от DB, използвайки ORM. Не забравяйте, че тъй като е async, върнатата стойност на нашата функция е автоматично обвита в Обещание. След това вземаме пакетната функция и я предаваме в нов DataLoader.

Използване на DataLoader с .load()

За да използвате действително DataLoader, вие не извиквате партидната функция. Вместо това използвате метода .load() . Ето как вашият DataLoader може да събере всички необходими ключове. Всеки .load() запазва ключа и връща обещание. При тиктака на цикъла на събитието той взема всички ключове и след товаги предава на пакетната функция. Пакетната функция разрешава своите стойности, които след това се съхраняват със съответните ключове. И накрая, обещанието на всяко .load() се разрешава до стойността на дадения ключ.

Накратко за цикъла на събитията

Продължавам да споменавам тикове, така че трябва да говорим за цикъла на събитията. Ако имате 20 минути, гледайте това видео от Филип Робъртс . Ако не, достатъчно е да се каже, че всяко отметка на цикъла на събитието е това, което хвърля следващото разрешено асинхронно обратно извикване в главния Call Stack.

Всичко, коетонаистина трябва да знаете, е, че DataLoader използва този процес на тиктакане на цикъл на събития като начин за маркиране кога да се задейства пакетната функция. Той прави това, защото до този момент всички .load() методи ще бъдат задействани за дадена заявка, което означава, че знае точно колко ключа да провери спрямо DB.

Обратно към DataLoaders

Добре, сега вижте този примерен код:

const DataLoader = require('dataloader');
const fakeDB = ['Tom', 'Bo', 'Kate', 'Sara', 'Gene', 'Noel'];
const batchGetUserById = async (ids) => {
  console.log('called once per tick:', ids);
  return ids.map(id => fakeDB[id - 1]);
};
const userLoader = new DataLoader(batchGetUserById);
console.log('\nEvent Loop Tick 1');
userLoader.load(1);
userLoader.load(2).then((user) => {
  console.log('Here is the user: ', user);
});
setTimeout(() => {
  console.log('\nTick 2');
  userLoader.load(3);
  userLoader.load(4);
}, 1000);
setTimeout(() => {
  console.log('\nTick 3');
  userLoader.load(5);
  userLoader.load(6);
}, 2000);

Ако стартирате това, ще получите:

Event Loop Tick 1
called once per tick: [ 1, 2 ]
Here is the user:  Bo
Tick 2
called once per tick: [ 3, 4 ]
Tick 3
called once per tick: [ 5, 6 ]

Както можете да видите, нашата пакетна функция наистина се извиква само веднъж на тик. Не забравяйте, че setTimeout е асинхронен, така че всеки път ни хвърля в следващото отметка.

Също така забележете, че въпреки че пакетните функции се извикват с масиви, можете да видите, че .load(2) преобразува в правилния потребител на Bo и нищо друго. Всеки .load() метод винаги ще връща единствената стойност. Вероятно ще бъде един масив в реалния свят, така че нека опитаме за по-реалистичен пример:

Направете Pseudo GraphQL резолвер

Вземете тази GraphQL заявка:

query {
 authors 
  books {
   title
  }
 }
}  

Нашият зареждащ инструмент ще бъде в полето books в този пример и ще намери всички книги, свързани с author_id. Нека използваме for цикъл, за да симулираме 3 различни родителски обекта на автор с идентификатори 1, 2 и 3:

const DataLoader = require('dataloader');
const fakeBooksDB = [
  { title: 'book 1', author_id: 1 },
  { title: 'book 2', author_id: 2 }, 
  { title: 'book 3', author_id: 3 }, 
  { title: 'book 4', author_id: 3 },
];
const batchGetBooksById = async (ids) => {
  const books = ids.map((authorId) => {
    return fakeBooksDB
      .filter(book => book.author_id === authorId);
  });
  console.log('I only get fired once');
  return books;
};
const bookLoader = new DataLoader(batchGetBooksById);
// loop simulates 3 author parent resolvers,
for (let i = 1; i <= 3; i++) {
  bookLoader.load(i).then((res) => {
    console.log(`\nAuthor #${i} books:`);
    console.log(res);
  });
}

Ако стартирате това, ще получите нещо като:

I only get fired once
Author #1 books:
[ { title: 'book 1', author_id: 1 } ]
Author #2 books:
[ { title: 'book 2', author_id: 2 } ]
Author #3 books:
[ { title: 'book 3', author_id: 3 },
  { title: 'book 4', author_id: 3 } ]

Тъй като всичко това се случи в един и същ тик на цикъл на събитие, нашата пакетна функция се задейства само веднъж. И отново, всеки .load() разреши една стойност, масив с една или две книги.

Кеширане

Въпреки че групирането е основният фокус, DataLoaders също имат кеширане. Това означава, че ако някога извикате .load() със същия ключ два пъти, той ще потърси стойността в хранилището за ключ/стойност на DataLoader, без отново да задейства пакетната функция:

const DataLoader = require('dataloader');
const fakeDB = ['Tom', 'Bo', 'Kate', 'Sara'];
const batchGetUserById = async (ids) => {
  console.log('I ran!');
  return ids.map(id => fakeDB[id - 1]);
};
const userLoader = new DataLoader(batchGetUserById);
console.log('\nEvent Tick 1');
userLoader.load(1);
userLoader.load(2);
setTimeout(() => {
  console.log('\nEvent Tick 2'); 
  userLoader.load(2).then((res) => {
    console.log('cached res: ', res);
  });
}, 1000);

което извежда:

Event Tick 1
I ran!
Event Tick 2
cached res:  Bo

Пакетната функция не беше стартирана, тя просто потърси стойността.

Методът .loadMany().

Понякога на искате да имате достъп до повече от един ключ наведнъж. В тези случаи използвайте метода .loadMany():

userLoader.loadMany([3, 4]).then((res) => {
  console.log('Returns an array of values: ', res);
});

Ако включите това към пример по-горе, ще получите Returns an array of values: [ ‘Kate’, ‘Sara’ ]. Чисто нали?

Заредете!

Ето го, основният DataLoader. За повече информация вижте тази „страхотна статия“. Той обхваща тези теми с по-конкретни примери. Надявам се, че това помогна и ако имате въпроси, не се колебайте да ги зададете по-долу.

приятно кодиране на всички,

Майк

последната статия:Как да изградим динамична, контролирана форма с React Hooks