Strelki.js — ще одна бібліотека для роботи з масивами

При програмуванні на JavaScript часто виникає проблема вибору оптимального представлення даних в програмі: масиви, хеши, масиви хешів, хеши масивів і т. д. Одні і ті ж дані можуть бути завантажені в різні комбінації структур, але труднощі вибору зазвичай полягає в тому, як знайти компроміс між простотою коду для доступу до цих даних, швидкістю роботи і кількістю необхідної пам'яті.

У статті розповідається про моїй спробі пошуку універсального рішення.


Нехай нам, наприклад, треба відобразити деякі дані з двох таблиць:


Стандартний підхід зазвичай складається з наступних кроків:
  1. На сервері написати SQL-запит з JOIN-ом
  2. На сервері додати для нього функцію, яка повертає масив об'єктів, і зробити її доступною через routes
  3. На клієнті додати AJAX-виклик до сервера, і обробку отриманого результату в таблицю


Недоліки стандартного підходу мені бачаться в наступному:
  1. SQL-запит і функція-обгортка повинні враховувати можливі колізії імен колонок, тобто не можна просто зробити "SELECT *".
  2. Відповідь сервера буде містити велику кількість повторюваних записів зв'язаних таблиць. У нашому прикладі запис з ключем «sales» з таблиці departments буде передана два рази.
  3. При зв'язку великої кількості таблиць чи ми отримаємо довгі ключі, що призведе до збільшення марного витрати пам'яті і трафіку по передачі цих ключів, або імена колонок в SQL-запиті необхідно перераховувати вручну, що призведе до додаткових витрат при внесенні змін у структуру БД.
  4. Кількість функцій API для отримання даних з пов'язаних таблиць може значно перевищити кількість таблиць, що веде до роздування коду, і, як наслідок, витратам.


Нестандартний підхід — отримати таблиці окремо і пов'язати їх на клієнті. Іноді це можна зробити легко. Наприклад, у наведеній вище структурі можна завантажити таблицю «departments» в хеш, і здійснювати доступ з «id». Але найчастіше цього зробити не можна, і доводиться користуватися різними функціями пошуку типу Array.find або Array.indexOf.

Працюючи над своїм черговим проектом, і в черговий раз ламаючи голову над тим, як мені краще організувати інформацію, щоб уникнути недоліків обох підходів, я вирішив, що настав час розрубати цей вузол один раз і назавжди.

Підхід, при якому сервер видає нам нормалізовані таблиці, а ми потім їх пов'язуємо у JavaScript коді, здався мені більш привабливим. Не вистачало тільки інструменту, щоб їх легко зв'язувати. Я відклав усі справи і сів його писати.

Вимоги вийшли наступні:
  • Інструмент повинен дозволяти мені створювати довільні індекси на масив, і підтримувати їх в актуальному стані.
  • Інструмент повинен вміти шукати в масиві з індексами, по можливості без повільних операцій перебору елементів.
  • Интрумент повинен вміти поєднувати масиви індексованих полів у відповідності з певним об'єктом-декларацією, щось типу оператора JOIN в SQL, але без парсинга запитів і всієї потужності, пропонованої SQL.


Так з'явилися Strelki.js і поки єдиний в ньому клас — IndexedArray.

Отже, створимо новий IndexedArray:

var emp = new StrelkiJS.IndexedArray();


Додамо в нього дані:

emp.put({
id: "001", 
first_name: "John", 
last_name: "Smith", 
dep_id: "sales", 
address_id: "200"
});
emp.put({
id: "002", 
first_name: "Ivan", 
last_name: "Krasonov", 
dep_id: "sales", 
address_id: "300"
});


Подивимося, що всередині:

Під капотом IndexedArray представляє з себе хеш (this.data), де зберігаються посилання на об'єкти. В якості хеш ключа використовується поле «id» зберігається елемента, яке має бути унікальним. Так як багато сучасні серверні фреймворки також використовують поле «id» подібним чином, то це обмеження не повинно стати проблемою.

