Profiler – narzędzie dewelopera cz. II

Dziś kontynuujemy wykorzystanie narzędzia Profiler Control do przetestowania kodu naszej aplikacji. Weźmy np. prosty poniższy przykład CustReport.p który wyświetla w pętli podstawowe pola z tablicy Customer,

DEFINE VARIABLE dOrdervalue AS DECIMAL NO-UNDO.

FOR EACH customer WHERE custnum < 100:
   dOrdervalue = 0.
   IF custnum < 40 THEN 
      RUN ordervalue.p (custnum, OUTPUT dOrdervalue).
   
   DISP custnum name dOrdervalue.

END.

a dla części iteracji uruchamia dodatkową procedurę ordervalue.p zliczającą sumę zamówień klienta.

DEF INPUT PARAM icustnum AS INTEGER.
DEF OUTPUT PARAM dordval AS DECIMAL.

FIND customer WHERE customer.custnum = icustnum.

FOR EACH order OF customer, EACH orderline OF order:
   dordval += price * qty.
END.

Po uruchomieniu Profilera zostanie wygenerowany plik typu .out. Jeśli spróbujemy go otworzyć przy pomocy przycisku VIEW z panelu narzędzia, dostaniemy komunikat o błędzie: Cannot load data. Don’t know how to load data for version 3.
Otwórzmy plik zwykłym edytorem tekstowym, np. Notepad++. Na pierwszej pozycji znajduje się nr wersji a potem data. Zmieniamy wartość nr wersji 3 na 1.
Zobaczmy przy okazji, że plik ten zawiera informacje generowane podczas inicjalizacji sesji oraz dane niezbędne do analizy przy pomocy Viewera.

Po naciśnięciu VIEW zobaczymy poniższy ekran.

Profiler udostępnia statystyki dotyczące działania programu. Dane te są podzielone według bloków i poszczególnych linii kodu. Każdy blok reprezentuje aktualną procedurę, procedurę wewnętrzną lub funkcję. W górnym pasku, po lewej stronie możemy wybrać zarejestrowaną sesję. Obszar 1 zawiera następujące informacje:

Code Block: nazwa wewnętrznej procedury lub funkcji, tryger interfejsu użytkownika i nazwa programu
Calls To: liczba wykonań tego bloku kodu
Avg Time: średni czas w sekundach potrzebny do wykonania tego bloku kodu
Tot Time: całkowity czas w sekundach potrzebny do wykonania tego bloku kodu
%Session: procent całej sesji wykorzystany przez ten blok kodu w porównaniu z innymi blokami kodu
Cum Time: całkowity czas potrzebny do wykonania tego bloku kodu oprócz całkowitego czasu wszystkich bloków kodu wywołanych przez ten blok.

Obszar 3 zawiera informacje dot. linii kodu:

Line: numer linii w wybranym programie
Exec Count: liczba wykonań tej linii kodu
Avg Exec: średni czas wykonania tej linii kodu w sekundach
Tot Time: całkowity czas w sekundach potrzebny do wykonania tej linii kodu
Cum Time: całkowity czas w sekundach, jaki zajęło wykonanie tego wiersza kodu, a także wszelkich bloków kodu, wywołanych przez tę linię. UWAGA: ta liczba będzie się różnić od Tot Time tylko wtedy, gdy linia kodu jest instrukcją uruchomienia procedury lub wywołania funkcji.
Pozostałe pola nie są używane.

Wreszcie obszar 2.
Calling Code Block oraz Called Code Block umożliwiają nawigację między różnymi sekcjami kodu poprzez dwukrotne kliknięcie wybranego wiersza.
Widzimy, że procedura ordervalue.p była wywołana 38 razy, co zajęło niecałe 4% czasu sesji.

Jeszcze jedna sprawa – na ekranie wyjściowym Profilera (pierwszy obraz w poprzednim artykule) widzimy po prawej stronie trzy pola wyboru: Listing, Coverage, Tracking.
Pierwsze służy do wygenerowania listingu, który będzie widoczny w Viewerze. Coverage służy do generowania dodatkowych danych dla procedur zewnętrznych, a Tracking do określenia filtra dla generowania danych tylko dla określonych procedur. Jeśli zaznaczymy Listing i ponownie uruchomimy sesję, na ekranie pojawi się listing programu, a wybór linii kodu a w browserze podświetli tę linię na listingu. Bardzo użyteczna funkcja.


Teraz trzeba już tylko przetestować własne procedury aby zorientować się w przydatności Profilera.
To jeszcze nie koniec. W następnym odcinku zobaczymy jak używać tego narzędzia w Developer Studio.

Profiler – narzędzie dewelopera cz. I

Profiler czy Profiler Control to narzędzie znane już od wersji V9 Progressa. Było dostępne w postaci źródłowej z katalogu DLC\src\samples\profile, a w późniejszych wersjach dodane do pakietu samples, który można pobrać oddzielnie. Jest to deweloperskie narzędzie darmowe i niewspierane. Wielu klientów korzysta z niego na całym świecie. Zostałem niedawno poproszony o przypomnienie jak rozpocząć z nim pracę, co opiszę w niniejszym artykule.

Profiler jest powszechnym i użytecznym narzędziem programisty do oceny wydajności aplikacji. Krótko mówiąc, profiler dostarcza „profil” konkretnego wykonania programu czyli informacje o czasach wykonania poszczególnych modułów oraz kolejność ich wywołań; dzięki czemu można przeanalizować, gdzie program spędza większość czasu i jaka część aplikacji wywołuje inną część aplikacji. W szczególności możemy przeanalizować czas wykonań dla konkretnych zapytań lub znaleźć podejrzanie dużą liczbę iteracji dla instrukcji.

