Oracle Forms. Реализация LOV для выбора нескольких записей


Утилита MSLOV была написана в августе 2001 года. Мне кажется она имеет право на существование. Похожая реализация (Multi Select T-List) на OTN использует PJC (Pluggable Java Components) и, соответственно, будет работать далеко не на всех версиях и конфигурациях Forms.
Описание утилиты построено в виде вопросов-ответов.

Q1. О чём собственно речь

Разработчикам Oracle Forms хорошо известны такие объекты форм как списки значений (LOV). С их помощью можно легко и быстро реализовать выбор нужной записи из динамически построенного списка. Список этот как правило основывается на команде SELECT, хотя может быть построен и "вручную".

Однако бывают случаи, когда нужно выбрать не одну, а сразу несколько записей из списка значений. Например, в форме управления заказами (смотри форму ORD_DEMO), построенной на до боли знакомых таблицах ORD и ITEM, было бы неплохо для заполнения детального блока (ITEM) вызвать список значений, построенный на таблице PRODUCT, и дать пользователю возможность выбрать сразу несколько товаров одновременно. И затем, для каждого выбранного товара создать запись в блоке ITEM. К сожалению, возможности встроенных списков значений не позволяют это сделать. И если Вы все-таки намерены реализовать такую возможность, то придется немало потрудиться.

Но что Вы скажете если всей этой функциональности можно добиться при помощи следующего триггера KEY-LISTVAL:

   IF fp_mslov.show_lov (
         label_in       => 'descrip',
         id_in          => 'prodid',
         from_clause_in => 'FROM product ORDER BY descrip',
         col1_in        => 'descrip',
         title_in       => 'Select Products'
      )
   THEN
      fp_mslov.add_selected_records ('ITEM.prodid', 'ITEM.descrip');
   END IF;

Если Вам нравится, то эта статья для Вас.

Итак, речь пойдет об утилите, предназначенной для реализации списков, предоставляющих пользователю возможность выбора нескольких записей одновременно.

Несмотря на то, что разработка велась на Oracle Forms 6i, я думаю что утилита должна работать и на более ранних версиях. По крайней мере в ней не используется ничего, что не могло бы работать в Oracle Forms 5.0. И совершенно точно она будет работать на Forms9i. Для этого требуется лишь перекомпиляция модулей. Для работы в web (да и в клиент-сервер) вероятно потребуется изменить размеры, цвета, шрифты, но на вкус и цвет... сами понимаете, компромисс найти сложно.

Q2. А что там внутри

Этот раздел предназначен для тех кто уже не новичек в Forms. Если же Вы недавно начали работать с Forms или не хотите вдаваться в подробности реализации, а хотите просто использовать MSLOV, то Вам нужно сюда.
Если же Вы считаете себя специалистом по Forms, но все равно в этом разделе что-то не понятно, то вероятно я плохо сумел это объяснить. Попробуйте задать вопрос.

С точки зрения интерфейса пользователя, утилита должна выводить на экран окошко с полями, кнопками и т.д.
Oracle Forms предлагает два способа создания такого рода компонент:

Я предпочитаю второй способ. Во-первых, мне пришлось очень много и долго работать с Forms 3.0, где другого способа и не было. Во-вторых, использование отдельного модуля формы имеет то преимущество, что в случае изменений в нём, достаточно просто заменить старую версию формы на новую. Если же использовать Группу Объектов, то при любом изменении членов Группы потребуется перекомпиляция всех форм, на эту Группу ссылающихся.

Теперь, с точки зрения того, что пользователь должен увидеть на экране и как он должен сделать выбор. Опять же, есть два основных способа реализовать выбор нескольких значений:

Опять же, я предпочёл второй вариант, хотя никаких его особых преимуществ не вижу. Точно также можно было бы реализовать и первый. А самое лучшее решение, которое возможно будет реализовано, это сделать поддержку обоих стилей и тогда разработчик сам сможет выбрать то, что ему больше нравится.

Итак, с внешним видом определились, это отдельный модуль формы (MSLOV.fmb). Со стилем тоже определились. Теперь нужно определиться как эта форма узнает что предложить на выбор пользователю(чем заполнить левый список) и как/куда вернуть сделанный им выбор(правый список).

