ROZWIĄZANIA Raportowanie w aplikacji postępu wykonywania procedur składowanych w bazie danych z wykorzystaniem pakietu DBMS_PIPE

Operacjom wykonywanym na bazie danych, które trwają dłużej niż
3-4 sekundy, powinno towarzyszyć informowanie użytkownika o tym co się dzieje albo, że
w ogóle coś się dzieje. Przyjaznym rozwiązaniem w aplikacjach okienkowych jest w
takiej sytuacji wyświetlanie tzw. paska postępu, który pokazuje procentowo (od 0% do
100%) stan wykonania operacji. Artykuł ten omawia sposób realizacji paska postępu w
odniesieniu do aplikacji robionej w Developerze/2000, zarówno w przypadku procedur
uruchamianych po stronie klienta (jednostka programowa w Oracle Forms), jak i procedur
składowanych w bazie danych i uruchamianych na serwerze. Do raportowania postępu
procedur wykonywanych na serwerze został wykorzystany mechanizm potoków z pakietu
DBMS_PIPE.

Przykładowa procedura

Operacje, których wykonanie zajmuje dużo czasu pojawiają się
oczywiście tam gdzie mamy do czynienia z dużą liczbą rekordów lub złożonym
schematem bazy danych i tym samym złożonym przetwarzaniem rekordów (choćby wiele
operacji połączenia). Gdybyśmy chcieli przeanalizować jakieś rzeczywiste rozwiązanie
to większą część artykułu zająłby pewnie opis schematu bazy danych i wykonywanej
operacji. Przyjmijmy więc na potrzeby tego artykułu, że mamy do wykonania stosunkowo
prostą operację z użyciem prostego schematu bazy danych. Niech schemat bazy danych
składa się z trzech następujących tabel:

TOWARY ( TOWAR_ID, NAZWA, CENA_SPRZED, UWAGI, GRUPA_ID )
GRUPY ( GRUPA_ID, NAZWA, SWW )
DOSTAWY ( TOWAR_ID, DOSTAWCA_ID, CENA_ZAKUPU, UWAGI )

Pierwsza tabela zawiera wszystkie sprzedawane towary, z których każdy jest
przypisany do pewnej grupy towarów. Grupy towarów opisane są w drugiej tabeli. W
trzeciej tabeli przechowujemy rekordy mówiące, które towary są dostarczane przez
których dostawców i w jakich cenach. Załóżmy teraz, że chcemy skorygować cenę
sprzedaży wszystkich towarów w taki sposób, że:

  1. Jeśli cena sprzedaży jest mniejsza niż najniższa cena zakupu powiększona o 15% to
    podnosimy ją do tego właśnie poziomu, ale tylko w przypadku gdy towar jest dostarczany
    przez mniej niż 5 dostawców.
  2. Jeśli dany towar jest dostarczany przez 5 lub więcej dostawców, to cenę sprzedaży
    towaru podniesiemy tylko o 12%. Taki przypadek skomentujemy odpowiednio w kolumnie UWAGI.

Nie sposób powiedzieć ile dokładnie czasu zajmie wykonanie takiej
operacji dla danej liczby towarów, gdyż zależy to od zbyt wielu czynników, ale jeśli
weźmiemy pod uwagę tysiąc towarów i kilkudziesięciu dostawców to czas wykonania na
pewno wymagać będzie raportowania postępu operacji.
Wykonanie takiej korekty cen towarów możemy zapisać w postaci poniższej procedury
PL/SQL-owej. Żeby miało to znamiona praktycznego rozwiązania to podane wyżej liczby
(15%, 5 i 12%) potraktujemy jako parametry wejściowe procedury.

PROCEDURE Korekta_Cen(
p_marza_standard number,
p_liczba_dostawc number,
p_marza_zmniejsz number) IS

cursor c_towary is select * from towary
for update of cena_sprzed, uwagi;
v_cena_zakupu dostawy.cena_zakupu%type;
v_liczba_dost number(3);

BEGIN
   for TOWAR in c_towary loop

   /***** znajdź najniższą cenę i liczbe dostawców *****/
   select min(cena_zakupu), count(*)
   into v_cena_zakupu, v_liczba_dost
   from dostawy
   where towar_id = TOWAR.towar_id;

   /***** dokonaj korekty ceny zgodnie z przyjętymi regułami *****/
   if v_liczba_dost < p_liczba_dostawc then
     if TOWAR.cena_sprzed < v_cena_zakupu*(1+p_marza_standard) then
     update towary
      set cena_sprzed = v_cena_zakupu*(1+p_marza_standard)
    where current of c_towary;
   end if;
  else
    if TOWAR.cena_sprzed < v_cena_zakupu*(1+p_marza_zmniejsz) then
    update towary
     set cena_sprzed = v_cena_zakupu*(1+p_marza_zmniejsz),
            uwagi = ‚Zmniejszona