Narzędzie to składa się z dwóch modułów – modułu zbierającego dane oraz modułu graficznego (Windows), w którym można te dane analizować. Profiler śledzi opóźnienia w aplikacji spowodowane przez kod programu ignorując jednocześnie te wywołane interakcją użytkownika.

Pierwsza wersja tego narzędzia wymagała utworzenia pustej bazy prof.db i załadowania do niej definicji z pliku prof.df. Ja przedstawię tutaj nowszą wersję, w której baza nie jest potrzebna gdyż wszystkie dane są gromadzone w tablicach tymczasowych.
Wersja ta zawiera graficzny interfejs użytkownika, który upraszcza uruchamianie i zatrzymywanie procedur. Może wykrywać czy jest uruchomiony Progress Dynamics a także umożliwia uruchamianie dynamicznych kontenerów oraz statycznych smart obiektów i innych procedur. Zawiera wszystkie te same informacje, ale eliminuje znaczną część złożoności poprzedniego narzędzia, eliminując czasochłonne ładowanie danych profilu. Do statystyk wykonania, dodano więcej informacji. To narzędzie zawiera również dodatkową funkcjonalność, która daje użytkownikowi możliwość wykonywania porównań i eksportowania danych do zewnętrznych programów. Można go pobrać tutaj.

W niniejszym artykule oprócz wprowadzenia i informacji ogólnych znajdziecie metody jak rozpocząć z nim pracę. W styczniu natomiast napiszę jak interpretować dane.

W zasadzie są dwie metody generowania danych dla profilera: uruchamiając narzędzie z parametrem startowym -profile lub używając handle (uchwyt) PROFILER. Najprostszym sposobem jest uruchomienie pliku profiler.w, co widać powyżej. Program ten uruchamiany jest bezparametrowo ale wykorzystuje on wspomniany uchwyt.
Pojawi się komunikat czy uruchomić sesję profilera. Jeśli potwierdzicie, przełącznik radiowy ustawi się w pozycji Profile, jeśli nie, w pozycji Run.
Wybieramy Profile i w polu Procedure wpisujemy nazwę profilowanej procedury (nie ma niestety drzewa systemu z plikami do wyboru). Oprócz pola procedury widać także pole Container. Jest ono aktywne gdy mamy otwartą sesję z produktem Progress Dynamics. Domyślnie narzędzie jest ustawione tak aby śledzić dane dla sesji klient-serwer lecz jest też możliwe śledzenie sesji dla klient-appserver dla Dynamics.

Po wpisaniu nazwy programu i naciśnięciu Run, dane zostaną zapisane do pliku, którego nazwa jest generowana automatycznie (możemy ją oczywiście zmienić). W tym przypadku clntprof20221227.out. Po zakończeniu procesu wybierz przycisk Stop. Spowoduje to, że program profilujący zakończy działanie procedury (o ile nadal działa) a dane sesji zostaną wyeksportuje pliku wyjściowego (jeśli zaznaczone jest pole Write On Stop). Dane można także zapisać w dowolnym momencie naciskając przycisk Write Data.

Możemy sami skorzystać z uchwytu PROFILER. Ta metoda jest wygodna jeśli chcemy programowo profilować tylko wybrane fragmenty kodu. Wymaga ona więcej pracy ale daje dodatkowe możliwości i w przypadku złożonych programów jest nieoceniona. Przykładowy fragment kodu widzimy poniżej.

ASSIGN
   PROFILER:DESCRIPTION = "Profiler Start"
   PROFILER:DIRECTORY = "c:/WrkOpenEdge126/"
   PROFILER:FILE-NAME = "clntprof20221227.out"
   PROFILER:COVERAGE = TRUE
   PROFILER:LISTINGS = TRUE
   PROFILER:ENABLED = TRUE
   PROFILER:PROFILING = TRUE.

Jeśli chcemy profilować dane całej aplikacji, możemy skorzystać z parametru startowego -profile profiler.txt który wstawiamy do ikonki lub skryptu uruchamiającego procedurę.

W piku profiler.txt definiujemy parametry np:

   -description "Profiler Start"
   -listings = "c:/WrkOpenEdge126/"
   -filename = "clntprof20221227.out"

OK, to na razie tyle. Następnym razem zajmiemy się generowaniem i analizą danych.

OpenEdge 12.6

Wersja OpenEdge 12.6 pojawiła się z końcem września br. jako Innovation Release (termin który zastąpił Non-LTS Release) Podczas instalacji trzeba jak zwykle podać ścieżkę do JDK – w tej wersji jest to JDK 17.
Zaczynamy od nowości związanych z bazami danych.

PROUTIL IDXCOMPACT – to popularne narzędzie poprawiające wydajność indexów także dla bazy online. Komenda ta została wzbogacona o opcję UNUSEDBLOCKS, użyteczną szczególnie po operacji usuwania dużej liczby rekordów. Powoduje ona skanowanie łańcucha “delete” i czyszczenia bloków indexowych w przypadku indexów unikalnych. Stosowanie dla indexów nie-unikalnych nie spowoduje ostrzeżenia ani błędu na konsoli.

Z tą opcję nie można podać stopnia kompresji, gdyż spowoduje to ostrzeżenie i niewykonanie polecenia.

Obcięcie obszaru przechowywania danych online.
Do tej pory obcięcie obszaru (PROUTIL TRUNCATE AREA) można było wykonać jedynie dla zamkniętej bazy, po obcięciu pliku BI i wyłączeniu zapisu AI.
Obecnie operację można przeprowadzić dla bazy włączonej. Obcięcie obszaru stosuje się po usunięciu dużej ilości danych, np. przeniesieniu danych historycznych do innego obszaru. Obcięcie obszaru można wykonać tylko gdy w obszarze nie znajdują się żadne obiekty (tablice, indexy, LOBy).

