воскресенье, 10 апреля 2016 г.

Cache': Как сделать select from dual

В некоторых случаях, при использовании хранимых процедур, ну очень хочется иметь механизм аналогичный псевдотаблице dual из Oracle. Как сделать то же самое в Cache?
Dual - это условная псевдотаблица, к которой можно обратиться с операцией select и всегда получить одну строку. Операции update, delete и insert к ней не применимы (генерируют ошибку) или не имеют эффекта. Используется в операции обращения к хранимой процедуре, которая возвращает результат, чтобы использовать его как результат select. В штатные средства Cache, насколько я знаю, такой механизм не входит. Попробуем его выполнить самостоятельно.

Основная идея - это создать таблицу средствами cache, чтобы она принималась, как самая обычная, но при этом операции delete, insert и update ни к чему бы не приводили, а операция select всегда возвращала одну строку. Содержание этой строки неважно, пусть будет ID, который создается в Cache для любой таблицы автоматически.

Также, хотелось бы, добиться качественного решения, чтобы никакие системные администраторы или тестеры или пользователи не смогли повлиять на работоспособность таблицы ни по забывчивости, ни по ошибке. Основные требования:
  • после импорта и компиляции класса псевдотаблица должна быть работоспособна
  • при работе процессов, которые ее используют, не должно быть взаимных конфликтов
  • из таблицы нужно доставать ровно одну запись, независимо от действий различных процессов
В качестве основы решения, я выбрал идею из "Cache': Где наши данные?". И, в качестве одного из вариантов, решил использовать перенаправление операций set, kill и $order() на локальную переменную так, чтобы можно было получить лишь одну строку, а результаты set и kill на выборку не влияли.

Я экспериментировал с каше версии 5.2.3 и 5.0.15. В обеих версиях вариант получился работоспособным. Вот такой вот класс, как он выглядит в Cache Studio при включенной опции "показывать способ хранения":
Class User.Dual Extends %Persistent [ ClassType = persistent, Not
  ProcedureBlock, SqlTableName = DUAL ]
{

ClassMethod storage() As %String
{
 k zzzzdual
 s zzzzdual(1,0)=$lb("User.Dual")
 q 1
}

<Storage name="Default">
  <Data name="DualDefaultData">
    <Value name="1">
      <Value>%%CLASSNAME</Value>
    </Value>
  </Data>
  <DataLocation>zzzzdual($$zstorage^User.Dual.1())</DataLocation>
  <DefaultData>DualDefaultData</DefaultData>
  <IdLocation>zzzzdual($$zstorage^User.Dual.1())</IdLocation>
  <IndexLocation>^User.DualI</IndexLocation>
  <StreamLocation>^User.DualS</StreamLocation>
  <Type>%Library.CacheStorage</Type>
</Storage>
}
Здесь не используются ни одно свойство, ни один индекс, или потоковое свойство, поэтому, указание в способе хранения опций IndexLocation и StreamLocation - чистая формальность.

Идея такой организации состоит в том, что при обращении за данными мы вынуждаем движок каше вызвать сначала нашу функцию (storage), и использовать переменную, которой она оперирует. Сначала переменная очищается, потом создается структура, изображающая одну запись. Чтобы была вызвана наша функция, указываем первым индексом, что вызвать. Поэтому, хранение данных каше ожидает в виде подиндексов переменной zzzzdual(нечтонаше), например
zzzzdual("какая-то строка",ID1)=первая запись
zzzzdual("какая-то строка",ID2)=вторая запись
zzzzdual("какая-то строка",ID3)=третья запись
В моем варианте, в переменной zzzzdual в качестве "какая-то строка" используется строка "1", а в качестве ID строка "0". Ее-то и получит select.

Даже, если какие-то механизмы каше смогут создать ид новой записи (хотя мы можем вмешаться и в этот процесс) и дописать еще один узел, то мы, все равно, не дадим шанса прочитать ничего, кроме одной записи, а также, если будет произведено удаление, даже, всех локальных переменных, мы все равно снова восстановим структуру, по которой пройдется $order() при операции select.

Основная критика решения касается выражения zstorage^User.Dual.1. Здесь использованы не очень-то документированные соглашения каше:
  1. Если есть метод Method, написанный программистом, то в int коде ему соответствует метка zMethod.
  2. int код размещается в рутинах с именем, производным от имени класса.
В моем случае, я просто смотрю порождаемый int код, и рутина называется ^User.Dual.1
, значит метка - это zstorage^User.Dual.1. Здесь можно пойти по более аккуратному варианту - использовать вызов метода класса по штатным соглашениям:
  • zzzzdual(##class(User.Dual).storage()) - вызов через классовый синтаксис
  • zzzzdual($zobjclassmethod("User.Dual","storage")) - вызов через $zobj функцию
В версии 5.0.15 успешно работают все три варианта, а в силу расположенности к более прямым решениям, я себе оставляю первый, вызов через $$. Результат проверяется:
USER>w $zv
Cache for Windows NT (Intel/P4) 5.0.15
USER>d $System.SQL.Shell()
 
>>select * from dual
(1)>>go
0
>>exit
USER>
Теперь, можно спокойно обращаться к хранимым процедурам в средах, которые не очень хорошо к ним относятся (например путают call и execute). Достаточно написать
select stored_proc_name(params) from dual
Более того, поскольку используется форма обращения через select, возможно строить view, в том числе использующие хранимые процедуры:
create view dual2 as 
 select * from dual
После компиляции класса я получил MAC код (User.Dual.T1.MAC) с такими метками:
%delete(%rowid,%check) 
  s SQLCODE=-115,%msg="Cannot DELETE from a Read-Only table"
  q
%insert(%d,%check) 
  s SQLCODE=-115,%msg="Cannot INSERT into a Read-Only table"
  q ""
%update(%rowid,%check,%d) 
  s SQLCODE=-115,%msg="Cannot UPDATE a Read-Only table"
  q
Мне, конечно, приятно, что компилятор классов прочитал мои мысли и решил запретить модификацию данных в таблице, но почему он это сделал на самом деле, я еще не знаю.

Комментариев нет:

Отправить комментарий