marża’
     where current of c_towary;
   end if;
  end if;
end loop;
END;

Gdy umieścimy taką procedurę jako jednostkę programową w
formularzu to po jej wywołaniu, powiedzmy, za pomocą jakiegoś przycisku, jedynym
efektem widocznym dla użytkownika będzie zastygnięcie aplikacji w bezruchu na
kilkanaście lub kilkadziesiąt sekund, albo i parę minut. Naszym celem jest zatem
wyświetlanie postępu operacji w trakcie procesu korygowania cen.

Realizacja paska postępu

Zobaczmy teraz jak można wykonać pasek postępu opisanej wyżej
korekty cen w sytuacji kiedy procedura jest jednostką programową formularza i wykonuje
się po stronie klienta na stacji roboczej użytkownika. Do rozwiązania są tutaj dwa
zagadnienia: 1) graficzna prezentacja samego paska postępu i 2) jego uaktualnianie z
wnętrza procedury.
Zacznijmy od wykonania samego paska postępu, co można zapewne zrobić na wiele
sposobów. My zastosujemy bardzo prostą technikę, ale dającą dobry efekt wizualny.
Polega ona na tym, że nakładamy na siebie dwa pola wyświetlania posiadające różne
kolory i zmieniamy szerokość pola leżącego na wierzchu. Oprócz efektu wypełniania
paska wybranym kolorem możemy na takim pasku wyświetlać dodatkowe informacje, na
przykład o aktualnie przetwarzanej grupie towarów. Pasek można umieścić na osobnej
nakładanej kanwie wyświetlanej w tym samym okienku lub w osobnym okienku typu dialog.
Pasek wyświetlany jako nakładana kanwa może wyglądać tak jak pokazano na
rysunku 1.

Rysunek 1. Pasek postępu wykonany z użyciem dwóch pól wyświetlania

Przyjmijmy, że nazwy obiektów składających się na taki pasek są
następujące: nazwa pola pod spodem – PASEK_DOZROBIENIA, nazwa pola na wierzchu –
PASEK_ZROBIONE, nazwa kanwy – PASEK. Szerokość pola PASEK_DOZROBIENIA będzie
reprezentować stan zaawansowania 100%, a PASEK_ZROBIONE będzie zmieniał swą
szerokość w miarę postępu operacji. Ewentualne napisy (np. nazwa aktualnie
przetwarzanej grupy towarów) będziemy wprowadzać jednocześnie do obu pól, aby
uzyskać efekt zmiany koloru niezależnie od wyświetlanego napisu.
Drugim problemem do rozwiązania jest aktualizacja paska z wnętrza procedury. Aby móc
wyrazić w procentach stan zaawansowania operacji musimy znać liczbę wszystkich
rekordów do przetworzenia i liczyć ile rekordów zostało już przetworzonych. Na
podstawie tych dwóch wartości będziemy ustawiać szerokość paska, czyli pola
PASEK_ZROBIONE. Ustawianie szerokości paska w oparciu o te dwie liczby możemy zawrzeć w
osobnej procedurze:

PROCEDURE Zmien_szerokosc_paska(
  p_zrobione number,
  p_dozrobienia number default 100) IS
v_szer_100proc number;

BEGIN
  if p_zrobione=0 or p_dozrobienia=0 then
    set_item_property(, WIDTH, 0);
  else
    v_szer_100proc := get_item_property(,WIDTH);
   set_item_property(, WIDTH,
    v_szer_100proc * p_zrobione / p_dozrobienia);
  end if;
  synchronize;
END;

Pozostaje tylko dodać do procedury Korekta_Cen wyznaczenie liczby
wszystkich rekordów i wywołanie procedury Zmien_szerokosc_paska. Procedura przybierze
wówczas postać:

PROCEDURE Korekta_Cen( … ) IS
  cursor c_towary is
    select towar_id, g.nazwa nazwa_grupy, cena_sprzed
    from towary t, grupy g
    where t.grupa_id = g.grupa_id
    order by g.nazwa
    for update of cena_sprzed, uwagi;
  …
  v_dozrobienia number;
  v_zrobione number := 0;
