Представьте, что у вас есть хранилище данных с REST-интерфейсом. Пусть в нем хранится информация о книгах и вы хотите вывести список всех книг. Можно сделать метод «books», который будет возвращать нам список книг. Но при отображении списка обычно есть паджинация или ленивая подгрузка данных, а еще пользовать хочет фильтровать и сортировать данные. Когда мы добавляем поддержку мобильных устройств у нас появляется еще потребность как-то ограничить объем получаемых данных не передавая часть полей. Всю эту информацию должен уметь понимать почти любой метод получения списка объектов, т.к. списки отображаются с помощью специального виджета. И тут нам на помощь приходит Resource Query Language.

Resource Query Language (RQL) — это язык запросов, разработанный для использования в URI при работе с объекто-подобными структурами данных. С помощью RQL клиент может запрашивать у сервера список объектов соответствующих определенным правилам, т.е., по сути, это синтаксис, который описывает как запрашивать данные. Например, запрос выбирающий все книги авторства Перумова может быть записан как eq(author,Перумов) или в обычном формате URL: author=Перумов.

RQL можно рассматривать, в основном, как набор вложенных операторов, каждый из которых имеет множество аргументов. Он рассчитан на очень простую, но расширяемую грамматику, которая может быть использована в валидном URL запросе.

Как использовать RQL


RQL выражение содержит функции запроса или операторы сравнения, соединенные логическими операторами.

Типовые сценарии


Сначала разберем типовые сценарии. Для лучшего понимания будем сравнивать с аналогичными командами в MongoDB.

Пусть у нас есть список книг:

[
{ title: "Эльфийский клинок", year: 1993, series: "Кольцо тьмы" },
{ title: "Чёрное копьё", year: 1993, series: "Кольцо тьмы" },
{ title: "Адамант Хенны", year: 1995, series: "Кольцо тьмы" },
{ title: "Воин Великой Тьмы", year: 1995, series: "Летописи Хьёрварда" }
]

Выведем все книги из серии «Кольцо тьмы». В MongoDB мы бы сделали это так:

db.users.find({series: "Кольцо тьмы"})

в RQL это будет выглядеть так:

eq(series, "Кольцо тьмы")

или

series="Кольцо тьмы"

Такой запрос вернет нам три книги.

Теперь более сложный запрос: нам надо вывести все книги серии «Кольцо тьмы» изданные в 1995 году.

В MongoDB:

db.users.find({series: "Кольцо тьмы", year: 1995})

В RQL:

eq(series, "Кольцо тьмы"),eq(year, 1995)

Предыдущие запросы применялись к простым объектам. Но документы могут быть очень сложными по структуре. Например, добавим в нашу библиотеку новую книгу:

{ title: "Воин Великой Тьмы", year: 1995, series: "Летописи Хьёрварда", translations: { language: "English", title: "Godsdoom" } }

Здесь определяется вложенный объект с ключом translations. И чтобы найти все книги переведенные на английский язык, нам надо использовать точку.

В MongoDB:

db.users.find({"translations.language": "English"})

В RQL:

eq(translations.language, "English")

Со временем наша библиотека выросла. Список книг не помещается на экран и мы решили показывать его постранично.

В MongoDB мы можем получить второй десяток записей следующим образом:

db.users.find().skip(10).limit(10)

В RQL:

limit(10,10)

Но показывать постранично мало. Еще хочется сортировать данные.

В MongoDB это будет:

db.users.find().sort({title: 1})

В RQL:

sort(+title) 

Функции


Базовые функции стандарта:
Функция Описание
in (<propеrty>,<array-of-values>) Выбирает объекты, у которых значение
указанного свойства входит в заданный массив свойств.

Пример:
in(name,(Silver,Gold))
out (<propеrty>,<array-of-values>) Выбирает объекты, у которых значение указанного свойства не входит в заданный массив свойств.

Пример:
out(name,(Platinum,Gold))
limit (<stаrt>,<numbеr>) Возвращает заданное количество
(number)
объектов начиная с определённой (start) позиции.

Пример:
limit(0,2)
sort (<list of properties with + or — prefix>) Сортирует список объектов по заданным свойствам (количество свойств неограниченно). Сначала список сортируется по первому из заданных свойств, затем по второму, и так далее.
Порядок сортировки определяется префиксом: + — по возрастанию, — — по убыванию.

Пример:
sort(+memory,-diskspace)
select (<list оf attributes>) Обрезает каждый объект до набора свойств, определенных в аргументах.

Пример:
select(name,user)
values(<prоperty>) Возвращает набор значений из указанного поля всех объектов.

Пример:
values(ve.name)
count() Возвращает количество записей.

Пример:
in(name,(Silver,Gold))&count()
max(<prоperty?>) Возвращает максимальное значение из указанного поля всех объектов.

Пример:
max(ve.memory)
min(<prоperty?>) Возвращает минимальное значение из указанного поля всех объектов.

Пример:
min(ve.memory)

Больше существующих операторов можно найти в официальной документации.