В подавляющем большинстве случаев, LOV-ы строятся на результатах запроса. Значит было бы не плохо передавать в форму параметром команду SELECT и пусть эта форма сама этот запрос и выполняет. Т.е. нам нужно чтобы наш левый список представлял из себя отдельный блок, причем связанный с базой данных(Database Data Block=Yes), да так чтобы выполнение EXECUTE_QUERY для этого блока выполняло переданный параметром в форму запрос(по сути произвольный запрос).

Почему это было бы не плохо? Да потому, что выполнение запроса в блоке имеет важное преимущество перед стандартными LOV-ами или заполнением блока "вручную". Мы по умолчанию начинаем использовать буфферизированную выборку данных на клиента(свойство блока Number of Records Buffered). Такой подход позволит легко реализовать выбор из запросов, возвращающих очень много записей. Записи будут подгружаться на клиента только по мере необходимости, а не все сразу. Стандартный LOV, в свою очередь, всегда "вытаскивает" на клиента все записи запроса.

Вопрос в том, как добиться выполнения в блоке произвольного запроса. Мы можем, конечно, поставить свойство блока 'Query Data Source Type' в значение 'FROM clause query' и можем программно менять свойство блока 'Query Data Source Name', но Forms не позволяет программно добавлять новые элементы в блок и менять названия уже существующим. Поэтому для реализации задуманного требуется наложить ряд ограничений на "произвольность" запроса. Точнее, это ограничения на количество и названия столбцов в списке SELECT. Мы должны заранее знать сколько их и как они называются. В предложенной реализации столбцов восемь:

Ну а окончательный вид запроса, выполняемого в блоке, будет выглядеть так:

   '(SELECT rownum, MSLOV.* FROM (' || sql_in || ') MSLOV)'

Где sql_in - переданный в форму параметром запрос.
Т.о. столбец ROWNUM мы формируем сами, а остальные столбцы должны правильно быть сформированы перед вызовом формы. При этом LABEL и ID - столбцы обязательные, без них вообще нет смысла вызывать форму, а COLX - необязательные. Остальная часть команды SELECT, начиная с FROM может формироваться пользователем как угодно.

С возвращаемым набором записей - проще. Мы можем использовать глобальные группы записей(группы созданные командой 'CREATE_GROUP (group_name, GLOBAL_SCOPE);'). Такие группы, как и глобальные переменные действуют на протяжении всего сеанса Forms Runtime и могут использоваться для обмена группами записей между формами.

Для того чтобы разработчику не ошибиться при формировании запроса и для того чтобы не вдаваться в тонкости работы с группами записей нужно создать удобное API для работы с формой и возвращаемым значением. Этим API стал пакет FP_MSLOV из PL/SQL библиотеки MSLOV.pll

Q3. Предположим меня это заинтересовало, как пользоваться

Впервую очередь нужно присоединить библиотеку MSLOV.pll к Вашей форме и обеспечить доступ к MSLOV.fmx. Т.е. эта форма должна находится либо в текущем каталоге, либо в каталоге из списка FORMS60_PATH. (ВНИМАНИЕ. Форма MSLOV предлагается в исходных кодах, в виде FMB файла, поэтому перед использованием скомпилируйте исполняемый FMX файл)

Теперь Вы готовы к использованию MSLOV.
Первой вызывается функция SHOW_LOV. Это основная подпрограмма пакета, которая собственно и вызывает форму выбора. Название функции и ее возвращаемое значение в точности соответствуют встроенной функции SHOW_LOV. Т.е. FP_MSLOV.show_lov вернет Истину если пользователь сделал выбор и Ложь в противном случае.

Для вызова функции Show_Lov необходимо сформировать список входных параметров. Функция имеет большое количество параметров, многие из которых имеют значения по умолчанию и при вызове могут не указываться. Поэтому предпочтительной формой передачи параметров является передача параметров по имени (param => value).

Для того чтобы лучше понять назначение отдельных параметров, рассмотрим принцип работы утилиты.
Список возможных значений для выбора строится на основе команды SELECT, на формирование которой налагается ряд ограничений. Точнее ограничения налагаются на список столбцов, расположенных между ключевыми словами SELECT и FROM. Этот список всегда должен начинаться со столбцов с алиасами LABEL и ID. Это сделано по аналогии с элементами типа LIST.
Т.е. первый столбец (LABEL) будет показан пользователю на экране, второй столбец (ID) предназначен для хранения и возврата связанного с LABEL значения.