BEGIN
  /****** policz ile towarów jest do przetworzenia ******/
  select count(*) into v_dozrobienia from towary;

  for TOWAR in c_towary loop
  /***** znajdź najniższą cenę i liczbe dostawców *****/
  …
  /***** dokonaj korekty ceny zgodnie z przyjętymi regułami *****/
  …
  /***** dokonaj ewentualnej korekty paska postępu *****/
  v_zrobione := v_zrobione + 1;
    :PASEK_DOZROBIENIA := ‚ Aktualnie: ‚||TOWAR.nazwa_grupy;
    :PASEK_ZROBIONE := ‚ Aktualnie: ‚||TOWAR.nazwa_grupy;
    Zmien_szerokosc_paska(v_zrobione, v_dozrobienia);

  end loop;
END;

Opcjonalnie, oprócz zmiany szerokości paska, można uatrakcyjnić
raportowanie postępu wyświetlając na pasku dodatkowe informacje. W powyższym
przykładzie wyświetlana jest nazwa grupy towarów. W tym celu w kursorze dodano
odczytywanie nazwy grupy towarów i według tego atrybutu zostały posortowane rekordy.
Przed rozpoczęciem operacji trzeba wyświetlić kanwę z paskiem postępu, a po jej
zakończeniu trzeba ją ukryć. Załóżmy, że dla przejrzystości i porządku zrobimy to
nie w procedurze Korekta_Cen, ale na zewnątrz. Wyświetlenie i ukrycie kanwy można
wykonać w wyzwalaczu, z którego wywoływana jest procedura (np. WHEN-BUTTON-PRESSED), za
pomocą poleceń:

show_view(‚PASEK’);
Zmien_szerokosc_paska(0);
Korekta_cen(:MARZA_STANDARD, :LICZBA_DOST,
:MARZA_ZMNIEJSZ);
hide_view(‚PASEK’);

Pasek jest gotowy. Można uruchomić formularz i sprawdzić jego
działanie.

Problem przetwarzania klient-serwer

Załóżmy teraz, że chcemy procedurę przenieść do bazy danych.
Może to być podyktowane chęcią zwiększenia efektywności wykonania operacji lub
współdzielenia procedury przez kilka różnych formularzy. Nie ma żadnego problemu z
przeniesieniem do bazy danych procedury w takim kształcie jak podano na samym początku,
czyli bez raportowania postępu wykonania operacji, ale procedury w nowym kształcie ze
zmianą szerokości paska postępu nie da się umieścić w bazie danych.
Procedura składowana w bazie danych i uruchamiana na serwerze nie może przecież w
żaden sposób odwołać się do pola w formularzu uruchomionym na stacji roboczej
użytkownika lub wywołać jednostki programowej umieszczonej w formularzu. Zatem, w jaki
sposób możemy pokazać na pasku postępu aktualny stan wykonania operacji na serwerze?
Aby było to możliwe, procedura, zamiast bezpośrednio modyfikować pasek postępu, musi
przekazywać aplikacji po stronie klienta informacje o stanie zaawansowania operacji, a
korektą paska postępu będzie się zajmować aplikacja. Można to zrobić z
wykorzystaniem jednego z dwóch pakietów: DBMS_OUTPUT lub DBMS_PIPE. Pakiet DBMS_PIPE
jest bardziej uniwersalnym narzędziem, więc ten pakiet został użyty i opisany w
poniższych przykładach.
Istnieje jeszcze jeden mały problem do rozwiązania. Gdy z formularza wywołamy
procedurę składowaną na serwerze to wykonanie kodu programu zostaje wstrzymane do
zakończenia wykonania procedury na serwerze, więc w tym czasie formularz, z którego
wywołamy procedurę nie może dokonywać zmiany szerokości paska. Rozwiązanie nasuwa
się samo: pasek postępu musi być wyświetlany przez drugi formularz pracujący
zupełnie niezależnie (uruchomiony w trybie asynchronicznym), w ramach odrębnej sesji, a
wywołanie formularza musi nastąpić przed wywołaniem procedury.
Przenieśmy zatem procedurę Korekta_Cen do bazy danych i zastosujmy opisane przed chwilą
rozwiązanie do wyświetlania postępu wykonania procedury. Na początek przyjrzyjmy się
bliżej jak działa komunikacja z wykorzystaniem DBMS_PIPE.