В технологии APS в RQL добавлена новая функция:
Функция Описание
like (<prоperty>, <pаttern>) Ищет заданный паттерн (pattern) в заданном свойстве (property). Эта
функция аналогична оператору SQL LIKE, хоть и использует символ * вместо %. Чтобы определить в паттерне сам символ *, он должен быть процентно-кодированным, то есть надо писать %2A вместо *, см. примеры. Кроме того, в паттерне можно использовать символ ?, обозначающий, что любой символ в этой позиции является валидным.

Примеры:
like(firstName,Jo*)
like(firstName,*ohn)
like(firstName,*oh*)
like(firstName,Joh?)

И еще три специфичные для APS функции:
Функция Описание
implementing (<basе-type>) Возвращает список объектов (ресурсы и типы), реализующих базовый тип и включающих в себя сам базовый тип.

Пример:
implementing (http://aps-standard.org/samples/user-mgmt/offer/1.0)
composing (<dеrived-type>) Возвращает список типов, которые реализованы производным типом (derived type), включая сам производный тип.

Пример:
composing(http://aps-standard.org/samples/user-mgmt/offer/1.0)
linkedWith (<rеsource ID>) Возвращает список ресурсов, которые связаны тем ресурсом, чей ID указан в качестве аргумента. APS-контроллер ищет все ссылки на ресурсы, включая внутренние системные ссылки. Например, актор admin/owner/referrer, имеющий доступ к ресурсу, тоже будет считаться «связанным» ресурсом.

Примеры:
linkedWith(220aa29a-4ff4-460b-963d-f4a3ba093a0a)

implementing(http://aps-standard.org/types/core/user/service/1.0), linkedWith(220aa29a-4ff4-460b-963d-f4a3ba093a0a)

Логические операторы


Логические операторы позволяют посредством булевой логики объединить две и больше функций запроса. Все стандартные логические операторы имеют короткие алиасы.
Оператор Алиас Примеры
and (<quеry>,<quеry>,...) &
,
and (limit(0,2),like(name,*L*))

Значение
Выбирает первые два предложения, имена которых соответствуют нечувствительному к регистру паттерну *L*
or (<quеry>,<quеry>,...) |
;
or(like(description,*free*),in(name,(Silver,Gold)))

Значение
Выбирает все предложения, описания (description) которых соответствуют паттерну *free*, а также те, чьё имя Silver или Gold.

В технологии APS в RQL также добавлен новый логический оператор отрицания:
Оператор Примеры
not (<quеry>) not(like(name,*free*))

Значение
Выбирает все предложения, за исключением тех, чьё имя соответствует
Хабр и Гиктаймс — RQL паттерну *free*.

Примечание

  1. Оператор and является неявным RQL-оператором верхнего уровня. Например, выражение http://hosting.com?and(arg1,arg2,arg3) эквивалентно http://hosting.com?arg1,arg2,arg3.
  2. У оператора and приоритет выше, чем у or. Поэтому при использовании алиасов нужно заключать объединённые запросы в круглые скобки, тем самым определяя необходимый порядок обработки. Например, implementing(<typе>),(prop1=eq=1|prop2=ge=2).

Операторы сравнения


Оператор сравнения используется для фильтрации объектов посредством сравнения одного из их свойств с заданным значением.
Оператор Алиас Примеры
eq (<propеrty>,<valuе>) =eq= eq(aps.status,aps:ready)
aps.status=eq=aps:ready


Значение
Выбирает все объекты, чей aps.status имеет значение aps:ready.
ne (<propеrty>,<valuе>) =ne= ne(aps.status,aps:ready)
aps.status=ne=aps:ready


Значение
Выбирает все объекты, чей aps.status имеет значение aps:ready.
gt (<propеrty>,<valuе>) =gt= implementing(http://aps-standard.org/samples/user-mgmt/offer),hardware.memory=gt=1024)

Значение
Выбирает все предложения (offers), предоставляющие hardware.memory больше 1024.
ge (<propеrty>,<valuе>) =ge= implementing(http://aps-standard.org/samples/user-mgmt/offer),hardware.memory=ge=1024)

Значение
Выбирает все предложения (offers), предоставляющие hardware.memory больше или равно 1024.
lt (<propеrty>,<valuе>) =lt= implementing(http://aps-standard.org/samples/user-mgmt/offer),hardware.CPU.number=lt=16)

Значение
Выбирает все предложения (offers), предоставляющие hardware.CPU.number меньше 16.
le (<propеrty>,<valuе>) =le= implementing(http://aps-standard.org/samples/user-mgmt/offer),hardware.CPU.number=le=16)

Значение
Выбирает все предложения (offers), предоставляющие hardware.CPU.number меньше или равно 16.

Строковые типы сравниваются в лексикографическом порядке.

Значения


Функции запросов и операторы сравнения могут содержать следующие значения:

  • Строковые (с использованием URL-кодирования)
  • Числа
  • Даты (в формате ISO UTC без двоеточия)
  • Булевы
  • Функции-значения (Value functions)

Функции-значения — это функции, возвращающие особые значения вроде null, true, false или пустое строковое значение. Все они применимы к определённым типам данных.
Функция-значение Применимые типы Описания Примеры
null() Любой тип Задаётся, если значение null name=eq=null()
true()
false()
Булевы Задаётся, если значение true или false disabled=eq=false()
empty() Строковые Задаётся, если строковое значение является пустым (не null, но не содержит никаких символов) addressPostal.extendedAddress=eq=empty()

Использование


Существует большое количество реализации парсеров RQL на различных языках программирования.

Реализация на JavaScript кроме парсера содержит еще и движок, который умеет применять RQL-запрос к массиву объектов.

Оригинальная реализация RQL на JavaScript есть в npmjs: https://www.npmjs.com/package/rql

Реализация с добавленной нами функциональностью также доступна через npm: https://www.npmjs.com/package/aps-rql
Поделиться с друзьями
-->

Комментарии (19)


  1. shude
    04.07.2017 13:10

    Таблички что-то совсем разъехались.


    1. babylon
      04.07.2017 13:14

      Чем вас JSONPath не устраивает?


      1. shude
        04.07.2017 13:17

        А причем JsonPath? Я про оформление поста…


      1. BuranLcme
        04.07.2017 15:43

        Посмотрел документацию по JSONPath. Не увидел там возможность поддержки функций типа like и логических операторов.


  1. EreminD
    04.07.2017 13:29
    +1

    Для табличек удобно обернуть значения в первой колонке в тег

    <nobr>не переносимый длинный текст</nobr>
    
    


    1. BuranLcme
      04.07.2017 15:41

      Спасибо большо! Поправим. Конечно, жаль, что Habr не позволяет просто указать ширину колонок.


  1. raveclassic
    04.07.2017 13:55
    +2

    А чем odata не устраивает?


    1. alemiks
      04.07.2017 17:58

      пару лет про него ничего не слышал, в то время он загибался вроде. Щас ожил?


      1. raveclassic
        04.07.2017 18:05
        +3

        Так же загибается. Это же ждет и RQL.


  1. garex
    04.07.2017 13:58
    +3

    Чем ваш вариант лучше http://graphql.org/ ?


    С допущением, что у проекта нет уже написанного REST-api?


    1. raveclassic
      04.07.2017 14:11
      +1

      У меня даже получилось заставить работать вот эту штуку


      1. garex
        04.07.2017 15:20

        Месье знает толк ))


        А по сути — прикольно: клиент точно будет делать один запрос, а уже адаптер локально кучу. По сети улетит один результат.


        1. raveclassic
          04.07.2017 15:23

          Ну у нас просто REST заказчиком навязан, так как есть еще сервисы, которые будут им пользоваться. А под свое приложение хочется чего-то более высокоуровнего и чтобы еще и генерилось само.


          Спойлер: graphql все-равно не пропустили, так что пока генерим ts через swagger-codegen.


    1. BuranLcme
      04.07.2017 15:49

      Наш проект старше чем graphql, а сам по себе RQL еще старше.
      Плюс для нас удобно, что RQL запрос это просто строка. Например, в APS приложении структура экранов описывается XML-метафайлом и приложение может указать какие данные нужны для отображения этого View. Тогда в момент показа они уже будут загружены, а если обязательных данных нет, то View будет скрыто из меню и пользователь не сможет на него перейти. В виде строчного query это удобно описать, а в graphql формате, похоже, это будет выглядит весьма громоздко.


      1. arvitaly
        04.07.2017 18:33
        +1

        GraphQL — это тоже строка.

        GraphQL documents are expressed as a sequence of Unicode characters. However, with few exceptions, most of GraphQL is expressed only in the original non?control ASCII range so as to be as widely compatible with as many existing tools, languages, and serialization formats as possible and avoid display issues in text editors and source control.

        в graphql формате, похоже, это будет выглядит весьма громоздко.

        Не будет, а вернее, будет так, как вы опишите схему.

        Простейший запрос
        query1


        1. BuranLcme
          04.07.2017 18:44

          Понятно что в строку можно записать почти что угодно. Минификатор в том числе этим и занимается. Но, судя по большинству примеров, запросы GraphQL это все-таки структуры и их лучше писать в отформатированном виде.


          1. alibertino
            05.07.2017 03:37
            +1

            Речь о том, что в GraphQL нет ничего громоздкого, скорее наборот, это один из самых минималистичных способов записи запроса данных (или представления данных). Мультидревовидная структура с поддержкой функций и директив покрывает большинство сценариев, как показали дальнейшие исследования, не хватает только последовательных запросов (batching), что легко решается.
            https://dev-blog.apollodata.com/new-features-in-graphql-batch-defer-stream-live-and-subscribe-7585d0c28b07


  1. hensew
    04.07.2017 15:40

    Предусматривается ли возможность подписаться на изменения?
    Как отреагирует limit(n, m) на добавление/удаление записей до n+m?


    1. BuranLcme
      04.07.2017 15:52
      +1

      RQL это просто запросы к хранилищу. Никакого байндинга не подразумевается.
      В APS UI отслеживание можно делать на уровне хранилищ. Повесить observer на хранилище и по его событиям обновлять виджеты, которые отображают данные из него.