Т.о. в минимальном виде, запрос для построения LOV будет выглядеть так:

   SELECT label_in AS LABEL,
          id_in AS ID
     FROM table_name

Для того чтобы не передавать весь текст запроса одним параметром и соответственно не писать "разборщик" запроса, в функции SHOW_LOV есть несколько параметров из которых и будет "сложена" окончательная команда SELECT. В общем виде разбивка на параметры выглядет так: по отдельному параметру на каждый столбец и один параметр на 'FROM...' и всё остальное.

Теперь можно перейти к описанию параметров функции SHOW_LOV
Параметр Описание
LABEL_IN Столбец или выражение, которое будет использовано как LABEL для отображения на экране. Алиас столбца 'AS LABEL' добавлять не нужно, т.к. он будет подставлен автоматически.
ID_IN Столбец или выражение, которое будет использовано в качестве возвращаемого значения. Алиас столбца 'AS ID' не нужен.
FROM_CLAUSE_IN Часть команды SELECT начиная с фразы FROM. Должна начинаться с ключевого слова 'FROM' и может включать необязательные фразы WHERE, GROUP BY, ORDER BY (начиная с 8i), START WITH .. CONNECT BY и т.д.
COL1_IN
COL2_IN
COL3_IN
COL4_IN
COL5_IN
При работе со списками значений часто бывает необходимо вернуть для выбранной записи более одного столбца. Для реализации такой потребности в функции Show_Lov можно определить помимо ID еще до пяти возвращаемых столбцов. Для этого служат параметры col1_in, col2_in,..., col5_in.
В этих необязательных параметрах могут быть описаны дополнительные столбцы или выражения.
TITLE_IN Определяет заголовок окна выбора значений

Итак, если Show_Lov вернула Истину, то это значит, что во внутренних структурах (глобальная группа записей) сформировался массив выбранных значений.

Для получения доступа к этим значениям предназначены следующие подпрограммы:
Подпрограмма Описание
ROWCOUNT Функция, возвращает количество выбранных записей
GET_ROW Функция, возвращает информацию об одной выбранной записи, по указанному параметром номеру. Допустимые значения для параметра от 1 до ROWCOUNT
CLEAR Производит очистку памяти, отведенной под хранение выбранных значений
ADD_SELECTED_RECORDS Для каждой записи выбранной пользователем, создает в текущем блоке новую запись и заполняет указанные параметрами поля. По окончании, освобождает память.

Примеры

Сценарий 1.
В форме управления сотрудниками (EMP_DEMO) мы хотим предоставить пользователю возможность поиска сотрудников в заданных отделах. Например, показать всех сотрудников 10 и 20 отделов. "Продвинутый" пользователь, конечно, может в режиме ввода запроса ввести '# IN (10, 20)' в поле DEPTNO блока EMP, но для этого нужно знать не только правила построения такого запроса, но и номера отделов.

Мы бы хотели чтобы пользователь вызвал список значений, в котором перечислены названия(DNAME) и месторасположения отделов(LOC), выбрал бы нужные отделы, а мы бы уже сформировали значение поле DEPTNO на основе сделанного выбора.

Нужная нам команда SELECT выглядет так:

   SELECT dname ||' (' || loc || ')' AS LABEL,
          deptno AS ID
     FROM dept
    ORDER BY dname

Ну а примерный вид триггера KEY-LISTVAL, вызывающего MSLOV в режиме ввода запроса, будет таким:

DECLARE
   -- Тип FP_MSLOV.OUT_RECORD_TYPE является записью,
   -- именно эту запись и возвращает функция GET_ROW.
   row_v fp_mslov.out_record_type;

   -- Здесь будет формироваться значение поля 'EMP.DEPTNO'
   deptno_v VARCHAR2(2000);
BEGIN
   -- Вызываем MSLOV только в режиме ввода запроса
   IF :SYSTEM.MODE = 'ENTER-QUERY'
   THEN
      -- Вот в такой вызов MSLOV трансформировался вышеприведенный запрос
      IF fp_mslov.show_lov (
            label_in       => 'dname ||'' (''||loc||'')''',
            id_in          => 'deptno',
            from_clause_in => 'FROM dept ORDER BY dname',
            title_in       => 'Select Departments'
         )
      THEN
         deptno_v := '# IN (';

	 -- Для каждой выбранной записи
         FOR i IN 1 .. fp_mslov.rowcount
         LOOP
	    -- Получаем очередную запись
            row_v := fp_mslov.get_row (i);

	    -- Считываем значение поля ID 
            deptno_v := deptno_v || row_v.id;
            IF i < fp_mslov.rowcount
            THEN
               deptno_v := deptno_v || ',';
            END IF;
         END LOOP;
         deptno_v := deptno_v || ')';

	 -- Освобождаем память, связанную с MSLOV
         fp_mslov.clear;

         COPY(deptno_v, 'EMP.deptno');
      END IF;
   END IF;