Pakiet DBMS_PIPE

Pakiet DBMS_PIPE jest bardzo ciekawym i użytecznym pakietem, choć
dosyć mało popularnym. Umożliwia on komunikację pomiędzy różnymi sesjami otwartymi
w ramach tej samej instancji bazy danych. Idea tej komunikacji polega na tym, że tworzymy
potok (pipe), do którego wysyłamy kolejne wiadomości. Wiadomości znajdujące się w
potoku mogą być po kolei odbierane przez inną transakcję, pracującą w ramach innej
sesji – tego samego lub zupełnie innego użytkownika.
Odpowiednie procedury i funkcje pakietu DBMS_PIPE pozwalają na: utworzenie potoku
(CREATE_PIPE), usunięcie potoku (REMOVE_PIPE), zbudowanie wiadomości do wysłania
(PACK_MESSAGE), wysłanie gotowej wiadomości do potoku (SEND_MESSAGE), odczytanie
wiadomości z potoku (RECEIVE_MESSAGE), odczytanie kolejnego elementu wiadomości
(UNPACK_MESSAGE) i jeszcze paru innych rzeczy. Procedury te zostaną za chwile omówione
nieco dokładniej.
Istnieją dwa rodzaje potoków: publiczne (public) i prywatne (private). Do potoku
publicznego może wysyłać swoje wiadomości wielu użytkowników jednocześnie – pod
warunkiem, że wiedzą jak się potok nazywa i mają odpowiedni przywilej (execute do
pakietu DBMS_PIPE). Tak samo, wielu użytkowników jednocześnie może czytać wiadomości
z publicznego potoku. W przeciwieństwie do potoku publicznego, prywatny potok może być
czytany tylko w ramach sesji tego samego użytkownika, który utworzył potok (może to
być inna sesja, ale ten sam użytkownik) lub przez uprzywilejowanych użytkowników: DBA
i INTERNAL.

Tworzenie potoku

Nowy potok możemy utworzyć za pomocą funkcji DBMS_PIPE.CREATE_PIPE.
Poniższy przykład pokazuje utworzenie potoku o nazwie PASEK:

wynik := dbms_pipe.create_pipe(‚PASEK’, 200, TRUE);

Drugi parametr określa pojemność (łączny rozmiar wiadomości)
potoku w bajtach, a TRUE oznacza, że ma to być potok prywatny. Wynik równy 0 oznacza
utworzenie potoku.

Budowanie wiadomości

Budowanie wiadomości do wysłania polega na kilkukrotnym wywołaniu
procedury DBMS_PIPE.PACK_MESSAGE. Jedno wywołanie odpowiada dodaniu do wiadomości
kolejnego elementu. Poniższy przykład pokazuje dodanie do wiadomości bieżącej daty i
godziny:

dbms_pipe.pack_message(sysdate);

Element może być łańcuchem znaków, liczbą lub datą. Dla każdego
z tych typów wywołanie procedury wygląda tak samo. Dla elementów typu RAW i ROWID
istnieją procedury o nieco innych nazwach: PACK_MESSAGE_RAW i PACK_MESSAGE_ROWID.
Zbudowana w ten sposób wiadomość może zawierać jeden lub wiele elementów różnych
typów. Odczytywanie wiadomości odbywa się analogicznie, tzn. poprzez odczytywanie
kolejnych jej elementów.
Po wysłaniu wiadomości, o czym mowa za chwilę, zanim zaczniemy budować nową
wiadomość do wysłania, musimy wyczyścić bufor wiadomości wołając procedurę
DBMS_PIPE.RESET_BUFFER.

Wysyłanie wiadomości

Wysłanie gotowej wiadomości realizuje się za pomocą funkcji
DBMS_PIPE.SEND_MESSAGE. Wysłanie wiadomości do potoku o nazwie PASEK wygląda
następująco:

wynik := dbms_pipe.send_message(‚PASEK’, 2);

Drugi parametr oznacza maksymalną liczbę sekund na wysłanie
wiadomości. Jeśli w tym czasie operacja się nie uda się to traktujemy to jako błąd.
Wynik równy 0 oznacza wysłanie. Funkcją SEND_MESSAGE możemy również w sposób
niejawny tworzyć nowe potoki, bez wywoływania CREATE_PIPE, ale tylko potoki publiczne.

Odbieranie wiadomości

