Думаю, не преувеличением будет сказать, что почти каждый разработчик информационной системы сталкивается с задачей формирования начальных данных при внедрении.
У 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
// очистка существующих данных класса
s 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:
w ##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 %String) as %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:cacheregion.xml"))
XMLDEPLOY>
А затем откроем файл любым редактором/вьювером:
Полученный XML уже нетрудно вставить в блок XData.
Итого
Аналогичным образом можно снабдить подобными методами все классы, требующие ввод начальных данных при деплойменте. В итоге, с помощью приведенной техники начальные данные хранятся вместе с классом, для которого они нужны, в технологичном XML-формате. Клиенту можно поставлять только код классов, а генерацию начальных данных выполнять с помощью метода Populate. А что делать, если классы связаны друг с другом? Этот и другие сценарии рассмотрим во второй части статьи. Продолжение следует…
Автор: intersystems