Речь пойдет о создании веб-приложения на 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 Теперь используется его упрощенный функциональный аналог, исходники можно забрать здесь