Inna transakcja może odebrać wiadomość z potoku za pomocą funkcji
DBMS_PIPE.RECEIVE_MESSAGE. Odebrana wiadomość jest usuwana z potoku. Poniżej pokazano
odebranie wiadomości z potoku PASEK:

wynik := dbms_pipe.receive_message(‚PASEK’, 30);

Drugi parametr określa maksymalną liczbę sekund jaką będziemy
czekać na wiadomość, aż uznamy, że wystąpił błąd. Wynik równy 0 oznacza
odebranie wiadomości.

Czytanie wiadomości

Czytanie wiadomości, tak samo jak jej budowanie, polega na
wielokrotnym wywołaniu procedury, tym razem DBMS_PIPE.UNPACK_MESSAGE, aby odczytać
kolejne elementy składające się na wiadomość. Problem jaki w tym momencie napotykamy
polega na tym, że kolejne elementy mogą być zupełnie różnych typów, a sposób ich
czytania (przetwarzania) zależy od tego jakiego są typu. Problem rozwiązujemy z
użyciem funkcji DBMS_PIPE.NEXT_ITEM_TYPE, która informuje nas jaki będzie typ kolejnego
elementu wiadomości (12 – DATE, 9 – VARCHAR2, 6 – NUMBER, 0 – nie ma już
żadnych elementów). Odczyt elementu typu data może wyglądać następująco:

if dbms_pipe.next_item_type = 12 then
    dbms_pipe.unpack_message(godzina_zdarzenia);
end if;

Usuwanie potoku

Gdy potok nie będzie nam już potrzebny możemy go usunąć za pomocą
funkcji DBMS_PIPE.REMOVE_PIPE. Poniżej usuwamy potok PASEK:

wynik := dbms_pipe(‚PASEK’);

Wynik równy 0 oznacza usunięcie potoku.

Uwagi dotyczące DBMS_PIPE

Oprócz podstawowych operacji na potokach omówionych powyżej mamy do
dyspozycji jeszcze inne, np: PURGE – czyści zawartość potoku (usuwa wszystkie
nieodebrane wiadomości), które jednak nie będą wykorzystane przy realizacji naszego
paska postępu.
Bufor wykorzystywany do budowania i odbierania wiadomości jest wspólny dla wszystkich
wykorzystywanych potoków. Może być to przyczyną błędnego działania programu,
ponieważ w niezauważony sposób możemy wysłać wiadomość zawierającą jakieś stare
elementy i to z innego potoku. Nigdy nie zaszkodzi dla zabezpieczenia się przed tym
zjawiskiem wywoływać częściej (być może nadmiarowo) DBMS_PIPE.RESET_BUFFER.
Mechanizm potoków w Oracle działa zupełnie niezależnie od przetwarzanych transakcji. Z
tego względu trzeba stosować go z rozwagą, bo w przeciwnym razie mogą nas spotkać
niemiłe niespodzianki. Chodzi dokładnie o to, że operacja wykonana z użyciem potoku
(na podstawie odebranej wiadomości) nie może zostać wycofana. Jeśli taka operacja jest
integralną częścią transakcji to nie powinna być realizowana za pomocą potoku.

Procedura w bazie danych

Powróćmy do naszej procedury Korekta_cen. Jeśli chcemy ją
umieścić w bazie danych to musi mieć ona nieco inną postać od procedury w formularzu.
Zamiast wywoływania pomocniczej procedury Zmien_szerokosc_paska i wyświetlania nazwy
grupy towarów bezpośrednio w polu formularza musimy utworzyć potok, do którego
będziemy wysyłać wiadomości mówiące o tym, że trzeba coś zmienić na pasku
postępu.
Zmiany, które zachodzą na pasku postępu są dwojakie: 1) zmienia się szerokość paska
lub 2) zmienia się nazwa grupy towarów. Załóżmy, że wysłanie do potoku wartości
liczbowej będzie oznaczać chęć zmiany szerokości paska, a wysłanie łańcucha
znaków będzie oznaczać przejście do nowej grupy towarów i konieczność wyświetlenia
nazwy kolejnej grupy na pasku.

Procedura przybierze następującą postać:

PROCEDURE Korekta_Cen(…) IS

  v_zrobione number := 0;
  v_dozrobienia number;
  v_nazwa_grupy grupy.nazwa%type;