Wprowadzono obsługę dużych plików dla licencji Workgroup, co ma zlikwidować problem gdy baza działająca w środowisku produkcyjnym Enterprise jest skopiowana do testowania i rozwoju, co często przeprowadza się w niższej licencji Workgroup. Od OE 12.6 znacznik obsługi dużych plików jest włączany automatycznie dla obu typów licencji.

Usprawniony został mechanizm działania narzędzia do naprawy indexów PROUTIL IDXFIX. Dla opcji 1 i 8 skanowane są tylko te bloki obszaru typu II, które należą do podanych tabel i indexów. Usprawnienie nie działa dla bloków obszaru I. Poprawiono także mechanizm kasowania rekordów w tym samym narzędziu.

Przejdźmy teraz do wybranych nowości deweloperskich.
Identyfikacja Memory Leaks. Programiści mogą teraz używać klasy OpenEdge.Core.Util.LeakCheck w bibliotece corelib do przetwarzania logów i identyfikacji wycieków pamięci. Dokładne informacje o tej klasie można znaleźć w dokumentacji.
Praktyczny opis identyfikacji wycieków pamięci znajduje się tutaj.

Środowisko Progress Developer Studio dla OpenEdge zostało zaktualizowane i korzysta z wersji Eclipse 4.23.

Został zoptymalizowany tutaj proces inicjalizacji widoczny szczególnie w przypadku złożonych i rozbudowanych workspace’ów.

Warto dodać, że w tym samym czasie pojawiła się nowa wersja OE Command Center 1.2, w której administratorzy systemu mogą monitorować metryki wydajności PASOE oraz baz danych OE. Metryki wydajności są zbierane przy użyciu standardu OpenTelemetry (OTel).
Zarządzanie PASOE zostało wzbogacone o funkcję klonowania instancji.
Ponadto, został zautomatyzowany proces instalacji w systemie Windows, zamiast ręcznej edycji skryptów uruchamiany jest teraz instalator.
O wielu innych nowościach możecie przeczytać w dokumentacji. Zapraszam do dyskusji na polskiej grupie PUG.

Swagger UI i PASOE

Swagger UI jest narzędziem które umożliwia wizualizację zasobów API bez konieczności implementacji dodatkowych zewnętrznych aplikacji.

Swagger składa się z plików HTML, JavaScript i CSS, które dynamicznie generują dokumentację na podstawie interfejsu API.
Jest dostępny w PASOE począwszy od wersji OE 12.0 (domyślnie wyłączony). Od wersji OE 12.2 jest domyślnie włączony dla instancji deweloperskich – należy pamiętać aby nigdy nie włączać go w środowiskach produkcyjnych.