Крім того, в IndexedArray є хеш this.indexData. Ключі цього хеш містять назву індексованого поля, а значення — хеші з ids відповідних елементів основного хеш. Поки індексів у нас немає, тому this.indexData порожній.

Додамо індекс:
emp.createIndex("dep_id");


Подивимося this.indexData:


this.indexData тепер містить ключ «dep_id», який містить дані індексу у вигляді вкладених хешів.

Пошукаємо щось за індексом:
> emp.findIdsByIndex("dep_id","sales")
< ["001", "002"]


На відміну від функцій типу Array.find, індексний пошук не використовує перебір даних, а тільки хеші, що дозволяє добитися високої швидкості. Заміри, правда, я поки не робив, але має працювати швидко.

Додамо ще даних:
emp.put({
id: "003", 
first_name: "George", 
last_name: "Clooney", 
dep_id: "hr", 
address_id: "400"
});
emp.put({
id: "004", 
first_name: "Dev", 
last_name: "Patel", 
dep_id: "board", 
address_id: "500"
});


Знайдемо елементи за індексом, і сформуємо з них новий IndexedArray:
var sales_emp = emp.where("dep_id","sales");


Створимо і заповнимо ще один IndexedArray:
var adr = new StrelkiJS.IndexedArray();
adr.put({ id: "200", address: "New Orleans, Bourbon street, 100"});
adr.put({ id: "300", address: "Moscow, Rojdestvensko-Krasnopresnenskaya Naberejnaya"});
adr.put({ id: "500", address: "Bollywood, India"});


Зв'язування масивів

Для опису зв'язку даного IndexedArray з будь-яким іншим служить об'єкт наступного виду:
{
from_col: "address_id", // поле в даному IndexedArray
to_table: adr, // посилання на связываемую таблицю
to_col: "id", // "id", або інше індексоване поле в зв'язаної таблиці
type: "outer", // "outer" LEFT OUTER JOIN, або null для INNER JOIN
join: // null або посилання на масив точно таких же об'єктів опису зв'язку, для побудови вкладених JOIN-ів
}


Приєднаємо adr до emp JOIN-ом:
var res = emp.query([
{
from_col: "address_id", // name of the column in "emp" table
to_table: adr, // reference to another table
to_col: "id", // "id", or other indexed field in "adr" table
type: "outer", // "outer" for LEFT OUTER JOIN or null for INNER JOIN
//join: [ // optional recursive nested joins of the same structure
// {
// from_col: ...,
// to_table: ...,
// to_col: ...,
// ...
// },
// ...
//],
}
])


Аналогічний оператор SQL виглядав би так:
SELECT ...
FROM emp
LEFT OUTER JOIN adr ON emp.address_id = adr.id


Результат буде виглядати так:
[
[
{"id":"001","first_name":"John","last_name":"Smith","dep_id":"sales","address_id":"200"},
{"id":"200","address":"New Orleans, Bourbon street, 100"}
],
[
{"id":"002","first_name":"Ivan","last_name":"Krasonov","dep_id":"sales","address_id":"300"},
{"id":"300","address":"Moscow, Rojdestvensko-Krasnopresnenskaya Naberejnaya"}
],
[
{"id":"003","first_name":"George","last_name":"Clooney","dep_id":"hr","address_id":"400"},
null
],
[
{"id":"004","first_name":"Dev","last_name":"Patel","dep_id":"board","address_id":"500"},
{"id":"500","address":"Bollywood, India"}
]
]


Більш складний приклад зв'язування 3-х таблиць

var dep = new StrelkiJS.IndexedArray();
dep.createIndex("address");
dep.put({id:"sales", name: "Sales", address_id: "100"});
dep.put({id:"it", name: "IT", address_id: "100"});
dep.put({id:"hr", name: "Human resource", address_id: "100"});
dep.put({id:"ops", name: "Operations", address_id: "100"});
dep.put({id:"warehouse", name: "Warehouse", address_id: "500"});