BEGIN
  /****** policz ile towarów jest do przetworzenia ******/
  select count(*) into v_dozrobienia from towary;

  /****** utwórz potok ******/
  if dbms_pipe.create_pipe(, 200, TRUE) <> 0 then
    raise_application_error(-20001, );
  end if;

  for TOWAR in c_towary loop
    /***** znajdź najniższą cenę i liczbe dostawców *****/

    /***** dokonaj korekty ceny zgodnie z przyjętymi regułami *****/

    /***** przygotuj wiadomość o postępie operacji *****/
    v_zrobione := v_zrobione + 1;
    dbms_pipe.reset_buffer;
    dbms_pipe.pack_message(v_zrobione);
    dbms_pipe.pack_message(v_dozrobienia);

  /***** sprawdź czy zmieniła się grupa towarów *****/
  if TOWAR.nazwa_grupy <> v_nazwa_grupy then
    dbms_pipe.pack_message(TOWAR.nazwa_grupy);
    v_nazwa_grupy := TOWAR.nazwa_grupy;
  end if;

  /***** wyślij wiadomość *****/
  if dbms_pipe.send_message(,5) <> 0 then
    raise_application_error(-20002, );
  end if;
end loop;
EXCEPTION
  when others then /***** wyślij komunikat aby schować pasek *****/
    dbms_pipe.reset_buffer
    dbms_pack_message();
    dbms_pipe.send_message(, 5);
END;

Nasza procedura wysyła już sygnały o tym jak jej idzie korygowanie
cen. Pozostaje „tylko” odbierać te sygnały po stronie klienta i na ich podstawie
korygować wyświetlany pasek postępu.

Asynchroniczny formularz z paskiem postępu

Jak już zostało zasygnalizowane, pasek postępu musi być
wyświetlany w osobnym formularzu, pracującym współbieżnie do macierzystego formularza
i procedury w bazie danych. Formularz ten będzie odpowiedzialny za odbieranie
komunikatów wysyłanych przez procedurę i na ich podstawie korygowanie paska postępu. Działanie
tego mechanizmu zostało schematycznie pokazane na rysunku 2.

W konstrukcji formularza wyświetlającego pasek postępu (załóżmy,
że właśnie tak się będzie nazywał: PASEK) należy uwzględnić kilka elementów.
Formularz powinien zawierać: jeden blok z polami PASEK_ZROBIONE i PASEK_DOZROBIENIA, oraz
kanwę z minimum jednym aktywnym elementem (w przeciwnym razie formularz zostanie
zamknięty zaraz po jego wywołaniu). Aktywnym elementem może być np. przycisk z napisem
Zamknij. Aby uzyskać zamierzony efekt, czyli żeby cały formularz wyglądał
podobnie jak małe okno dialogowe, musimy go pozbawić menu i paska narzędziowego.
Ustawmy w tym celu własności modułu: Moduł menu na wartość pustą, Okno konsoli
na <brak>, a własności okna, w którym jest wyświetlana kanwa: Zamykanie
dozwolone
, Zmiana rozmiaru dozwolona, Zmiana rozmiaru dozwolona, Maksymalizacja
dozwolona
i Minimalizacja dozwolona ustawmy na NIE.
Odpowiednie wyświetlenie formularza (pozycja, rozmiar, tytuł belki), aby wyglądał on
tak jak pokazano poniżej na rysunku 3, wymaga programowego ustawienia własności okna
MDI. Należy to zrobić najwcześniej jak się da, czyli w wyzwalaczu PRE-FORM.

PRE-FORM

set_window_property(FORMS_MDI_WINDOW, window_size, 270, 60);
set_window_property(FORMS_MDI_WINDOW, position, 160, 120 );
set_window_property(FORMS_MDI_WINDOW, title, );
set_window_property(,window_state,maximize);

Rysunek 3. Asynchroniczny formularz z paskiem postępu procedury wykonywanej na serwerze.

