XML-сериализация для развёртывания начальных данных в Caché. Часть I

image
Думаю, не преувеличением будет сказать, что почти каждый разработчик информационной системы сталкивается с задачей формирования начальных данных при внедрении.
У Caché-разработчиков есть несколько стандартных подходов к инициализации начальных данных:

  • загрузка данных для классов-справочников из внешних файлов,
  • получение данных из онлайн-сервисов,
  • импорт статических данных из файлов-глобалов,
  • выполнение методов класса, создающих начальные данные из “зашитых” в код данных.

Для инициализации статических данных, небольших справочников или каких-либо конфигурационных данных системы, есть еще один способ, о котором пойдет речь в статье.

В Caché есть возможность включать в код класса блоки XML данных – XDATA блоки. Обычно эти данные используются для хранения вместе с классом данных о формах Zen-страниц и Zen-отчетов. В этих блоках можно также прекрасно хранить начальные данные для персистентных классов.

Десериализация из XData

Рассмотрим пример простого хранимого класса с одним свойством. Это будет класс регионов с названиями – типичный пример справочника.

Код этого класса в Caché выглядит следующим образом:

Class map.Region Extends %Persistent
{
/// Название
Property 
Name As %String;
}

Добавим в класс начальные данные в XML-виде в блок XData:

XData populate
{
<
xml>
  <
item>
    <
Name>Красноярский край</Name>
  </
item>
  <
item>
    <
Name>Свердловская область</Name>
  </
item>
  <
item>
    <
Name>Хабаровский край</Name>
  </
item>
</
xml>
}

Для того, чтобы загрузить эти данные из класса, добавляем метод класса Populate. Кроме того, для работы с XML-данными необходимо класс сделать XML-enabled – добавляем в список наследования также класс %XML.Adaptor. В итоге преобразованный код класса выглядит следующим образом:

Class map.Region Extends (%Persistent%XML.Adaptor)
{

/// Название
Property 
Name As %String;

ClassMethod Populate() As %Status
{
  
#dim sc As %Status $$$OK
  
// очистка существующих данных класса
  
sc=..%DeleteExtent()  
  
if $$$ISERR(sc) quit sc
  
  
// загрузка xml-данных блока XData из библиотеки скомпилированных классов  
  
#dim stream As %Stream.Object ##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1) _ "||" "populate").Data
  
// создание инстанса XML ридера
  
#dim reader As %XML.Reader ##class(%XML.Reader).%New()
  
// открытие в ридере поток xml 
  
set sc = reader.OpenStream(stream, "literal")
  
if $$$ISERR(sc) quit sc
  
// указание ридеру в каком элементе искать данные класса
  
do reader.Correlate("item", ..%ClassName(1))
  
  
#dim obj as %Persistent
  
// загрука в цикле элементов класса
  
while reader.Next(.obj, .sc)
  
{
    
if $$$ISERR(sc) quit
    
// сохранение прочитанного из xml инстанс в базу данных
    
set sc = obj.%Save()
    
if $$$ISERR(sc) quit
    
    set 
obj = ""
  
}
  
  
quit sc
}

XData populate
{
<
xml>
  <
item>
    <
Name>Красноярский край</Name>
  </
item>
  <
item>
    <
Name>Свердловская область</Name>
  </
item>
  <
item>
    <
Name>Хабаровский край</Name>
  </
item>
</
xml>
}

}

Как видно из кода, всю работу выполняет класс %XML.Reader, который позволяет вычитывать “объектные” данные из XML.

Для загрузки данных в класс при деплойменте достаточно выполнить метод класса Populate:
##class(map.Region).Populate()

Убедимся, что инстансы класса действительно созданы. Выполним запрос в SQL-шелле терминала:

XMLDEPLOY>d $System.SQL.Shell()
SQL Command Line Shell
----------------------------------------------------
 
The command prefix is currently set to: <<nothing>>.
Enter q to quit, ? for help.
XMLDEPLOY>>select * from map.Region
1.      select * from map.Region
 
ID      Name
1       Красноярский край
2       Свердловская область
3       Хабаровский край
 
3 Rows(s) Affected
statement prepare time: 0.9125s, elapsed execute time: 0.0687s.
---------------------------------------------------------------------------
XMLDEPLOY>>quit
 
XMLDEPLOY>

Заполнение XData

Очевидно, что блок XData можно заполнить “вручную”. И это удобно, если объектов в классе мало. Но если их много, это может сделать программа.
Воспользуемся классом %XML.Writer, который позволяет сериализовать в XML инстансы класса.
Для этого добавляем в класс метод сериализации:

ClassMethod SerializeToFile(file as %Stringas %Status
{
  
#dim sc as %Status
  
#dim wr as %XML.Writer
  
set wr=##class(%XML.Writer).%New()
  
// установка файла, как устройство вывода
  
set sc=wr.OutputToFile(file) if $$$ISERR(sc) quit sc
  
// открытие корневого тега
  
set sc=wr.RootElement("xml"if $$$ISERR(sc) quit sc
  
#dim rset as %ResultSet
  
// выполнение запроса Extent, содержащего все объекты класса
  
set rset = ##class(%ResultSet).%New("map.Region:Extent")
   
do rset.Execute()
  
#dim obj
   
while (rset.Next()) {
     
set obj=##class(map.Region).%OpenId(rset.Data("ID"))
     
// сериализация объекта
     
set sc=wr.Object(obj)
     
if $$$ISERR(sc) quit
  
}
  
if $$$ISERR(sc) quit sc
  
// закрытие корневого тега
  
set sc=wr.EndRootElement() 
  
quit sc
}

Метод выводит в файл все объекты класса, сериализованные в XML.
Выполним метод в терминале:

XMLDEPLOY>D $System.OBJ.DisplayError(##class(map.Region).SerializeToFile("C:\cache\region.xml"))

XMLDEPLOY>

А затем откроем файл любым редактором/вьюером:
image
Полученный XML уже нетрудно вставить в блок XData.

Итого

Аналогичным образом можно снабдить подобными методами все классы, требующие ввод начальных данных при деплойменте. В итоге, с помощью приведенной техники начальные данные хранятся вместе с классом, для которого они нужны, в технологичном XML-формате. Клиенту можно поставлять только код классов, а генерацию начальных данных выполнять с помощью метода Populate. А что делать, если классы связаны друг с другом? Этот и другие сценарии рассмотрим во второй части статьи. Продолжение следует…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *