- PVSM.RU - https://www.pvsm.ru -

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

При программировании на JavaScript часто возникает проблема выбора оптимального представления данных в программе: массивы, хеши, массивы хешей, хеши массивов и т.д. Одни и те же данные могут быть загружены в различные комбинации структур, но трудность выбора обычно заключается в том, как найти компромисс между простотой кода для доступа к этим данным, скоростью работы и количеством требуемой памяти.
Strelki.js — еще одна библиотека для работы с массивами - 1
В статье рассказано о моей попытке поиска универсального решения.

Пусть нам, например, необходимо отобразить некоторые данные из двух связанных таблиц:
Strelki.js — еще одна библиотека для работы с массивами - 2

Стандартный подход обычно состоит из следующих шагов:

  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"
	});

Посмотрим, что внутри:
Strelki.js — еще одна библиотека для работы с массивами - 3
Под капотом IndexedArray представляет из себя хеш (this.data), куда сохраняются ссылки на объекты. В качестве ключа хеша используется поле «id» сохраняемого элемента, которое должно быть уникально. Так как многие современные серверные фреймворки также используют поле «id» подобным образом, то это ограничение не должно стать проблемой.

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

Добавим индекс:

emp.createIndex("dep_id");

Посмотрим this.indexData:
Strelki.js — еще одна библиотека для работы с массивами - 4

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 [1]
Песочница: www.izhforum.info/strelkijs [2]

Автор: amaksr

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/121139

Ссылки в тексте:

[1] github.com/amaksr/Strelki.js: https://github.com/amaksr/Strelki.js

[2] www.izhforum.info/strelkijs: https://www.izhforum.info/strelkijs/

[3] Источник: https://habrahabr.ru/post/301258/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best