Pozostają nam jeszcze do zrobienia dwie rzeczy: 1) oprogramowanie w
formularzu PASEK odbierania wiadomości z potoku i odpowiednie ustawianie własności
paska oraz 2) odpowiednie wywołanie formularza PASEK z macierzystego formularza,
powiedzmy TOWARY, w którym użytkownik wydaje polecenie wykonania operacji, czyli
również w tym formularzu, z którego jest wywoływana procedura składowana w bazie
danych. Jak się okazuje, oba te punkty nie są takie proste na jakie wyglądają na
pierwszy rzut oka. Dlaczego? Pojawiają się następujące problemy:

  • Formularz PASEK musi być wywołany asynchronicznie, tak aby pracował współbieżnie
    do formularza TOWARY. Gdybyśmy wywołali go synchronicznie, np. za pomocą CALL_FORM, to
    sterowanie zostałoby przekazane do formularza PASEK i nie doszłoby do uruchomienia
    procedury na serwerze, zaś wywołanie CALL_FORM po zakończeniu procedury nie ma sensu.
  • Ustawianie pozycji i rozmiaru formularza PASEK jest robione programowo, gdyż nie da
    się inaczej, więc efektem nie do uniknięcia jest to, że przez krótką chwilę
    formularz pojawia się na domyślnej pozycji i z domyślnym rozmiarem, a dopiero potem
    robi się mały, ustawiony na środku ekranu. Problem polega na tym, że wykonywany
    współbieżnie formularz TOWARY dokonuje w międzyczasie wywołania procedury na serwerze
    i zawiesza swą aktywność dopóki procedura się nie zakończy. Nie jest wtedy w stanie
    dokonać odświeżenia obszaru swojego okna, który wyłania się po zmniejszeniu i
    przemieszczeniu formularza PASEK, a co za tym idzie większość obszaru okna TOWARY to po
    prostu biała plama.

Jeśli chodzi o asynchroniczne wywołanie formularza PASEK to
chciałbym w tym miejscu polecić inny artykuł mojego autorstwa „Synchroniczne i
asynchroniczne wywoływanie modułów i programów w Developer/2000” omawiający ten
problem w sposób kompleksowy. Artykuł ukaże się w następnym wydaniu Gazetki PLOUG. W
tym artykule ograniczę się do podania szczególnego, gotowego rozwiązania.
Wywołanie formularza PASEK z formularza TOWARY, które zapewnia od razu rozwiązanie
zasygnalizowanych wyżej problemów, możemy wykonać za pomocą dwóch wyzwalaczy: 1)
WHEN-BUTTON-PRESSED – zdefiniowanym na przycisku (ewentualnie jakiś inny) i 2)
WHEN-TRIMER-EXPIRED na poziomie formularza.

WHEN-BUTTON-PRESSED

DECLARE
  v_param_id paramlist;
  v_timer_id timer;
BEGIN
  v_timer_id := create_timer(, 3000, NO_REPEAT);
  run_product(FORMS, , ASYNCHRONOUS, RUNTIME,
            
FILESYSTEM, v_param_id, NULL);

END;

Pierwsze polecenie tworzy Timer, który uruchomi wyzwalacz
WHEN-TIMER-EXPIRED po upływie 3000 milisekund, czyli 3 sekund (na szybkich komputerach
czas ten można skrócić, a na wolnych być może trzeba będzie wydłużyć), a
następnie za pomocą RUN_PRODUCT uruchamiany jest w sposób asynchroniczny formularz
PASEK.

WHEN-TIMER-EXPIRED

redisplay;
Korekta_cen(:MARZA_STANDARD, :LICZBA_DOST, :MARZA_ZMNIEJSZ);

Po upływie trzech sekund, na wszelki wypadek wołana jest procedura
REDISPLAY (ewentualnie SYNCHRONIZE) aby odświeżyć zawartość okna po zmianie rozmiaru
formularza PASEK, a następnie wywoływana jest procedura Korekta_Cen składowana w bazie
danych.
Formularz PASEK, jak już pokazano wyżej, zawiera wyzwalacz PRE-FROM dokonujący m.in.
ustawienia pozycji i rozmiaru okna. Oprócz tego formularz zawiera dwa inne wyzwalacze:
WHEN-NEW-FORM-INSTANCE – inicjujący odświeżanie paska, WHEN-TIMER-EXPIRED –
czytający wiadomości z potoku i na ich podstawie odświeżający pasek postępu.

WHEN-NEW-FORM-INSTANCE

DECLARE
  v_timer_id timer;
BEGIN
  /**** ustaw pasek w pozycji początkowej *****/
  Zmien_szerokosc_paska(0);
  /**** uruchom za 0,1 sekundy timer *****/
  v_timer_id := create_timer(,100, NO_REPEAT);
END;

WHEN-TIMER-EXPIRED

DECLARE
  v_zrobione number;
  v_dozrobienia number;
  v_tekst varchar2(100);
  v_timer_id timer;
