Окончание статьи, описывающей использование индексированного хранилища в Internet Explorer и других современных Web-обозревателях.
4. Автоматически задаваемые первичные ключи
Мы уже знаем, как сохранить значение под заданным нами самими первичным ключом. А можно ли сделать так, чтобы первичные ключи для значений брались из самого этого значения (это будет актуальным, если значение представляет собой экземпляр объекта) или генерировались автоматически? (Такие первичные ключи, кстати, называются автоматически задаваемыми.) Разумеется, можно!
4.1. Использование в качестве первичных ключей данных, являющихся частью сохраняемого значения. Пути ключей
Если мы сохраняем в объектном хранилище экземпляры каких-либо объектов, мы можем использовать в качестве первичных ключей значения из любого свойства этих объектов. Для этого достаточно при создании хранилища указать путь ключа - фактически, имя свойства, из котором он будет браться.
Метод createObjectStore объекта IDBDatabase может принимать второй параметр - экземпляр объекта Object, задающий параметры создаваемого объектного хранилища. В составе этих параметров и указывается путь ключа. Он задаётся свойством keyPath этого экземпляра объекта и должен представлять собой строку.
Пусть нам нужно записать в хранилище следующие экземпляры объектов:
var obj1 = {name: "Вася Пупкин", email: "vasya@mail.ru"}
var obj2 = {name: "Петя Васькин", email: "petya@inbox.ru"}
.ru"}[/code]
В качестве первичного ключа мы можем использовать значение свойства name этих экземпляров объектов. И используем для создания хранилища вот такой код:
Теперь значение первичного ключа будет браться из свойства name. И при записи значений в хранилище нам уже не понадобится указывать первичный ключ самим во втором параметре метода add:
[code]var tran = db.transaction("addresses");
var os = tran.objectStore("addresses");
var req = os.add(obj1);
req = os.add(obj2);[/code]
Разумеется, мы и в этом случае можем получить нужное значение по его первичному ключу:
[code]var tran = db.transaction("addresses");
var os = tran.objectStore("addresses");
var req = os.get("Вася Пупкин");
req.onsuccess = function (evt) {
window.alert(evt.target.result.email);
}[/code]
4.2. Использование генераторов ключей. Счётчики
А ещё мы можем указать Web-обозревателю, чтобы он генерировал первичные ключи сам. На данный момент поддерживается лишь одна разновидность генерируемых первичных ключей - счётчик, который при каждом обращении выдаёт целое число, на единицу большее, чем выданное при предыдущем обращении. Такой счётчик аналогичен полям автоинкрементного типа, часто используемым в записях баз данных.
Чтобы создать счётчик, следует в экземпляре объекта Object, передаваемом вторым параметром методу createObjectStore объекта IDBDatabase, объявить свойство autoIncrement и присвоить ему значение true:
Мы также можем указать путь ключа. В этом случае сгенерированный первичный ключ будет сохранится в свойстве, заданном этим путём:
[code]var obj1 = {id: 0, name: "Вася Пупкин", email: "vasya@mail.ru"};
var obj2 = {id: 0, name: "Петя Васькин", email: "petya@inbox.ru"};
. . .
db.createObjectStore("addresses", {keyPath: "id", autoIncrement: true});
. . .
var tran = db.transaction("addresses");
var os = tran.objectStore("addresses");
var req = os.add(obj1);
req = os.add(obj2);[/code]
Теперь сгенерированные первичные ключи будут записываться в свойство id сохраняемых нами значений.
5. Расширенные средства поиска
Как получить значение по его первичному ключу, мы уже знаем. Однако не всегда мы можем знать этот ключ. Так, если созданное нами объектное хранилище использует генератор ключей, мы никак не сможем выяснить, какой первичный ключ соответствует нужному нам значению.
Что же делать в таком случае? Можно либо выполнить прямой перебор всех находящихся в хранилище значений, чтобы найти нужное, либо произвести поиск по индексу. Сейчас мы узнаем, как это делается.
5.1. Прямой перебор значений. Курсоры
Проще всего выполнить прямой перебор значений. Для этого применяются курсоры - особые сущности, позволяющие получить доступ к текущему значению и выполнить после этого переход на следующее.
5.1.1. Использование курсоров
Первым нашим действие будет открытие курсора. Для этого используется метод openCursor объекта IDBObjectStore:
Первым, необязательным, параметром передаётся диапазон ключей, для которого будет создан курсор. (О диапазонах значений мы поговорим позже.) Если он не указан или равен null, курсор будет создан на основе всех первичных ключей, что есть в хранилище.
Вторым, необязательным, параметром задаётся направление перебора значений. (Разговор об этом также пойдёт потом.) Если оно не указано, значения будут перебираться в прямом порядке - в том, в котором отсортированы значения.
Внимание! Значения в курсоре, открытом непосредственно из объектного хранилища, будут отсортированы по величинам их первичных ключей.
Метод openCursor возвращает запрос. Вся работа по перебору значений будет выполняться в обработчике события success этого запроса.
Сначала мы получим из свойства result запроса сам курсор, представляемый экземпляром объекта IDBCursorWithValue. Далее мы проверим, существует ли этот курсор, то есть содержит ли хранилище хоть одно значение. И после этого сможем получить текущее значение и его ключ из следующих свойств курсора:
key - первичный ключ значения;
value - само значение.
Теперь нам нужно выполнить переход на следующее значение. Для этого мы вызовем не принимающий параметров и не возвращающий результата метод continue объекта IDBCursorWithValue. После чего выполнение обработчика события success запроса завершится, и запустится операция по переходу на следующее значение. А когда этот переход будет выполнен, снова возникнет событие success запроса, и снова выполнится обработчик события success, в котором мы получим доступ к следующему значению.
Если свойство result запроса хранит null, это является признаком того, что значений в курсоре больше нет, то есть мы перебрали его полностью.
Давайте для примера переберём все значения, записанные в хранилище addresses, и поместим всех Вась в отдельный массив vals:
[code]var vals = [];
var os = db.transaction("addresses").objectStore("addresses");
os.openCursor().onsuccess = function (evt) {
var cur = evt.target.result;
// Проверяем, не достигнут ли конец курсора и содержит
// ли хранилище значения
if (cur) {
// Это Вася?
if (cur.value.name.indexOf("Вася") > -1) {
// Помещаем очередного Васю в отдельный массив
vals.push(cur.value);
}
// Переходим на следующее значение
cur.continue();
}
}[/code]
5.1.2. Задание диапазонов ключей для курсора
В некоторых случаях удобно включить в состав курсора не все первичные ключи, что есть в хранилище, а лишь часть их. То есть указать диапазон ключей.
Диапазон ключей передаётся первым параметром в метод openCursor объекта IDBObjectStore. Но как его задать? С помощью ряда статичных методов объекта IDBKeyRange, который можно получить из одноимённого свойства объекта Window. Все эти методы возвращают сформированный диапазон в виде экземпляра объекта IDBKeyRange.
Метод bound задаёт для создаваемого диапазона ключей начальную и конечную границы:
[code]window.IDBKeyRange.bound(
<начальная граница>,
<конечная граница>[,
<исключить значение начальной границы>[,
<исключить значение конечной границы>]]
)[/code]
Первые два параметра указывают первичные ключи, которые станут, соответственно, начальной и конечной границами для диапазона.
Если третий параметр равен false или вообще не указан, начальная граница будет включена в диапазон ключей. Если же он равен true, начальная границе не будет в него включена.
Аналогично работает и четвёртый, также необязательный, параметр, задающий поведение для конечной границы диапазона.
Создаём диапазон, содержащий первичные ключи от test1 до test4, и используем его для создания курсора:
Этот курсор включит лишь значения с первичными ключами test2, test3 и test4.
Методы lowerBound и upperBound задают для создаваемого диапазона только, соответственно, начальную и конечную границу; тогда с противоположной стороны диапазон не будет ограничен. Формат вызова этих методов таков:
[code]window.IDBKeyRange.lowerBound | upperBound(
<граница>[,
<исключить значение границы>]
)[/code]
Для примера создадим диапазон, который включит все первичные ключи, начиная с самого первого из имеющихся в хранилище и заканчивая test7:
5.1.3. Изменение порядка перебора значений в курсоре
По умолчанию значения в курсоре перебираются в том порядке, в котором они отсортированы. Однако мы можем изменить этот порядок.
Порядок перебора значений указывается вторым параметром метода openCursor объекта IDBObjectStore. Он указывается в виде строки "next" или "prev"; первая задаёт порядок по умолчанию, а вторая - обратный.
[code]var os = db.transaction("addresses").objectStore("addresses");
os.openCursor(null, "prev").onsuccess = function (evt) { . . . }[/code]
5.2. Использование индексов
В последних примерах мы записывали в хранилище экземпляры объектов с двумя свойствами: name и email. В качестве первичных ключей мы использовали целые числа, сгенерированные счётчиком.
Теперь мы хотим иметь возможность быстро получать сохранённые экземпляры объектов по значениям их свойств, например, по имени (свойству name). Как нам это выполнить? Прямым перебором? Но это очень долгий процесс, особенно если данных в хранилище много.
В качестве выхода можно использовать индексы. Их можно рассматривать как особые массивы. Индекс элемента такого массива представляет собой значение, извлечённое из свойства сохранённого экземпляра объекта, и называется ключом индекса. А сам элемент - это указатель на соответствующий экземпляр объекта.
Тогда для выполнения вышеописанной задачи мы можем создать индекс, указав в качестве его ключа свойство name, и выполнять поиск нужного значения по этому индексу. Ещё мы можем создать на основе этого индекса курсор, значения в котором будут отсортированы по ключу индекса, и произвести перебор значений.
5.2.1. Создание индексов
Внимание! Создать индекс можно только в обработчике события upgradeneeded запроса за создание или открытие базы данных. В любом другом месте кода попытка создания индекса вызовет ошибку.
Для создания индекса нужно вызвать метод createIndex объекта IDBObjectStore:
Первый параметр задаёт имя индекса. Это имя должно быть уникально для объектного хранилища, в котором создаётся индекс. Обычно оно совпадает с путём ключа индекса - это позволяет избежать путаницы.
Второй параметр указывает путь ключа индекса - имя свойства объекта, из которого будет браться этот ключ. Он указывается так же, как путь первичного ключа (см. параграф 4.1).
Третий, необязательный, параметр служит для задания параметров индекса и должен представлять собой экземпляр объекта Object. В нём мы можем указать свойство unique. Если это свойство имеет значение true, то индекс может содержать лишь уникальные значения ключа; попытка добавить в хранилище значение с тем же ключом вызовет ошибку. Если же это свойство равно false или если третий параметр вообще не задан, индекс может содержать дублирующиеся значения ключа.
На заметку В экземпляре объекта Object, передаваемом третьим параметром методу createIndex, мы также можем указать свойство multiEntry. Если его значение равно true, в индексе для каждого значения ключа индекса будет создан всего один элемент, значением которого станет массив с указателями на соответствующие значения. Если же его значение равно false или если данное свойство вообще не задано, в индексе для каждого значения, соответствующей ключу индекса, будет создан отдельный элемент. Автор не смог выяснить, какие преимущества даёт этот параметр; вероятно, впоследствии, в новых версиях Web-обозревателя, он будет использоваться в полную силу.
Метод createIndex возвращает созданный индекс в виде экземпляра объекта IDBIndex.
В качестве примера давайте создадим два индекса: индекс name с путём ключа "name" и индекс email с путём ключа "email"; последний может содержать лишь уникальные значения ключа.
[code]var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("email");[/code]
После чего вызовем метод get объекта IDBIndex, который аналогичен одноимённому методу объекта IDBObjectStore (см. параграф 3.7).
[code]ind.get("test1").onsuccess = function (evt) {
window.alert("Имя: " + evt.target.result.name);
}[/code]
Метод getKey объекта IDBIndex позволяет получить не само значение, а его первичный ключ.
[code]var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("email");
ind.getKey("test1").onsuccess = function (evt) {
window.alert("Первичный ключ найденного значения: " + evt.target.result);
}
[/code]
Однако этот приём пригоден лишь в случае использования индекса, хранящего уникальные значения. Если же индекс позволяет хранить дублирующиеся значения, и в хранилище есть несколько значений с одинаковым ключом индекса, мы получим лишь то значение, что имеет наименьший первичный ключ.
Проиллюстрируем вышесказанное на примере:
[code]var obj1 = {name: "Вася Пупкин", email: "vasya@mail.ru"};
var obj2 = {name: "Петя Васькин", email: "petya@inbox.ru"};
var obj3 = {name: "Вася Пупкин", email: "vasya2@mail.ru"};
. . .
var os = db.transaction("addresses").objectStore("addresses");
var req = os.add(obj1);
req = os.add(obj2);
req = os.add(obj3);
. . .
var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("name");
ind.get("Вася Пупкин").onsuccess = function (evt) {
window.alert(evt.target.result);
}[/code]
В этом случае мы получим лишь самый первый из сохранённых экземпляров объекта - самого первого Васю Пупкина, имеющего адрес электронной почты vasya@mail.ru, поскольку он был добавлен самым первым и, следовательно, имеет наименьший первичный ключ.
5.2.3. Перебор значений с использованием индекса
Объект IDBIndex поддерживает знакомый нам метод openCursor. Так что мы без проблем сможем выполнить перебор значений с применением указанного нами индекса и, соответственно, отсортированных по его ключу. Единственное отличие - свойство key курсора будет содержать не первичный ключ значения, а его ключ индекса.
Мы также можем получить первичный ключ для текущей записи. Для этого достаточно обратиться к свойству primaryKey курсора.
Перебираем значения по индексу email и помещаем их в массив vals, а их первичные ключи - в массив keys:
[code]var vals = [];
var keys = [];
var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("email");
ind.openCursor().onsuccess = function (evt) {
var cur = evt.target.result;
if (cur) {
vals.push(cur.value);
keys.push(cur.primaryKey);
cur.continue();
}
}[/code]
А вот так мы можем выбрать все значения с одинаковыми ключами индекса, например, всех Вась Пупкиных:
[code]var vals = [];
var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("name");
var kr = window.IDBKeyRange.only("Вася Пупкин");
ind.openCursor(kr).onsuccess = function (evt) {
var cur = evt.target.result;
if (cur) {
vals.push(cur.value);
cur.continue();
}
}[/code]
Мы уже знаем, что при создании курсора можем указать порядок перебора значений: прямой ("next") или обратный ("prev"). В случае использовании курсора, основанного на индексе, который может хранить дублирующиеся значения ключа, мы можем использовать ещё два порядка перебора:
"nextunique" - прямой с перебором лишь уникальных ключей;
"prevunique" - обратный с перебором лишь уникальных ключей.
Метод openKeyCursor отличается от метода openCursor тем, что свойство value курсора содержит не само значение, а его первичный ключ.
Перебираем значения по индексу email и помещаем их первичные ключи в массив keys:
[code]var keys = [];
var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("email");
ind.openKeyCursor().onsuccess = function (evt) {
var cur = evt.target.result;
if (cur) {
keys.push(cur.value);
cur.continue();
}
}[/code]
6. Обновление базы данных
Ещё в начале этой статьи говорилось, что каждая база данных имеет определённую версию, выражаемую целым числом. В обычной работе эта версия никак не используется.
Однако она может очень пригодиться, если мы соберёмся модифицировать страницу, использующую индексированное хранилище, и выясним, что для этого придётся обновить структуру используемой этой страницей базы данных. В этом случае нам потребуется создать новые объектные хранилища, индексы и, возможно, удалить существующие.
Выполнить обновление базы данных очень просто. Достаточно открыть её, указав в вызове метода open объекта IDBFactory (см. параграф 3.2) больший номер версии.
Откроем созданную ранее базу данных test, указав номер версии 2:
После этого в запросе возникнет событие upgradeneeded, в обработчике которого мы сможем изменить структуру базы. Например, создать новые объектные хранилища и индексы, применив методы createObjectStore и createIndex соответственно.
Создаём объектное хранилище goods и индекс price:
[code]var req = window.indexedDB.open("test");
req.onupgradeneeded = function (evt) {
var db = evt.target.result;
var os = db.createObjectStore("goods", {keyPath: "title"});
os.createIndex("price", "price");
}[/code]
Чтобы удалить объектное хранилище, нужно вызвать метод deleteObjectStore объекта IDBDatabase:
Единственным параметром указывается строка с именем объектного хранилища. Результата этот метод не возвращает.
[code]req.onupgradeneeded = function (evt) {
var db = evt.target.result;
db.deleteObjectStore("data");
}[/code]
Свойство objectStoreNames возвращает список имён всех имеющихся в базе объектных хранилищ, заданных в виде строк. Он представляет собой экземпляр объекта-коллекции DOMStringList, который можно рассматривать как обычный массив JavaScript с расширенными возможностями. В частности, он поддерживает метод contains, позволяющий выяснить, содержится ли в коллекции указанное значение:
[code]<экземпляр объекта DOMStringList>.contains(
<искомое значение>
)[/code]
Строка, наличие которой в коллекции следует проверить, указывается в виде строки. Метод возвращает true, если эта строка имеется в коллекции, и false в противном случае.
Перед удалением объектного хранилища data проверяем, имеется ли оно в базе данных:
[code]req.onupgradeneeded = function (evt) {
var db = evt.target.result;
if (db.objectStoreNames.contains("data")) {
db.deleteObjectStore("data");
}
}[/code]
Свойство indexNames объекта IDBObjectStore возвращает список имеющихся в объектном хранилище индексов, также представляющий собой экземпляр объекта-коллекции DOMStringList.
На заметку Объектом IDBObjectStore также поддерживается метод deleteIndex, выполняющий удаление индекса. Но проблема в том, что вызвать этот метод можно лишь в транзакции, запущенной в режиме versionchange, то есть в обработчике события upgradeneeded. А получить доступ к созданному ранее объектному хранилищу в этом обработчике мы не можем. Поэтому данный метод использовать никак не получится.
Эта досадная проблема существует даже не на уровне отдельных Web-обозревателей, а на уровне самого стандарта. Будем надеяться, что она со временем будет устранена.
7. Дополнительные инструменты для работы с индексированным хранилищем
Осталось рассмотреть остальные инструменты, которые предоставляет разработчику индексированное хранилище.
7.1. Работа с базами данных
Прежде всего, база данных, то есть представляющий её объект IDBDatabase, поддерживает следующие свойства:
name - возвращает имя базы данных в виде строки;
version - возвращает версию базы данных в виде целого числа.
[code]var req = window.indexedDB.open("test");
req.onsuccess = function (evt) {
var db = evt.target.result;
window.alert("База данных " + db.name + " версии " + db.version);
}[/code]
Метод close того же объекта IDBDatabase закрывает базу данных. Он не принимает параметров и не возвращает результата.
[code]db.close();[/code]
Отметим, что операция закрытия базы выполняется асинхронно. При этом, поскольку мы не можем получить представляющий эту операцию запрос, мы не можем отследить момент её завершения.
Иногда возникает необходимость удалить базу данных. Для этого предназначен метод deleteDatabase объекта IDBFactory:
[code]<экземпляр объекта IDBFactory>.deleteDatabase(
<имя базы данных>
)[/code]
Имя удаляемой базы данных задаётся в виде строки. Метод возвращает в качестве результата запрос, в котором мы можем отследить завершение операции по удалению базы данных.
[code]var req = window.indexedDB.deleteDatabase("test");
req.onsuccess = function (evt) {
window.alert("База данных test удалена.");
}[/code]
7.2. Работа с запросами
Объект IDBRequest поддерживает ряд свойств, которые могут нам пригодиться:
source - возвращает источник запроса, например, объектное хранилище, в котором выполняется операция по поиску значения; если источник отсутствует (скажем, при вызове метода open объекта IDBFactory), возвращается null;
transaction - возвращает транзакцию, в контексте которой выполняется операция; если операция выполняется не в транзакции (к операциям такого рода относятся, в частности, открытие базы данных), возвращается null;
readyState - возвращает состояние выполнения запроса в виде строки "pending" (операция выполняется) или "done" (операция выполнена, и результат, если он есть, может быть получен).
7.3. Работа с объектными хранилищами
Объект IDBObjectStore, представляющий объектное хранилище, поддерживает такое свойства:
name - возвращает имя объектного хранилища в виде строки;
keyPath - возвращает путь ключа в виде строки; если путь ключа не был задан при создании объектного хранилища, возвращается null;
autoIncrement - возвращает true, если в объектном хранилище используется счётчик, и false в противном случае.
Метод clear объекта IDBObjectStore очищает объектное хранилище, удаляя все записанные в нём значения. Он не принимает параметров и возвращает в качестве результата запрос, так что мы можем отследить момент, когда очистка хранилища завершится.
Единственным параметром указывается количество значений, на которые следует переместить курсор в заданном при его создании направлении. Результата этот метод не возвращает.
Помещаем в массив vals все нечётные значения:
[code]var vals = [];
var os = db.transaction("addresses").objectStore("addresses");
os.openCursor().onsuccess = function (evt) {
var cur = evt.target.result;
if (cur) {
vals.push(cur.value);
cur.advance(2);
}
}[/code]
Метод update заменяет значение, на котором в данный момент установлен курсор, другим, не изменяя позиции самого курсора:
[code]<курсор>.update(
<новое значение>
)[/code]
Он возвращает в качестве результата запрос, так что мы можем отследить момент, когда значение будет изменено.
Сменим имя у каждого Васи Пупкина, что хранился в нашей базе:
[code]var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("name");
var kr = window.IDBKeyRange.only("Вася Пупкин");
ind.openCursor(kr).onsuccess = function (evt) {
var cur = evt.target.result;
if (cur) {
cur.update("Василий Пупкин");
cur.continue();
}
}[/code]
А не принимающий параметров метод delete удаляет текущее значение, не изменяя позицию курсора. Он также возвращает в качестве результата запрос.
Удалим всех Петь Васькиных:
[code]var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("name");
var kr = window.IDBKeyRange.only("Петя Васькин");
ind.openCursor(kr).onsuccess = function (evt) {
var cur = evt.target.result;
if (cur) {
cur.delete();
cur.continue();
}
}[/code]
7.5. Работа с диапазонами ключей
Объект IDBKeyRange поддерживает четыре свойства, которые могут нам пригодиться:
lower - возвращает начальную границу диапазона, если она была задана;
upper - возвращает конечную границу диапазона, если она была задана;
lowerOpen - возвращает true, если начальная граница исключена из диапазона, и false в противном случае;
upperOpen - возвращает true, если конечная граница исключена из диапазона, и false в противном случае.
7.7. Работа с индексами
Объект IDBIndex поддерживает следующие свойства:
name - возвращает имя индекса в виде строки;
keyPath - возвращает путь ключа индекса в виде строки;
unique - возвращает true, если индекс может содержать лишь уникальные ключи, и false в противном случае;
objectStore - возвращает объектное хранилище, в котором был создан этот индекс.
Метод count позволяет узнать количество значений в индексе:
[code]<индекс>.count(
[<ключ индекса или диапазон ключей>]
)[/code]
В качестве необязательного параметра можно указать либо ключ индекса, либо диапазон ключей; тогда будут подсчитаны значения с заданным ключом индекса или со всеми ключами, укладывающимися в заданный диапазон. Если же параметр не указан, будет подсчитано общее количество значений в индексе.
Метод count возвращает запрос. Как только представляемая этим запросом операция подсчёта значений завершится, количество значений можно будет получить из свойства result этого запроса.
Для примера подсчитаем общее количество всех Вась Пупкиных:
[code]var os = db.transaction("addresses").objectStore("addresses");
var ind = os.index("name");
var kr = window.IDBKeyRange.only("Вася Пупкин");
ind.count(kr).onsuccess = function (evt) {
window.alert(evt.target.result);
}[/code]
8. Заключение
Индексированное хранилище можно применять для сохранения набора каких-либо величин, относящихся к одному типу: электронных писем в Web-приложениях почтовых клиентов, список приобретённых товаров в приложениях интернет-магазинов и пр. Хоть реализация индексированного хранилища на данный момент ещё хромает (и виноваты в этом не только разработчики Web-обозревателей, но и создатели интернет-стандартов), оно уже вполне функционально.
Дополнительные материалы
[url=https://msdn.microsoft.com/en-us/library/hh772651(v=vs.85).aspx]Справочник по API индексированного хранилища на сайте MSDN[/url].
[url=https://msdn.microsoft.com/en-us/library/jj154908(v=vs.85).aspx]Весьма основательное руководство разработчика по индексированному хранилищу, в котором описывается создание модуля "облака тегов" для сайта фотогалереи, на сайте MSDN[/url].
Сайт является источником уникальной информации о семействе операционных систем Windows и других продуктах Microsoft. Перепечатка материалов возможна только с разрешения редакции.
Работает на WMS 2.34 (Страница создана за 0.032 секунд (Общее время SQL: 0.013 секунд - SQL запросов: 51 - Среднее время SQL: 0.00026 секунд))