END;

Сценарий 2.
В форме ввода заказов (ORD_DEMO) мы хотим упростить пользователю заполнение детального блока(ITEM) с информацией о заказываемых товаров. Пользователь должен иметь возможность вызвать список имеющихся товаров, выбрать нужные и в соответствии с этим выбором, в блоке ITEM должны появиться новые записи о каждом выбранном товаре. Причем из списка значений нам нужно получить не только код товара(PRODID), но и описание(DESCRIP) и стандартную цену из таблицы PRICE.

Примерный вид PL/SQL кода, реализующего это с использованием MSLOV:

DECLARE
   from_clause_v  VARCHAR2(32000) := 
      'FROM product a, price b 
      WHERE a.prodid = b.prodid
        AND (b.prodid, NVL(b.enddate, TO_DATE(''9999'',''YYYY''))) IN 
            (SELECT prodid, MAX(NVL(enddate, TO_DATE(''9999'',''YYYY'')))
               FROM price
	      GROUP BY prodid)
      ORDER BY a.descrip';
BEGIN
   IF fp_mslov.show_lov (
         label_in       => 'a.descrip',
         id_in          => 'a.prodid',
         from_clause_in => from_clause_v,
         col1_in        => 'a.descrip',
         col2_in        => 'b.stdprice',
         title_in       => 'Select Products'
      )
   THEN
      -- Для каждого выбранного товара, будет создана запись
      -- в блоке ITEM.
      -- Выбранное значение ID будет присвоено в 'ITEM.prodid'
      -- Выбранное значение COL1_IN будет присвоено в 'ITEM.descrip'
      -- Выбранное значение COL2_IN будет присвоено в 'ITEM.actualprice'
      fp_mslov.add_selected_records (
         'ITEM.prodid', 'ITEM.descrip', 'ITEM.actualprice'
      );
   END IF;
END;

Важное замечание относительно демо форм.
Приведенные формы ORD_DEMO и EMP_DEMO имеют очень существенный минус в реализации. В них не используются связанные(bind) переменные. Например, в форме EMP_DEMO значение поля DEPTNO формируется в виде '# IN (10,40)'. Т.е. с жестко зашитыми номерами отделов. Неиспользование связанных переменных существенно сказывается на произволительности приложения, но эта тема требует отдельного разговора и, самое главное, дополнительного программирования, которое может "заслонить" собой использование MSLOV.

Q4. Ну и где эта хвалёная утилитка

Утилита состоит из нескольких файлов, лежащих в архиве mslov.zip
Ниже дано описание каждого из них:
Файл Описание
DEMOBLD.SQL Демонстрационные формы, описанные ниже, требуют для своей работы стандартные оракловские демо таблицы: DEPT, EMP, ORD, ITEM, PRODUCT. Если у вас нет этих таблиц, то можете использовать этот скрипт для их создания.
ORD_DEMO.fmb Демонстрационная форма ввода заказов. Стоя на любом заказе, нажмите на кнопку 'Select Products'. Выбранные товары будут занесены в детальный блок формы.
EMP_DEMO.fmb Демонстрационная форма управления сотрудниками. MSLOV используется при вводе запроса, для выбора нескольких отделов одновременно. Следуйте краткой инструкции на экране. Она хоть и на английском, но думаю будет понятна всем.
MSLOV.fmb Это именно та форма, которую Вы видите при вызове MSLOV
MSLOV.pll Это именно та PL/SQL библиотека, в которой находится пакет FP_MSLOV

Q5. У меня есть вопрос, куда обращаться

Если у Вас появился вопрос, по использованию MSLOV или Вы обнаружили ошибки, или хотите предложить улучшения/дополнения, да в конце концов, просто хотите что-то сказать, то пишите мне или оставте сообщение в гостевой книге


22 августа 2002г.

Hosted by www.Geocities.ws

1