BEGIN
  /***** czekamy na wiadomość o postępie – maks. 30 sekund *****/
  if dbms_pipe.receive_message(, 30) <> 0 then
    if show_alert() = ALERT_BUTTON1 then
    exit_form;
    end if;
  end if;
  /***** odczytaj kolejne elementy wiadomości *****/
  if dbms_pipe.next_item_type = 6 then /* liczby – zmien szerokość */
    dbms_pipe.unpack_message(v_zrobione);
    dbms_pipe.unpack_message(v_dozrobienia);
    Zmien_szerokosc_paska(v_zrobione, v_dozrobienia);
  end if;
  if dbms_pipe.next_item_type = 9 then /* tekst */
    dbms_pipe.unpack_message(v_tekst);
    if  v_tekst = then
   exit_form; /* ukryj pasek */
  else
    :PASEK_ZROBIONE := v_tekst; /* nowa grupa towarów */
    :PASEK_DOZROBIENIA := v_tekst;
    end if;
  end if;
  /**** uruchom ponownie za 0,1 sekundy *****/
  v_timer_id := create_timer(, 200, NO_REPEAT);
END;

Każde kolejne wywołanie wyzwalacza rozpoczyna się od oczekiwania na
wiadomość od procedury. Zakładamy, że jeśli nie nadejdzie w ciągu 30 sekund to
znaczy, że coś jest nie tak. Oczywiście, w przypadku operacji trwających dziesiątki
minut i stosunkowo małej liczbie iteracji, czas ten powinien być znacznie większy.
Potokiem przekazywane są liczby oznaczające proporcję przetworzonych rekordów do
wszystkich rekordów, nazwy grup towarów i sygnał „Koniec!”. Pierwszych jest
najwięcej, od czasu do czasu pojawia się nazwa grupy, a raz przesyłany jest
„Koniec!”. W ogólności, każda kolejna wiadomość może mieć różną zawartość:

  • tylko dwie liczby,
  • dwie liczby i ciąg znaków z nazwą nowej grupy towarów,
  • ciąg znaków „Koniec!”.

Dlatego, analizujemy typy przekazanych w wiadomości elementów i w
zależności od tego wykonujemy odpowiednie operacje: zmiana szerokości paska,
wyświetlenie nazwy grupy lub zamknięcie formularza.

Podsumowanie

W artykule pokazano w jaki sposób można zrealizować wyświetlanie
paska postępu w Oracle Forms w przypadku długo trwających operacji. Jeśli jednostka
programowa realizująca operację jest częścią formularza, to można w niej zawrzeć
ustawianie elementów i wyświetlanie informacji bezpośrednio na kanwie nakładanej tego
samego formularza. W przypadku procedur składowanych w bazie danych i wykonywanych na
serwerze pojawia się problem, jak procedura na serwerze ma zmodyfikować pasek
wyświetlany w formularzu? Rozwiązaniem jest użycie pakietu DBMS_PIPE (ewentualnie
DBMS_OUTPUT) i za jego pomocą informowanie aplikacji o postępie wykonania operacji.
Niezbędne jest w takim przypadku umieszczenie paska postępu w osobnym formularzu, który
jest wywołany w sposób asynchroniczny i pracuje współbieżnie do macierzystego
formularza.
Przedstawiony w przykładach formularz i mechanizm komunikacji z użyciem DBMS_PIPE mają
oczywiście uproszczoną konstrukcję dostosowaną do potrzeb omówienia w artykule
działania samego mechanizmu. W rzeczywistym systemie formularz taki można w znacznym
stopniu rozbudować. Pasek taki z pewnością chcielibyśmy wykorzystać przy raportowaniu
postępu kilku zupełnie różnych operacji. W tym celu, co najmniej tytuł samego okienka
i napis wyświetlany na kanwie paska powinny być przekazywane jako parametry podczas
wywołania formularza PASEK. Zamiast paska poziomowego można wykonać pasek pionowy
zmieniając wysokość pola (a nie jego szerokość) lub postęp procedury może być
reprezentowany przez przemieszczanie na kanwie efektownej grafiki, ustawianie własności
kontrolki OCX, itp. Można wykonać raportowanie operacji z użyciem dwóch i większej
liczby pasków pokazujących przetwarzanie innego rodzaju rekordów (np. grupy towarów na
jednym pasku, towary w ramach grupy na drugim). Krótko mówiąc – dużo zależy od
inwencji projektanta. Miłego eksperymentowania!

Maciej Matysiak
maciej.matysiak@cs.put.poznan.pl
Instytut Informatyki Politechniki Poznańskiej