var emp = new StrelkiJS.IndexedArray();
emp.createIndex("dep_id");
emp.put({id:"001", first_name: "john", last_name: "smith", dep_id: "sales", address_id: "200"});
emp.put({id:"002", first_name: "Tiger", last_name: "Woods", dep_id: "sales", address_id: "300"});
emp.put({id:"003", first_name: "George", last_name: "Bush", dep_id: "sales", address_id: "400"});
emp.put({id:"004", first_name: "Vlad", last_name: "Putin", dep_id: "ops", address_id: "400"});
emp.put({id:"005", first_name: "Donald", last_name: "Trump", dep_id: "ops", address_id: "600"});

var userRoles = new StrelkiJS.IndexedArray();
userRoles.createIndex("emp_id");
userRoles.put({id:"601", emp_id: "001", role_id: "worker"});
userRoles.put({id:"602", emp_id: "001", role_id: "picker"});
userRoles.put({id:"603", emp_id: "001", role_id: "cashier"});
userRoles.put({id:"604", emp_id: "002", role_id: "cashier"});

var joinInfo = [
{
from_col: "id",
to_table: emp,
to_col: "dep_id",
type: "outer",
join: [{
from_col: "id",
to_table: userRoles,
to_col: "emp_id",
type: "outer",
}],
},
// {
// from_col: "id",
// to_table: assets,
// to_col: "dep_id",
// }
];
//var js1 = IndexedArray.IndexedArray.doLookups(dep.get("sales"),joinInfo);

var js = dep.where(null,null,function(el) { return el.id === "sales"}).query(joinInfo);

// result

[
[
{"id":"sales","name":"Sales","address_id":"100"},
{"id":"001","first_name":"john","last_name":"smith","dep_id":"sales","address_id":"200"},
{"id":"601","emp_id":"001","role_id":"worker"}
],
[
{"id":"sales","name":"Sales","address_id":"100"},
{"id":"001","first_name":"john","last_name":"smith","dep_id":"sales","address_id":"200"},
{"id":"602","emp_id":"001","role_id":"picker"}
],
[
{"id":"sales","name":"Sales","address_id":"100"},
{"id":"001","first_name":"john","last_name":"smith","dep_id":"sales","address_id":"200"},
{"id":"603","emp_id":"001","role_id":"cashier"}
],
[
{"id":"sales","name":"Sales","address_id":"100"},
{"id":"002","first_name":"Tiger","last_name":"Woods","dep_id":"sales","address_id":"300"},
{"id":"604","emp_id":"002","role_id":"cashier"}
],
[
{"id":"sales","name":"Sales","address_id":"100"},
{"id":"003","first_name":"George","last_name":"Bush","dep_id":"sales","address_id":"400"}
,null
]
]



Як бачимо, зв'язування масивів перетворилося з функцій з циклами і переборами в просту декларацію зв'язків.

Обмеження

IndexedArray не зберігає копії об'єктів, а лише покажчики на них (звідси і назва Strelki). Тому, якщо об'єкт був поміщений в IndexedArray методом put(), а потім змінений, інформація в індексах може стати некоректною. Щоб уникнути цієї ситуації необхідно видалити об'єкт з IndexedArray методом del() перед зміною.

Зв'язування може здійснюватися тільки за індексованого поля, або по полю «id».

Деякі методи об'єкта IndexedArray (наприклад length()) вимагають побудови масиву ключів «id». При цьому масив ключів зберігається в об'єкті для можливого повторного використання. При кожній зміні масиву (методи put(), del(), і т. п.) масив ключів обнуляється. Тому чергування методів, які створюють і потім обнуляють масив ключів, може призвести проблем продуктивності на великих наборах даних.

Плани
StrelkiJS створений для полегшення написання основного проекту KidsTrack, про який я писав на хабре раніше. Тому всі рішення про новий функціонал поки що диктуються потребами батьківського проекту. У найближчих планах — зробити більш зручний доступ до колонок в результатах JOIN-а,

Де завантажити
Github: github.com/amaksr/Strelki.js
Пісочниця: www.izhforum.info/strelkijs
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.