Речь пойдет о создании веб-приложения на Intersystems Caché с использованием javascript плагина для отображения табличных данных — jqGrid. Плагин часто упоминается на Хабре, поэтому основное внимание будет уделено особенностям его использования со стороны Caché
Преимущества использования jqGrid:
Состав блюда: хранимый класс с данными, класс-страница, класс-сервис данных. Необходимые библиотеки и стили подключаются из сетей доставки данных (CDN) и с сайтов разработчиков, поэтому, для работы примера в локальной сети, их необходимо будет скачать из этих источников.
Предупреждение: пример максимально упрощен, рассматривается только малая часть возможностей плагина, но кода все равно много, хотя он и тщательно задокументирован.
В качестве основы этого блюда будет выступать класс model.person.
Создадим в Caché Studio хранимый класс. Файл — Создать — Класс Caché
Сгенерируем небольшое количество тестовых экземпляров – переходим в окно «Вывод» студии (Alt+2) и выполняем:
Создадим страницу отображения данных — класс view.person с необходимыми стилями и вспомогательными библиотеками.
Перед тем как перейти к коду класса-сервиса данных, необходимо рассказать о протоколе и форматах обмена данными плагина с сервером. Плагин асинхронно подгружает порции данных с сервера, отправляя на него запросы следующего вида:
Где: rows – количество строк данных в порции, page – порядковый номер порции, _search – режим поиска (включен или выключен), McCormick – поисковое значение по колонке name таблицы, sidx – поле сортировки, sord – порядок сортировки
В ответ jqGrid ожидает следующие данные (для примера выбран следующий формат json данных)
Где, records-общее количество строк, total – количество порций данных на сервере, page – выгружаемая порция данных, rows – массив с данными
Запросы на изменение данных от плагина, поступающие на адрес, указанный в параметре editurl выглядят следующим образом:
создание:
изменение:
удаление:
Ответ сервера на эти запросы обрабатывается в функции afterSubmit, результатом которой должен быть массив вида:
Где: result — результат выполнения запроса (true || false), error — сообщение об ошибке, если result == false, id — значение идентификатора объекта (в случае операции создания)
Реализацией этого протокола займется класс-сервис данных service.person, который будет разбирать поступающие параметры запроса, формировать динамический запрос и выводить данные в определенном формате.
Для запуска приложения, в Cache Studio открываем класс view.person и нажимаем F5. Рабочий пример развернут здесь
Пример может произвести шокирующее впечатление из-за огромного количества кода необходимого для реализации относительно простой задачи. В этот момент нужно помнить о том, что Caché — объектно-ориентированная СУБД и большая часть этого кода может быть параметризована и вынесена в родительские классы, или реализована в виде интерактивного диалога, создающего комплект необходимых классов. Тогда для реализации данного примера потребовалось указание всего двух параметров — имя хранимого класса и списка его свойств.
UPD: Именно в день публикации статьи jqueryui.com сменил дизайн и структуру, убрав из доступа themeswitchertool.js Теперь используется его упрощенный функциональный аналог, исходники можно забрать здесь
Преимущества использования jqGrid:
- разгрузка сервера от клиентской логики
- возможность использования различных форматов обмена данными (xml, json)
- различные способы отображения данных (таблица, дерево, вложенные таблицы)
- готовый функционал по изменению данных – редактирование в строках, редактирование в формах, проверка данных
- большое количество настроек, опций и событий с документацией, примерами и исходным кодом
- богатый пользовательский функционал — сортировка, группировка, фильтрация, поиск, итоги, настройка отображения столбцов (видимость, порядок, размеры), поддержка множества тем визуального оформления от jqueryui
Состав блюда: хранимый класс с данными, класс-страница, класс-сервис данных. Необходимые библиотеки и стили подключаются из сетей доставки данных (CDN) и с сайтов разработчиков, поэтому, для работы примера в локальной сети, их необходимо будет скачать из этих источников.
Предупреждение: пример максимально упрощен, рассматривается только малая часть возможностей плагина, но кода все равно много, хотя он и тщательно задокументирован.
Модель
В качестве основы этого блюда будет выступать класс model.person.
Создадим в Caché Studio хранимый класс. Файл — Создать — Класс Caché
Исходный код хранимого класса model.person
/// Простой хранимый класс
Class model.person Extends (%Persistent, %Populate){
/// ФИО — укажем какой функцией заполнять
Property name As %String(POPSPEC = "Name()");
Index name On name;
/// Год рождения
Property year As %Integer(MAXVAL = 2012, MINVAL = 1910);
/// Поиск по годам будет быстрее
Index year On year [ Type = bitmap ];
}
Class model.person Extends (%Persistent, %Populate){
/// ФИО — укажем какой функцией заполнять
Property name As %String(POPSPEC = "Name()");
Index name On name;
/// Год рождения
Property year As %Integer(MAXVAL = 2012, MINVAL = 1910);
/// Поиск по годам будет быстрее
Index year On year [ Type = bitmap ];
}
Сгенерируем небольшое количество тестовых экземпляров – переходим в окно «Вывод» студии (Alt+2) и выполняем:
write ##class(model.person).Populate(100000)
Вид
Создадим страницу отображения данных — класс view.person с необходимыми стилями и вспомогательными библиотеками.
Исходный код класса view.person
/// Отображение данных экземпляров класса <class>model.person</class>
/// с помощью <a href="http://www.trirand.com/blog/">jqgrid</a>
Class view.person Extends %CSP.Page {
/// node для локализации текстовых ресурсов
Parameter DOMAIN = "person";
/// Метод вывода содержимого страницы
ClassMethod OnPage() As %Status
{
&html<<!DOCTYPE html>
<html lang="ru"><head>
<meta charset="ru"/>
<title>#($$$Text("Intersystems Caché + jqGrid"))#</title>
<style>
/*диалоговые окна должны выглядеть соразмерно оформлению грида*/
body {font-size: 11px; font-family: Georgia,Verdana,Arial,sans-serif; }
</style>
<!-- Цветовая схема по умолчанию -->
<link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.8.24/themes/base/jquery-ui.css" />
<!-- стили для оформления таблицы -->
<link rel="stylesheet" type="text/css" href="http://www.trirand.net/themes/ui.jqgrid.css" />
<!-- то, что любит нас -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<!-- интерфейсная библиотека (диалоги, кнопки, etc.) -->
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.23/jquery-ui.min.js"></script>
<!-- локализованные текстовые ресурсы jgGrid-->
<script src="http://www.trirand.net/js/trirand/i18n/grid.locale-ru.js"></script>
<!-- Собственно сам табличный плагин -->
<script src="http://www.trirand.net/js/trirand/jquery.jqGrid.min.js"></script>
<!-- Переключатель цветовых схем -->
<script src="themeswitcher.js"></script>
</head><body>
<!-- Элемент для грида c адресом источника данных -->
<table id="grid"></table>
<!--Элемент для панели управления гридом-->
<div id="bar"></div>
<!--Элемент для переключателя тем -->
<div id="theme"></div>
<!-- инициализации страницы -->
<script type="text/javascript">
$( function(){ //функция обработчик document.ready
var $grid=$( "#grid" ) //находим элемент крепления для грида
, bar="#bar" //элемент крепления панели управления
, url='#(..Link("service.person.cls"))#'
;
$grid.jqGrid({ //инициализируем плагин грида, передавая ему объект с настройками
caption: '#($$$Text("person"))#'
, colModel: [ // описание модели колонок
{ name: 'name', width: 250, editable: true }
,{ name: 'year', editable: true }
]
, pager: bar //передаем селектор для панели управления гридом
, url: url //источник данных
, editurl: url //он же для редактирования
, datatype: "json" // тип получаемых данных
, mtype: 'POST'
, jsonReader: { // особенности формата получаемых данных
// каждое свойство под своим именем
// увеличивает объем данных передаваемых сервером
// но делает источник данных более универсальным
repeatitems: false
}
, height: 350, width: 900 //определяем размеры грида
, rownumbers: true //включаем показ порядкового номера строк
, rownumWidth: 45 // ширина колонки с порядковым номером строк
, viewrecords: true // покажем какую порцию данных просматриваем
, gridview: true // ускорим загрузку строк, отключив события добавления строки
, scroll: 1 // режим виртуального скроллинга
, hoverrows: true // выделение строки под курсором мыши
, rowNum: 100 // размер порции данных, запрашиваемых у сервера
, sortable: true //разрешаем перетаскивать колонки грида
, sortname: 'name' //по какой колонке сортируем по умолчанию
})
.jqGrid('filterToolbar',{searchOnEnter:false}) //включаем фильтр
.jqGrid('gridResize', {}) //разрешаем изменять высоту и ширину грида
;
/// функция обработчик ответа сервера на запросы редактирования
var serverHandler=function( resp ){
var array=[];
try {
array=eval(resp.responseText);
} catch(err){
return ["",e.description];
}
return array;
};
/// Типовые настройки для форм редактирования / создания, удаления
var opts={ afterSubmit: serverHandler //стандартный обработчик ответа сервера
, closeAfterAdd: true //закрываем диалог после выполнения на сервере
, clearAfterAdd: true //очищаем поля диалога после выполнения на сервере
, closeAfterEdit: true //закрываем диалог после выполнения на сервере
//в режиме вирт. скроллинга кнопки перехода к след. записи не нужны
, viewPagerButtons: false
};
// формируем панель управления
$grid.jqGrid('navGrid',bar,
{ edit: true, edittext: '#($$$Text("Редактировать"))#'
, add: true, addtext: '#($$$Text("Создать"))#'
, del: true, deltext: '#($$$Text("Удалить"))#'
, view: false, search: false
}
, opts //опции редактирования
, opts //опции создания
, opts //опции удаления
);
//работаем в режиме виртуального скроллинга
//элемент зарезервированный под pager не нужен
$(bar+"_center").remove();
//запускаем переключатель тем, см UPD
themeswitcher( $( "#theme" ) );
});</script></body></html>>
Quit $$$OK
}
}
Контроллер
Перед тем как перейти к коду класса-сервиса данных, необходимо рассказать о протоколе и форматах обмена данными плагина с сервером. Плагин асинхронно подгружает порции данных с сервера, отправляя на него запросы следующего вида:
?rows=100&page=2&_search=true&name=McCormick&sidx=name&sord=desc
Где: rows – количество строк данных в порции, page – порядковый номер порции, _search – режим поиска (включен или выключен), McCormick – поисковое значение по колонке name таблицы, sidx – поле сортировки, sord – порядок сортировки
В ответ jqGrid ожидает следующие данные (для примера выбран следующий формат json данных)
{"records": 102, "total": 2, "page": 2, "rows": [
{ "id": 1,"name": "McCormick,Diane Z.", "year": 1910 }
,{ "id": 2,"name": "McCormick,Christen G.", "year": 1911 }
]}
Где, records-общее количество строк, total – количество порций данных на сервере, page – выгружаемая порция данных, rows – массив с данными
Запросы на изменение данных от плагина, поступающие на адрес, указанный в параметре editurl выглядят следующим образом:
создание:
?oper=add&id=_empty&name=habra&year=2005
изменение:
?oper=edit&id=1&name=habra&year=2006
удаление:
?oper=del&id=1
Ответ сервера на эти запросы обрабатывается в функции afterSubmit, результатом которой должен быть массив вида:
[ result, error, id ]
Где: result — результат выполнения запроса (true || false), error — сообщение об ошибке, если result == false, id — значение идентификатора объекта (в случае операции создания)
Реализацией этого протокола займется класс-сервис данных service.person, который будет разбирать поступающие параметры запроса, формировать динамический запрос и выводить данные в определенном формате.
Исходный код класса service.person
#; подключаемый файл определений csp.inc
#; смотри первую строчку в классе service.person
/// сокращает код извлечения параметров запроса
/// из системной переменной %request
#define get(%name) $g(%request.Data(%name,1))
Include csp
/// Контроллер - сервис
Class service.person Extends %CSP.Page {
/// node для текстовых ресурсов
Parameter DOMAIN = "person";
/// Can only be referenced from another CSP page
Parameter PRIVATE = 1;
ClassMethod OnPage() As %Status {
#; см. csp.inc
set oper=$$$get("oper") ;определяем тип запроса
#; если определена операция - переходим к ее выполнению
if ( oper = "add" ) Quit ..Add()
Q:oper="edit" ..Edit()
Q:oper="del" ..Del()
#; Во всех остальных случаях - режим просмотра данных
#;количеством строк в порции данных приводим к положительному целому числу
set rows=$$$get("rows")\1 ;целочисленное деление
if ( rows < 1 ) {
set rows = 100 ;это значение рекомендуется вынести в параметры класса
}
#;Аналогично поступаем с порядковым номером порции данных
s page=$$$get("page")\1 s:page<1 page=1
#;Если был включен режим поиска (в данном примере - фильтрации)
#;собирем часть sql выражения where и его параметры
s where="", params="", search=$CASE( $$$get("_search"), "true": 1, : 0 )
if ( search ) {
s name=$$$get( "name" ) if ( name'="" ) {
#; у name тип строка, поэтому поиск по вхождению
#; или на ваше усмотрение
s where=where_$ListBuild( "name Like ?" )
s params( $increment(params) )="%"_name_"%"
}
s year=$$$get( "year" ) if ( year'="" ) {
#; year - зарезервированное слово SQL - заключаем в двойные кавычки
s where=where_$LB( """year"" = ?" ) ;тип данных - целое
, params( $i( params ) )=year\1 ;
}
s where=$ListToString(where," AND ")
}
#; зная какие данные надо отображать
#; можем узнать сколько всего записей будет в результатах запроса
s countSQL=" SELECT Count(*) as records FROM model.person "
if ( search ) s countSQL=countSQL_" WHERE "_where
s records=0
#dim RS as %SQL.StatementResult
s stmt=##class(%SQL.Statement).%New()
s sc=stmt.%Prepare(countSQL) if 'sc d ..ShowError(sc) Q $$$OK
s RS=stmt.%Execute(params...)
if RS.%SQLCODE d ##class(%SYSTEM.SQL).SQLCODE(RS.%SQLCODE) Q $$$OK
s:RS.%Next() records=RS.records
kill RS
#; всего целых порций данных
s total = records\rows ;целочисленное деление
, part=records#rows ;остаток
s:part total=total+1 ;если есть остаток добавляем еще одну порцию
#; зная количество порций, еще раз проверяем номер запрощенной порцию
s:page>total page=total ;
#; рассчитываем граничные номера записей для выводимой порции
s end=page*rows, start=end-rows
#; выводить данные надо в определенном порядке
#; строим часть выражения sql order by
#; в последних версиях jqgrid есть множественная сортировка
s order="",sidx=$$$get( "sidx" ), sord=$$$get( "sord" )
#; сортировку добавляем только по разрешенным столбцам
if $ListFind( $ListBuild("name","year"), sidx ) {
s:sidx="year" sidx="""year""" ;year - зарезервированное слово в sql
s order=sidx _ " "_$CASE( sord, "desc": "desc", : "asc" )
}
#; строим выражение для вывода данных
#; оптимизируем проход по результату, отбирая только ID
s sql=" SELECT ID From model.person "
s:search sql=sql_" WHERE "_where
s:order'="" sql=sql_" ORDER BY "_order
s sc=stmt.%Prepare(sql) if 'sc d ..ShowError(sc) Q $$$OK
s RS=stmt.%Execute(params...)
if RS.%SQLCODE d ##class(%SYSTEM.SQL).SQLCODE(RS.%SQLCODE) Q $$$OK
write "{" ;выводим заголовочную часть ответа
, """records"": ", records ;всего записей подходящих по условиям
, ", ""total"": ", total ;всего порций данных
, ", ""page"": ", page ;номер текущей порции анных
, ", ""rows"": [" ;выводим массив записей
#;здесь нам понадобится макрос квотирования данных
#;в соответствии со стандартом JSON - http://json.org
#;стандартное квотирование ..QuoteJS() не подходит
#;из-за символа одинарной кавычки
#define json(%str) """"_$replace($zcvt(%str,"O","JS"),"\'","'")_""""
#;выводим строки
s sc="" for { s sc=RS.%Next() Quit:sc=0
s rnum=RS.%ROWCOUNT
if (rnum < start) continue ;пропускаем строки до порции
if (rnum > end) Quit ;прекращаем выводить после порции
#;строки после первой из порции, отделяем запятыми
#;rnum>1 - нужен для первой порции, когда start=0
if ( rnum > start ) && ( rnum > 1 ) {
w ","
}
s name=##class(model.person).nameGetStored(RS.ID)
, year=##class(model.person).yearGetStored(RS.ID)
w "{""id"":",RS.ID,",""name"":",$$$json(name),",""year"":",$$$json(year),"}"
}
w "]}"
Q $$$OK
}
/// Создаем новый объект
ClassMethod Add() As %Status {
s obj=##class(model.person).%New()
Q ..Set(.obj)
}
/// Открываем и редактируем существующий
ClassMethod Edit() As %Status {
s id=$$$get("id"), obj=##class(model.person).%OpenId(id,.sc)
if $$$ISERR( sc ) {
Q ..wResult( sc, id )
}
Q ..Set(.obj)
}
/// Обновление свойств, сохранение изменений, вывод результата
ClassMethod Set(obj As model.person) As %Status {
if ( $g(obj) = "" ) || ( '$IsObject(obj) ) {
s sc=$$$ERROR( $$$GeneralError, $$$Text("Не удалось открыть объект") )
Q ..wResult( sc )
}
s obj.name=$$$get("name")
s obj.year=$$$get("year")
s id="", sc=obj.%Save()
s:$$$ISOK(sc) id=obj.%Id()
Q ..wResult( sc, id )
Q $$$OK
}
/// Удаление объекта
ClassMethod Del() As %Status {
s id=$$$get("id"), sc=##class(model.person).%DeleteId(id)
Q ..wResult( sc, id )
Q $$$OK
}
/// Вывод результата
ClassMethod wResult(sc As %String = "", id As %String) As %Status {
s result="false", msg="", id=$g(id)
if $$$ISOK( $g(sc) ) {
s result="true"
} else {
s msg=##class(%SYSTEM.Status).GetOneErrorText(sc)
}
w "[",result,",",..QuoteJS(msg),",",..QuoteJS(id),"]"
Q $$$OK
}
}
Для запуска приложения, в Cache Studio открываем класс view.person и нажимаем F5. Рабочий пример развернут здесь
Соль и сахар по вкусу
Пример может произвести шокирующее впечатление из-за огромного количества кода необходимого для реализации относительно простой задачи. В этот момент нужно помнить о том, что Caché — объектно-ориентированная СУБД и большая часть этого кода может быть параметризована и вынесена в родительские классы, или реализована в виде интерактивного диалога, создающего комплект необходимых классов. Тогда для реализации данного примера потребовалось указание всего двух параметров — имя хранимого класса и списка его свойств.
UPD: Именно в день публикации статьи jqueryui.com сменил дизайн и структуру, убрав из доступа themeswitchertool.js Теперь используется его упрощенный функциональный аналог, исходники можно забрать здесь