Каша быстрого приготовления — делаем CRUD в Caché с помощью jqGrid

в 7:19, , рубрики: cache, crud, intersystems cache, javacript, jqgrid, jquery, jquery plugins, Блог компании InterSystems, Веб-разработка, объектные субд, метки: , , , , , ,

Речь пойдет о создании веб-приложения на Intersystems Caché с использованием javascript плагина для отображения табличных данных — jqGrid. Плагин часто упоминается на Хабре, поэтому основное внимание будет уделено особенностям его использования со стороны Caché
Преимущества от использования 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 ];

}

Сгенерируем небольшое количество тестовых экземпляров – переходим в окно «Вывод» студии (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="http://jqueryui.com/themeroller/themeswitchertool/"></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(); 
	//запускаем переключатель тем
	$( "#theme" ).themeswitcher();

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

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
	#; см. Incude csp в начале класса
	#; определен единственный макрос 
	#; #define get(%name) $g(%request.Data(%name,1))
	#; сокращающий код извлечения параметров запроса
	#; из системной переменной %request
	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 ) )=year1 ;
		}
		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 = recordsrows ;целочисленное деление
	, 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 result="", msg=##class(%SYSTEM.Status).GetOneErrorText(sc)
	}
	w "[",result,",",..QuoteJS(msg),",",..QuoteJS(id),"]"	
	Q $$$OK
}

}

Для запуска приложения, в Cache Studio открываем класс view.person и нажимаем F5. Рабочий пример развернут здесь

Соль и сахар по вкусу

Пример может произвести шокирующее впечатление из-за огромного количества кода необходимого для реализации относительно простой задачи. В этот момент нужно помнить о том, что Caché — объектно-ориентированная СУБД и большая часть этого кода может быть параметризована и вынесена в родительские классы, или реализована в виде интерактивного диалога, создающего комплект необходимых классов. Тогда для текущего примера требовалось указание всего двух параметров — имя хранимого класса и списка его свойств

Автор: doublefint

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js