Dostęp do Swaggera jest z poziomu oemanagera (http://host:port/oemanager/), ale jeśli dostęp jest zabroniony, to wchodzimy z poziomu managera, czyli http://host:port/manager/, np. http://localhost:8813/manager.


Tutaj klikając oemanager dostajemy ekran dokumentacji Swaggera.


Ekran składa się z sekcji np. dla każdej warstwy transportowej,  Agent Manager, Session Manager i OEABL ManagerService.
Przechodzimy do tej drugiej i klikamy na GET /applications, następnie Try it out i Execute.


Dostajemy listę wszystkich aplikacji ABL dla instancji PASOE wraz z parametrami w postaci JSON. Listę tę można pobrać jako plik tekstowy.

Jeśli w OEABL ManagerService kliknę GET /applications/{AppName}/webapps i wpiszę nazwę aplikacji ABL, dostanę w podobnej postaci listę wszystkich aplikacji webowych dla tejże aplikacji.

Aby pobrać listę web servisów dla danej aplikacji webowej wybieram GET/applications/{AppName}/webapps/{WebAppName} i podaję nazwę aplikacji ABL i web serwisu.

Jak widać na obrazkach czasowniki GET nie są jedynymi do użycia. Można np. zaktualizować listę poprzez czasownik POST czy zerować metryki (DELETE).
Opcji jest bardzo wiele.

Migracja do PASOE

O PASOE sporo pisałem. Wiadomo, w OE12 nie ma już klasycznego AppServera i trzeba podjąć decyzję o migracji aplikacji, a przed decyzją dobrze jest poznać za i przeciw nowego produktu.
Teraz jednak powracam do tematu samej migracji w szerszym kontekście (pisałem o podstawach migracji w 2017).

Korzyść z nowego AppServera widać już podczas instalacji: możemy wybrać czy instalowana instancja będzie deweloperska czy produkcyjna. Dla tej ostatniej dodajemy w komendzie parametr -Z prod i mamy instalowanie instancji z implementacją silniejszych zabezpieczeń niż dla wersji deweloperskiej. O samych zabezpieczeniach nie będę pisał ponieważ temu tematowi poświęciłem tutaj kilka wpisów.

Następnym krokiem jest migracja ustawień, czyli pliku z właściwościami. Istniejący plik ubroker.properties musi zostać przekonwertowany na nowy format używany przez PASOE openedge.properties.
Kolejność działania jest następująca: najpierw uruchamiamy polecenie paspropconv, które konwertuje właściwości z pliku ubroker.properties do tymczasowego pliku ubrokername.oemerge. Plik ten można dopasować do naszych potrzeb i włączyć ustawienia naszej nowej instancji do pliku openedge.properties.

Komendę paspropconv uruchamiamy w podkatalogu conf dla instancji PASOE z przykładowymi parametrami, które są wymagane. Podwójne myślniki przed nazwą parametru wynikają stąd, że skrypt jest napisany w języku Perl.

--ubrokerPropsFile C:\classicapp\ubroker.properties 
--ubrokerName UBroker.AS.asbroker1 
--pasoeAppName myprod

Pierwszy parametr określa ścieżkę do pliku z właściwościami dla klasycznego AppServera, drugi jest nazwą instancji tego AppServera, wreszcie trzeci określa nazwę nowej instancji PASOE.
Po uruchomieniu komendy w katalogu conf zostały utworzone poniższe pliki:

Dla nas istotny jest plik .oemerge. Niemal całą jego zawartość stanowi przewodnik po migracji, zawierający sekcje np:
Tryby pracy (operating modes) – zawiera porady dotyczące migracji istniejących trybów pracy (state-reset, state-aware, stateless, statefree) na tryby pracy w PASOE. Omówione są proste instrukcje, realizujące ten etap migracji, które wiążą i zwalniają bieżącą sesję ABL (były omawiane na blogu kilka lat temu).
Procedury zdarzeniowe (event procedures) – omawiane są stare i nowe procedury (agentStartupProc, sessionStartupProcParam) związane z inicjalizacją agenta wielo-sesyjnego oraz każdej sesji ABL.

# paspropconv  v1.15 (MSWin32)
# 
# Input arguments:
# 
#   ubrokerPropsFile    = C:\classicapp\ubroker.properties
#   ubrokerMergeFile    = 
#   ubrokerName         = UBroker.AS.asbroker1
#   pasoeAppName        = myprod
#   pasoeWebAppName     = ROOT
#   pasoeMergeFile      = myprod.asbroker1.oemerge
#   pasoeSetEnvFile     = asbroker1_setenv
#   convNotesDBFile     = C:\OPENED~1\bin\paspropconv_notesdb.en
#   logFile             = paspropconv.log
#   loggingLevel        = 2
# 
# Operating Modes
# ---------------
# 
# The classic AppServer supports 4 operating modes:
#   state-reset
#   state-aware
#   stateless
#   statefree
# 
# In the classic AppServer, the operating mode is specified when the AppServer
# is deployed.  Consequently, all ABL sessions supported by the AppServer
# employ the same operating mode.
# 
# In PASOE, the operating mode of a session is not specified during deployment.
# As such, a single PAS server can support concurrent ABL sessions, each
# emulating the behavior of different classic operating modes.
# 
# To support the different types of operating mode behavior
# provided in the various classic modes, some ABL code modifications
# may be required.  It is recommended that these changes are made in 
# the PASOE sessionConnectProc () and
# sessionDisconnProc () event procedures.
.......
[AppServer.Agent.myprod]
    PROPATH=${CATALINA_BASE}/openedge,${CATALINA_BASE}/webapps/ROOT/WEB-INF/openedge,.....
    agentMaxPort=2202
    agentMinPort=2002
    keyAlias=
    keyAliasPasswd=
    keyStorePasswd=
    keyStorePath=${DLC}/keys/
    noSessionCache=0
    numInitialSessions=5
    sessionActivateProc=
    sessionConnectProc=
    sessionDeactivateProc=
    sessionDisconnProc=
    sessionExecutionTimeLimit=0
    sessionShutdownProc=
    sessionStartupProc=
    sessionStartupProcParam=
    sessionTimeout=180
    sslAlgorithms=
    sslEnable=0
.......

Ostatnia sekcja nie jest komentarzem – zawiera zestaw właściwości, które powinny zostać scalone z plikiem openge.properties dla nowej instancji serwera.

Dalsze porady dotyczą ręcznej konfiguracji wynikającej z różnic w ścieżkach dostępu, architektury systemu operacyjnego, zmiennych środowiskowych, połączeń z bazami danych itd.
Dla pełniejszych informacji warto pobrać i przeczytać dokument: Quick Start: Moving Your Classic AppServer Applications to the Progress® Application Server for OpenEdge®.

OpenEdge 12.3

Powróćmy do tematu związanego z OpenEdge, ponieważ już od pewnego czasu mamy na rynku nową wersję OE 12.3.
Zacznijmy od nowinki dla programistów; dodano nową, bardziej zwięzłą instrukcję definicji zmiennych. Do tej pory ile zmiennych chcieliśmy zdefiniować, tyle musieliśmy wstawić definicji. Obecnie zapis jest krótszy, a w jednej instrukcji można zdefiniować wiele zmiennych. Zmienne zdefiniowane poprzez nową instrukcję są zawsze NO-UNDO.

Następna nowinka związana jest z definicją tablicy (extentu) o nieokreślonym rozmiarze. Rozmiar ten można teraz określić podczas fazy runtime, nawet jeśli tablica ma przypisane wartości początkowe.

W ABLu wprowadzono także operatory przypisania (+=, -=, *=, /=) do wykonywania operacji i przypisywania
wartość, używając skróconej notacji.

Od 12.3 jeśli zachodzi potrzeba aktualizacji aplikacji ABL, przy czym ta aktualizacja jest związana z nowymi elementami schematu, nie trzeba już
restartować maszyny wirtualnej ABL (AVM).

W OpenEdge 12.3 dodano ciekawą opcję komendy PROUTIL: PROUTIL TABLEREORG – umożliwia to reorganizację pofragmentowanych danych w tabeli, podczas gdy sama tabela pozostaje dostępna dla operacji OLTP (przetwarzanie transakcji online). Nowy proces zastępuje długotrwałe operacje zrzutu i ładowania danych oraz odbudowy powiązanych indeksów. Rekordy tabeli muszą znajdować się w tym samym obszarze przechowywania typu II. Operacja ta obsługuje także tabele multi-tenant oraz podzielone na partycje.

Kolejne parametry mogą być modyfikowane online, tym razem dla brokera secondary.
Maximum Clients per Server (-Ma)
Maximum Dynamic Server (-maxport)
Minimum Clients per Server (-Mi)
Minimum Dynamic Server (-minport)
Message Buffer Size (-Mm)
Maximum Servers per Broker (-Mpb)
Pending Connection Time (-PendConnTime)

Zmiany parametrów można dokonać w R&D -> opcja 4. Administrative Functions -> 16. Adjust Startup Parameters.

Jest jeszcze kilka ciekawych zmian związanych z tzw. Continuous Operations, bezpieczeństwem, programowaniem, ale to już musicie poszukać w sieci sami.

JSON: wyświetlanie danych

W kilku wcześniejszych artykułach kończyliśmy rozważania na utworzeniu serwisu generującego dane JSON. URI do tych danych może posłużyć do integracji z innym systemem czy interfejsem wyświetlającym dane, najczęściej w języku JavaScript. Przypomnę dwa posty sprzed kilku lat:
Serwis OpenEdge – Telerik Kendo UI,
Rollbase i serwisy OpenEdge,
w których opisałem dwa produkty wyświetlające dane pochodzące z formatu JSON.

Istnieje wiele środowisk, w których możemy tworzyć front-end w JavaScript, ale dziś pokażę jak wyświetlić dane na dwóch prostych przykładach.
Weźmy JSON wygenerowany w poprzednim artykule. Struktura danych jest następująca dsCustomer.ttCustomer[i]… dane do pól rekordów.


Nie ma tutaj znaczenia czy plik JSON jest wygenerowany do katalogu. Dane w formacie JSON są przechowywane w pamięci przeglądarki.

W pierwszym przykładzie skorzystamy z metody getJSON, której pierwszym parametrem jest URI do danych JSON.

<!DOCTYPE html>
<html>

<body>

<h2>Customers</h2>
<div id="mydata"></div>

<script>

var getJSON = function(url, callback) {

var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';

xhr.onload = function() {

var status = xhr.status;

if (status == 200) {
callback(null, xhr.response);
} else {
callback(status);
}
};

xhr.send();
};

getJSON('http://localhost:8810/wh/web/pdo/whsrv/customer', function(err, data) {

if (err != null) {
console.error(err);
} else {

var text = `Last Name: ${data.dsCustomer.ttCustomer[1].CustNum}<br>
First Name: ${data.dsCustomer.ttCustomer[1].Name}`

document.getElementById("mydata").innerHTML = text;

}
});
</script>

</body>
</html>

Ten prosty skrypt wyświetla 2 pola z rekordu Customer, a dokładniej zmienną text, w której mogą znajdować się znaki formatujące.

Drugi przykład jest podobny, ale wykorzystuje bibliotekę jQuery. Tym razem wyświetlanych jest kilka rekordów (zmienna text) sformatowanych wg pliku css (nie zamieszczonego).

<!DOCTYPE html>
<html lang="en">
<head>
<title>JavaScript - read JSON from URL</title>

<script src="https://code.jquery.com/jquery-3.2.1.min.js">
</script>

<link rel="stylesheet" href="customers.css">
</head>

<body>
<h2>Customers</h2>
<div class="mydata"></div>

<script>
$.getJSON('http://localhost:8810/wh/web/pdo/whsrv/customer', function(edata) {

var text="<table>";
var i;

for(i = 1; i < 11; i++) {

text += "<tr><td>" +`${edata.dsCustomer.ttCustomer[i].CustNum}` + "</td><td>" +
`${edata.dsCustomer.ttCustomer[i].Name}` + "</td><td>" +
`${edata.dsCustomer.ttCustomer[i].City}` + "</td></tr>"
}
text += "</table>";

$(".mypanel").html(text);
});
</script>

</body>
</html>

 

Efekt widać poniżej.

Napiszcie o swoich przykładach wykorzystujących dane JSON na forum PUG Poland. Jak zapewne zauważyliście forum to zostało przeniesione razem z resztą Progress Communities do nowej lokalizacji, a starsze wątki zastały przeniesione do Archiwum.

Trzeba ponownie się zarejestrować.
 

Serwisy REST Webhandler po raz trzeci

W 2018 opisałem 2 przykłady serwisów REST Webhandler ABL. Dostęp do nich jest nie poprzez warstwę transportową REST lecz WEB.

Temat powraca ze względu na Wasze zainteresowanie tą metodą. Intuicyjnie, niektórzy uważają, że jest to lepszy sposób na budowanie serwisów po przejściu na serwer aplikacji PASOE, ze względu na to że są to serwisy napisane w języku ABL. Niektórzy uważają jednakże, że ten typ serwisów jest trudniejszy do wygenerowania w porównaniu z typowymi serwisami REST Data Object Service. Pokażę za chwilę, że serwisy z użyciem Web Handera (w skrócie WH) są również bardzo proste.
W niniejszym blogu znajdziecie 2 metody tworzenia typowych serwisów REST: Data Object Service oraz z mapowaniem elementów URI. Pierwsza metoda jest bardzo łatwa, ale ma pewne ograniczenia, druga bardziej elastyczna, ale wymagająca więcej pracy.

Z serwisami WH jest podobnie. To co pokazałem 2 lata temu, to trudniejsza, elastyczna metoda związana z napisaniem własnego web handlera. Jednakże, można użyć szybkiej, prostej metody Data Object Service, co teraz chciałbym pokazać.

Tworzymy projekt typy WEB. Zaznaczamy “Create a Data Object Service….”.

Możemy poprzestać na domyślnej nazwie aplikacji webowej, lub nadać własną: tutaj wh. Aplikacja będzie wdrożona na serwerze oepas1.

Serwis ABL będzie miał nazwę whsrv.

Projekt jest podłączony do bazy danych sports2000. Naciskamy Finish.

Projekt został utworzony. Teraz zdefiniujemy klasę Business Entity o nazwie customer.

Wybieramy tabelę Customer z bazy sports2000 oraz nazwę zasobu w URI: /customer.

Prawym klawiszem myszy klikamy na wygenerowany serwis i Edit. Zaznaczamy klasę customer.cls, która ma być źródłem dla serwisu. Naciskamy Finish.

Warto zaznaczyć, że dla serwisów typy WEB obok pliku .json jest wygenerowany plik .gen. Możemy to zauważyć w widoku konsoli.

Teraz pora na restart pasoe i sprawdzenie naszego serwisu. W przeglądarce (tutaj Firefox) wpisujemy localhost:[port]/wh/web/pdo/whsrv/customer

W adresie możemy skorzystać z filtra; chcemy np. tylko rekordy z Finlandii: localhost:[port]/wh/web/pdo/whsrv/customer?filter=Country=”Finland”

Proste, prawda?

Jakie są rekomendacje Progress Software dotyczące wyboru warstwy transportowej i metody budowania serwisów wg warstwy REST czy WEB.

REST – oparte na Javie, są rekomendowane przy wystawianiu istniejącej, restowej logiki biznesowej, gdy nie chcemy pisać kodu od początku.

WEB – oparte na języku ABL, o wiele bardziej skalowalne. Są rekomendowane jeśli chcemy pisać serwisy od zera.

PASOE i WebSpeed

Myślę, że wielu spośród polskich klientów Progressa pamięta czasy wersji 9 (koniec lat 90-tych), z którą wprowadzona została technologia tworzenia aplikacji webowych WebSpeed. Składał się on z dwóch produktów: WebSpeed Transaction Servera – odpowiedzialnego za uruchamianie i poprawność działania aplikacji, zapewniającego spójność transakcji i danych przy jednoczesnym korzystaniu z wielu baz. Drugim produktem był WebSpeed Workshop, złożone środowisko do tworzenia aplikacji.

Pamiętam, że technologia ta cieszyła się sporym zainteresowaniem naszych klientów, co zaowocowało kilkoma szkoleniami z tego zakresu. W wersji OpenEdge 10, WebSpeed został scalony z produktem OpenEdge AppServer. Po wprowadzeniu nowego serwera aplikacji PASOE i zaprzestaniu rozwijania klasycznego AppServera pojawiło się pytanie: co dalej z aplikacjami WebSpeed?

Zanim odpowiem na to pytanie chciałbym krótko przypomnieć w jaki sposób można było tworzyć aplikacje WebSpeed. Użytkownik miał do wyboru trzy możliwości.

Pierwszym był plik HTML, w którym można było osadzić elementy języka SpeedScript pomiędzy znacznikami <script language=”SpeedScript”> … </script>. SpeedScript, to był w zasadzie język 4GL z wyłączeniem operacji wejścia/wyjścia. Zamieszczam prosty przykład; operacje wyjścia kierowane są do predefiniowanego strumienia.

<HTML>
  <HEAD>
  </HEAD>
  <BODY>
    <H1> Customer List </H1>
    <SCRIPT LANGUAGE=”SpeedScript”>
      FOR EACH customer:
        DISPLAY {&WEBSTREAM} customer.
      END.
    </SCRIPT>
  </BODY>
</HTML>

Drugim sposobem było niejako odwrócenie ról; językiem nadrzędnym był SpeedScript a znaczniki HTML były osadzane w strumieniu {&OUT}, jak poniżej…

PROCEDURE ws-out:
{&OUT}
 „<HTML>”:U SKIP
 „<BODY>”:U SKIP
 .
  FOR EACH customer:
   {&DISPLAY} customer.
  END.
{&OUT}
 „</BODY>”:U SKIP
 „</HTML>”:U SKIP
 .
END PROCEDURE.

Wreszcie ostatnim sposobem, najbardziej rozbudowanym było mapowanie HTML. Tworzone były wówczas 2 pliki: HTML (zwykle forma z polami i przypisaną akcją) oraz plik .w.

Otóż, w nowym serwerze aplikacji można obsługiwać dwa pierwsze typy aplikacji. Jednakże, jeśli przypomnimy sobie 2 artykuły PASOE i serwisy WebHandler zobaczymy, że ta nowa technologia daje nam o wiele większe możliwości.

Sposób otwierania naszych aplikacji .w jest bardzo prosty.

Załóżmy, że mamy skonfigurowaną instancję serwera aplikacji, która połączona jest do bazy danych sports2000. Chcemy otworzyć plik w-custlist.w, będący prostą aplikacją WebSpeeda.

W katalogu roboczym naszej instancji przechodzimy do podkatalogu: %WRK%\instancja\webapps\ROOT\WEB-INF\openedge i umieszczamy tam plik .w. (oczywiście zamiast słowa ‘instancja’ umieszczamy nazwę naszego pasoe).

Następnie otwieramy plik %WRK%\instancja\conf\openedge.properties
i szukamy tam frazy: instancja.ROOT.WEB.defaultHandler.
Powinna mieć wartość: OpenEdge.Web.OpenEdge.Web.CompatibilityHandler.
Jeśli tak nie jest uruchamiamy komendę:
oeprop instancja.ROOT.WEB.defaultHandler=OpenEdge.Web.OpenEdge.Web.CompatibilityHandler
Wpis w pliku konfiguracyjnym będzie miał zatem postać:

[instancja.ROOT.WEB]
    defaultCookieDomain=
    wsRoot=/static/webspeed
    srvrDebug=1
    defaultHandler=OpenEdge.Web.CompatibilityHandler

Oczywiście możemy także wartości parametrów wpisywać ręcznie, ale łatwiej wtedy o pomyłkę. Zawsze najpierw warto zrobić kopię pliku konfiguracyjnego.

Restartujemy instancję serwera i wpisujemy w przeglądarce URL:

http://localhost:8810/web/w-custlist.w

Proszę zauważyć, że wykorzystywana jest czwarta warstwa komunikacji – WEB. Zaglądając do wspomnianych wcześniej 2 artykułów można spróbować zdefiniować klasy obsługujące różne wywołania dla odpowiednio podanych wartości URL.

Tworzenie obiektowego UI w OE Development Studio cz. IV

W czwartej części naszego cyklu, zajmiemy się obiecanym tematem edycji rekordów w bazie danych. Przypomnijmy: dane wyświetlane w obiektach graficznych są zasilane ze źródła BindingSource.
To źródło pobiera dane z tabeli np. Customer czy Order, ale działa niejako w jednym kierunku, tzn. nie nadaje się do zapisu danych. W naszych przykładach zapis będzie realizowany do tabel w bazie poprzez zdefiniowane statyczne bufory rekordów.

W przypadku aplikacji działającej np. na serwerze aplikacji, gdzie nie ma bezpośredniego dostępu do bazy, źródło BindingSource konfiguruje się w oparciu o schemat ProDataSet, z tabelami tymczasowymi, a edycja jest realizowana poprzez zapis do tych właśnie obiektów.

OK, zaczynamy.

W formie fCustomer dodajemy przycisk z etykietą Delete, nazwa np. Del.
Klikamy dwa razy w zdarzenie Click aby utworzyć metodę do obsługi tego zdarzenia (Del_Click). W metodzie tej wstawiamy poniższy kod:

DEFINE BUFFER bCust FOR Customer.
DEFINE BUFFER bOrder FOR Order.
DEFINE VARIABLE lDel AS LOGICAL NO-UNDO.
DEFINE VARIABLE iActCustNum AS INTEGER NO-UNDO.
		
MESSAGE "Do you want to delete this record?" SKIP 
            bsCust:InputValue["Name"]:ToString() SKIP 
	    bsCust:InputValue["City"]:ToString() SKIP
VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO UPDATE lDel.
IF NOT lDel THEN 
   RETURN.

Na razie mamy sukces tylko połowiczny – pojawia się komunikat gdzie możemy podjąć decyzję o kasowaniu rekordu.

Teraz dodamy kod kasujący rekord Customer i połączone z nim rekordy Order.
Dodajemy kod poniżej instrukcji RETURN:

IF NOT lDel THEN 
   RETURN.

iActCustNum = INTEGER(bsCust:InputValue["CustNum"]:ToString()).
DO TRANSACTION ON ERROR UNDO, RETURN:
   FOR FIRST bCust WHERE bCust.CustNum = iActCustNum:
      FOR EACH bOrder WHERE bOrder.CustNum = bCust.CustNum.
          DELETE bOrder.
      END.
      DELETE bCust.
   END. 
END. 

Żeby program zadziałał trzeba pamiętać, że w bazie są jeszcze trygery, zabezpieczające przed utratą danych. Trzeba albo te trygery zmodyfikować albo wyłączyć. Decyzja należy do Was.

Drugim problemem jest to, że po skasowaniu rekordu DataGrid nie odświeża automatycznie danych; musimy to zrobić sami.
Dodajemy w metodzie kod:

bsCust:MovePrevious().
iPos = bsCust:POSITION.
refreshData().
bsCust:POSITION = iPos.

Zmienną iPos deklarujemy jako integer. Po skasowaniu kursor ustawia się na poprzednim rekordzie.

Teraz dodajemy przyciski Edit i Create i generujemy metody dla zdarzenia Click.

Przeważnie te same funkcjonalności można stworzyć na różne sposoby. Można np. wykorzystać możliwości jakie daje DataGrid, ale wg Wiesława, ta metoda może generować wiele problemów i nie jest zbyt czytelna. Wygodniej jest stworzyć oddzielne okno dla edycji. Tworzymy więc nową formę fCustEdit.

Dodajemy 4 pola TextBox: CustNum, Name, City, Country, oraz etykiety dla tych pól.

Wstawiamy przyciski btnOK (etykieta OK), btnCancel (etykieta Cancel) i ustawiamy ich predefiniowaną funkcjonalność.

We właściwościach fCustEdit ustawiamy parametr CancelButton -> btnCancel.
We właściwościach btnOK ustawiamy DialogResult -> OK.

Wracamy do okna fCustomer, do metody dla kliknięcia przycisku Edit. Wstawiamy tu kod:

METHOD PRIVATE VOID Edit_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
   DEFINE VARIABLE oCustEdit AS fCustEdit NO-UNDO.
   oCustEdit = NEW fCustEdit().
   WAIT-FOR oCustEdit:ShowDialog().
   DELETE OBJECT oCustEdit.

   RETURN.

Przycisk Edit już działa choć na razie nie widzimy danych.

Aby przekazać dane do edycji można skorzystać z różnych metod. My zdefiniujemy w fCustEdit właściwości dla każdego edytowanego pola. Np. pName dla pola Name; i tak dalej dla pozostałych pól.

    DEFINE PUBLIC PROPERTY pName AS CHARACTER NO-UNDO 
    GET.
    SET. 

Generujemy obsługę zdarzenia Shown dla fCustEdit i wpisujemu kod:

METHOD PRIVATE VOID fCustEdit_Shown( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
   ASSIGN
      textBoxName:TEXT = pName
      textBoxCity:TEXT = pCity
      textBoxCountry:TEXT = pCountry
      textBoxCustNum:TEXT = string(pCustNum).
   RETURN.

END METHOD.

Nasze okno edycyjne będzie pokazywać dane ustawione w fCustomer. Ponieważ pole CustNum nie może być przez nas edytowane ustawiamy w jego właściwościach Enabled = False, a żeby wyłączyć ramkę wokół pola ustawiamy BorderStyle = None.

Aby zapisać dane po edycji dodajemy obsługę kliknięcia na przycisk OK.

METHOD PRIVATE VOID btnOK_Click( INPUT sender AS System.Object, INPUT e AS System.EventArgs ):
   ASSIGN 
      pName    = textBoxName:Text
      pCity    = textBoxCity:Text
      pCountry = textBoxCountry:TEXT.
   RETURN.

END METHOD.

Wracamy do fCustomer i ustawiamy właściwości do wyświetlenia w oknie fCustEdit, czyli dodajemy kod w Edit_Click:

oCustEdit = NEW fCustEdit().
		   	
oCustEdit:pCustNum = int(bsCust:InputValue["CustNum"]:ToString()).
oCustEdit:pName = bsCust:InputValue["Name"]:ToString().
oCustEdit:pCity = bsCust:InputValue["City"]:ToString().
oCustEdit:pCountry = bsCust:InputValue["Country"]:ToString().
iActCustNum = INT(bsCust:InputValue["CustNum"]:ToString()).
		
WAIT-FOR oCustEdit:ShowDialog().

Okno fCustEdit wygląda teraz tak:

Następnym krokiem jest zapis wprowadzonych danych w fCustomer na dole obsługi Edit_Click. Dodajemy transakcję:

WAIT-FOR oCustEdit:ShowDialog().

DO TRANSACTION ON ERROR UNDO, RETURN:
   FOR FIRST bCust WHERE bCust.CustNum = iActCustNum SHARE-LOCK :
      ASSIGN
         bCust.Name    = oCustEdit:pName
         bCust.City    = oCustEdit:pCity
         bCust.Country = oCustEdit:pCountry.
   END.
END.

bsCust:Refresh().

OK, edycja jest już obsłużona. Pozostało jeszcze dodanie obsługi dla utworzenia nowego rekordu.

W tym celu wykorzystamy okno fCustEdit i fragment kodu dla edycji. Nie musimy przekazywać wartości pól, ale tworzymy nowy rekord komendą CREATE. Kod dla przycisku Create wygląda tak:

DEFINE BUFFER bCust FOR Customer.
DEFINE VARIABLE oCustEdit AS fCustEdit NO-UNDO.
DEFINE VARIABLE rRowid AS ROWID NO-UNDO.
        
oCustEdit = NEW fCustEdit().
                   
WAIT-FOR oCustEdit:ShowDialog().		
        
DO TRANSACTION ON ERROR UNDO, RETURN:
   CREATE bCust.
   ASSIGN
      bCust.Name    = oCustEdit:pName
      bCust.City    = oCustEdit:pCity
      bCust.Country = oCustEdit:pCountry.
   rRowid = ROWID(bCust).
END.

refreshData().
bsCust:HANDLE:reposition-to-rowid(rRowid).
RETURN.


Zmienna ROWID jest potrzebna żeby po dodaniu ustawić się na nowym rekordzie.
Małą niedogodnością wykorzystania formy fCustEdit jest to, że numer rekordu wynosi 0, ale z tym powinniście sobie poradzić bez problemu np. przenosząc instrukcję CREATE przed wywołanie formy i przekazanie parametru CustNum.


W zasadzie program jest gotowy, ale Wiesław chciał na koniec uatrakcyjnić go nieco dla Was.
Po pierwsze dodajmy klawisze skrótów dla operacji na rekordach.

We właściwościach formy fCustomer ustawiamy KeyPreview = True.

Następnie dla gridu włączamy zdarzenie KeyDown.

W metodzie obsługującej to zdarzenie definiujemy:

METHOD PRIVATE VOID dataGridView1_KeyDown( INPUT sender AS System.Object, INPUT e AS...
   CASE e:KEYCODE:ToString():
      WHEN "Insert" THEN Cre:PerformClick().
      WHEN "Return" THEN Edit:PerformClick().
      WHEN "Delete" THEN Del:PerformClick().
   END CASE.
   RETURN.

END METHOD.

Cre, Edit, Del to nazwy przycisków odpowiednio dla: Create, Edit i Delete. Całość działa poprawnie.

I już naprawdę na koniec, dodamy licznik rekordów. Przyda się gdy będziemy dodawać czy usuwać rekordy.
Dodajemy obiekt typu label, nadajemy nazwę np. labInfo. W metodzie refreshData dodajemy na końcu linię kodu:

labInfo:TEXT = "Liczba rekordów: " + STRING(bsCust:Count).

Po każdym wywołaniu tej metody liczba rekordów jest wyświetlana w obiekcie.

Niniejszym kończymy na razie cykl tworzenia aplikacji przy wykorzystaniu obiektów .Net w ABL, ale być może do niego wrócimy. Ponieważ pojawiła się już wersja OpenEdge 12, teraz trzeba napisać coś na ten temat.

Wiesław Kurzątkowski
Piotr Tucholski

1 2 3 4 5