Partycje Tabel OpenEdge cz. III

Zanim przejdę do kolejnej części związanej z partycjami tabel, winien jestem odpowiedzi na pytanie związane z definiowaniem zasad partycjonowanie.
Otóż kilku z Was chciało wiedzieć czy jest planowana możliwość definiowania podziału danych w tabeli w oparciu nie o wartość pola, lecz o funkcję (np. YEAR(TODAY)-1).
Miałem okazję niedawno rozmawiać z Richem Banvill’em – odpowiedzialnym za rozwój bazy OpenEdge. Taka możliwość nie jest w planach, ani w wersji OE12 ani w dalszej przyszłości.
Rich wspomniał, że problem ten można rozwiązać np. poprzez zdefiniowanie zadań w cronie.

Wróćmy jednak do tematu. W poprzedniej części zakończyliśmy podział tabeli w oparciu o wartości pola Country (lokalny idex). Wspomniałem, że partycjonowanie jest transparentne dla aplikacji, jednakże mogą zdarzyć się sytuacje gdy zapytanie zwraca nie te same rekordy co przed partycjonowaniem. Jednym z przykładów jest stosowanie w kodzie wyszukiwania po adresach RECID/ROWID. Ponieważ partycjonowanie przenosi rekordy, ich fizyczne adresy ulegają zmianom. W przypadku stosowania tego rozwiązania trzeba wprowadzić odpowiednie poprawki w kodzie aplikacji.

Innym, częstszym przykładem jest stosowanie w zapytaniu filtra na dane pole (WHERE). Ponieważ w partycjonowanej tabeli dodany jest zazwyczaj nowy, lokalny index, wyszukiwanie może być realizowane w oparciu o niego. Aby mieć pewność, że index nie został zmieniony i aby zachować ten sam porządek rekordów trzeba ew. dopisać frazę USE-INDEX.

Teraz trzeba wspomnieć o ważnej funkcji technologi partycjonowania tzw. pruning (okrajanie partycji).

Proces pruningu analizuje automatycznie zapytanie i (o ile to możliwe) nie bierze pod uwagę rekordów w partycjach, które nie spełniają warunków zapytania. Na rysunku widać przykład podziału rekordów na kwartały wg pola OrderDate. Jeśli w zapytaniu warunki będą dotyczyły rekordów tylko z kwartału drugiego, to tylko ta jedna partycja będzie w użyciu. Technologia ta może znacząco wpłynąć na poprawę wydajności.

Następnym zagadnieniem jest stosowanie frazy TABLE-SCAN. Powoduje ona pobranie rekordów bez dostępu do indexów i wyświetlenie ich w porządku, w jakim znajdują się w blokach bazy. Porządek rekordów zmieni się więc po partycjonowaniu, ze względu na przeniesienie ich do innych bloków. Przed pobieraniem wykonywana jest operacja pruning. Zobaczmy poniższy przykład:

FOR EACH order WHERE country = “USA” AND
  OrderDate >= 10-01-2014 AND
  OrderDate <= 12-31-2014 TABLE-SCAN:
DISPLAY OrderNum...

Jak widać, partycjonowanie tabel może wpłynąć na porządek przetwarzanych rekordów. Zobaczmy teraz jak wygląda sytuacja z tworzenie nowych rekordów i ich edytowaniem.

Należy pamiętać, że rekordy muszą mieć określone wartości dla pól, wg których odbywa się partycjonowanie. Dlatego warto stosować instrukcję ASSIGN grupującą ustawienia wartości dla rekordu, zaraz po instrukcji CREATE. Instrukcja ASSIGN wymusza fizyczne utworzenie rekordu w bazie, a więc zadziałanie mechanizmu portycjonowania. Poniższy przykład wywoła błąd (partycjonowanie po polu Country).

CREATE order.
ASSIGN ordernum = NEXT-VALUE(next-ord-num). /* ERROR! */
ASSIGN country = “USA”. 

Przykład ten łatwo poprawić do postaci:

CREATE order.
ASSIGN ordernum = NEXT-VALUE(next-ord-num)
       country = “USA”.

Edycja pola rekordu, wg którego realizowany jest podział tabeli niesie ze sobą przeniesienie całego rekordu do innego obszaru, a więc skasowanie rekordu, utworzenie go w nowym obszarze, aktualizacja indexów. Zbyt częste takie operacje mogą nieść ze sobą obniżenie wydajności. Może to być wynikiem niewłaściwie wybranego klucza partycji, co warto przedyskutować i ew. wybrać inny klucz.

 

 

Partycje Tabel OpenEdge cz. II

W poprzedniej części zapoznaliśmy się nieco z częścią teoretyczną, dotyczącą partycjonowania tabel OpenEdge. Teraz czas zrobić coś w praktyce.

Tworzymy nową bazę np. mytp, kopię bazy sports2000 :

prodb mytp sports2000

Do partycjonowania tabel dane muszą znajdować się w obszarach Typ II. Demonstracyjna baza sports2000 posiada tylko obszary Typ I. Dodajemy zatem obszary, do których przeniesiemy tabele Customer (Cust_Data2) i Order (Order2), a także  obszary, w których będą przechowywane dane po procesie partycjonowania.

Najpierw tworzymy plik add_tp.st

d "Cust_Data2":20,64;8 . f 1280
d "Cust_Data2":20,64;8 .
#
d "Cust_Index2":25,32;8 . f 1280
d "Cust_Index2":25,32;8 .
#
d "Order2":30,64;8 . f 1280
d "Order2":30,64;8 .
#
d "CustomerData":300,64;8 ./customer f 1280
d "CustomerData":300,64;8 ./customer
d "CustomerIndex":301,32;8 ./customer f 320
d "CustomerIndex":301,32;8 ./customer
#
d "OrderData":400,64;8 ./order f 1280
d "OrderData":400,64;8 ./order 
d "OrderIndex":401,32;8 ./order f 320
d "OrderIndex":401,32;8 ./order
#

W katalogu z bazą tworzymy podkatalogi customer i order.
Teraz uruchamiamy polecenie:

prostrct add mytp add_tp.st

Przenosimy tabele Customer i Order do nowych obszarów Typ II:

proutil mytp -C tablemove customer Cust_Data2

proutil mytp -C tablemove order  Order2

Niestety, musimy także przenieść wszystkie indexy do obszarów Typ II (nie będę tego tutaj pokazywać, ale to też proste zadanie). Bazę demo można przygotować na różne sposoby, np. kasując niepotrzebne obiekty lub dump i load do nowej struktury.

Podobnie jak w przypadku CDC, dodamy konfigurację serwera bazy w OE Management (lub OE Explorer).  Po zalogowaniu się wybieramy Resources -> Database. Pojawia się widok Database Migration Utility, w którym podajemy parametry utworzonej bazy mytp wraz z numerem portu, np. 1009. Zaznaczamy Autostart database broker.

Na górnej listwie OE Management (OE Exlporer) klikamy Database Administration i na nazwę bazy: mytp. Pojawia się poniższy ekran.

W sekcji Database Features klikamy Table Partitioning Enable

… i jeszcze w Enable table partitioning. Funkcja jest już włączona w bazie.

W słowniku baz danych dodajemy dla tablicy Order index: OrderDateLocal, obszar: OrderIndex, pole: OrderDate. Analogicznie dla tablicy Customer index: CustomerCountryLocal, obszar: CustomerIndex, pole: Country.

Wracamy do OE Explorera. W sekcji Storage Management klikamy Create partition policy.

Pojawia się poniższy ekran, w którym definiujemy ustawienia dla naszej pierwszej partycji dla tablicy Order. Klikamy Create partition policy. Tworzymy partycję dla tablicy Customer.

Wypełniamy dane jak powyżej (w lookupach wyświetlają się tylko tablice w obszarach Typ II), zaznaczamy: Immediate – set new partitions to allocate space. Klikamy Next.

Będzie to partycja typu List, więc NIE zaznaczamy opcji Has range. Klikamy Add fields from index, wybieramy nowo utworzony index CustomerCountryLocal. Partycja podzieli dane wg pola Country.

Klikamy Next oraz Load Details. Otrzymujemy info, że znaleziono 9 podobszarów dla naszej partycji (jest to związane z wartościami pola Country). Klikamy Next.

Widzimy szczegóły Table Partition Policy. Klikamy Finish.

Zostaje utworzona nowa zasada o nazwie Customer. Dane w tablicy nie są jednak jeszcze podzielone.

Przygotowujemy dane do migracji. Klikamy w Edit Details wchodząc ponownie w szczegóły zasady partycjonowania. Klikamy podwójnie w każdy szczegół np. Austria i zaznaczamy Split-target. Tylko te zaznaczone elementy zostaną podzielone. Oznaczam Split-target wszystkie 9 elementów (czyli dla wszystkich krajów).


Wybieram Commit. Wszystko jest już przygotowane aby przenieść dane z partycji złożonej (Composite) do partycji podzielonych (Split-target).

W oknie komend proenv wpisujemy polecenie:

proutil mytp -C partitionmanage split table customer composite initial useindex

CustomerCountryLocal recs 100

Dane zostały podzielone przy użyciu zdefiniowanego indexu lokalnego. Ilość rekordów w transakcji: 100.

Możemy sprawdzić gdzie znajdują się teraz dane z tablicy Customer uruchamiając dobrze znane polecenie: proutil mytp -C tabanalys

Analiza dla obszaru CustomerData pokazuje podział tablicy na 9 partycji.

To na razie tyle w niniejszym odcinku, ale to nie koniec tematu partycjonowania.

Partycje Tabel OpenEdge cz. I

W ostatnim czasie kilka osób prosiło mnie o wyjaśnienie na czym polega partycjonowanie tabel w bazach OpenEdge. Ten temat czasem powraca w rozmowach z Wami, więc postanowiłem go nieco przybliżyć.

Partycjonowanie tabel (Table Partitioning) to oddzielny produkt dla baz Enterprise, umożliwiający dzielenie dużych tabel na mniejsze części pod względem logicznym zwane partycjami.

Partycje są zaimplementowane na poziomie bazy danych i przezroczyste dla aplikacji. Korzystanie z partycjonowanych tabel może wymagać niewielkich zmian w kodzie aplikacji lub nie wymagać ich wcale.

Uwaga: obiekty podzielone na partycje muszą znajdować się w storage area Typ II.

Partycjonowanie posiada kilka istotnych cech. Partycje w tabeli są niezależne, więc można realizować do nich jednoczesny dostęp, poprawiając wydajność zapytań. Jeśli jedna partycja w tabeli nie jest dostępna, pozostałe partycje są nadal dostępne dla aplikacji.


Każdy rekord tabeli podzielonej na partycje ma te same kolumny.
Każdy rekord może znajdować się tylko w jednej partycji.
Każda partycja może znajdować się we własnym obszarze przechowywania (storage area),
Każda partycja może być niezależnie modyfikowana i zarządzana bez wpływu na inne partycje tej samej tabeli.


Ponadto:
indeksy związane z tabelami podzielonymi na partycje można również podzielić na partycje (tzw. lokalne indeksy).
Podobnie jak partycje tabel, każdy lokalny indeks może znajdować się we własnym obszarze przechowywania i być niezależnie zarządzany. Oprócz indeksów lokalnych mogą istnieć także indeksy globalne dla wszystkich danych w tabeli, bez względu na wydzielone partycje.

Kiedy warto stosować tę technologię?

Np. dla tabel zawierających dane historyczne, które muszą być
archiwizowane. Ciekawą cechą partycji jest to, że jeśli jakaś partycja zawiera dane, które nie mogą być modyfikowane, może być ustawiona tylko do odczytu. W ten sposób w tabeli można modyfikować tylko dane aktualne, a nie zarchiwizowane.

Inne przykładowe przyczyny:

Tabele zawierające dane, które muszą być rozłożone na różnych urządzeniach pamięci masowej.
Duże tabele, które muszą podlegać okresowym operacjom na rekordach i indeksach.
Tabele zawierają kolumny, wg których można logicznie pogrupować dane.
Tabele zawierające dane z częstymi zapytaniami z frazą TABLE-SCAN, a nie WHOLE-INDEX.
Tabele, które będą rosły do bardzo dużych rozmiarów.

Partycjonowanie wykorzystuje jedną kolumnę (tzw. partition key) do jednoznacznej identyfikacji każdej partycji. Podział może być według zakresu (np. zakres dat) lub według listy (lista odrębnych wartości kolumn, np. kraje, województwa, itp.). Kolumna partition key nie może mieć wartości nieokreślonych (“?”).

Należy dodać, że dane w partycji można podzielić na dalsze partycje (możliwe jest 15 poziomów podziału). Np. dzielimy zamówienia (tablica order) według zakresu dat, a następnie każdą partycję dzielimy dodatkowo na subpartycje wg kraju.

 

Sposób w jaki tabele są podzielone jest opisane w tzw. zasadach partycjonowania (partition policy), znajdujących się w meta-schemacie bazy.

W następnym odcinku pokażę jak w praktyczny sposób stworzyć partycje i przenieść do nich dane.

PASOE i serwisy WebHandler cz. II

Reasumując to co można było prześledzić w poprzednim artykule, warstwa transportowa WEB jest związana z interfejsem biznesowym podobnym do REST. Wykorzystuje te same żądania (czasowniki) HTTP (GET, PUT, POST,…). Istotna różnica to przechwytywanie obsługi żądań przez WebHandler, czyli klasę OO ABL. Dzięki temu unika się żmudnego mapowania, a deweloperzy dostają duże możliwości dopasowania obsługi do własnych potrzeb. Dostęp do serwisu jest przez URI, w którym zamiast warstwy REST podaje się WEB oraz nazwę WebHandlera, który obsługuje dany serwis np: webh1/web/webh1.
Webh1 jest tutaj nazwą serwisu, WEB wartswą transportową, a następne wystąpienie webh1, WebHandlerem (domyślny WebHandler ma tę samą nazwę jak serwis).

W drugim przykładzie (także opartym na bazie wiedzy) pokażę jak zdefiniować w projekcie/serwisie dodatkowe URI i jak zrealizować dostęp do 2 obiektów business entity.

Utwórzmy kolejny projekt webh2, typ Server i warstwa transportowa WEB. Projekt jest połączony z baza sports2000. Kto nie wie jak to zrobić niech zajrzy do części I.

Definiujemy business entity: customerBE dla tablicy customer i orderBE dla tablicy order. Zostają utworzone dwie klasy customerBE.cls oraz orderBE.cls (a także pliki .i) – patrz poniżej.

Teraz klikamy prawym klikiem myszy na nazwę serwisu i wybieramy Edit oraz przycisk Add.


Dodajemy URI: /Customer oraz /Orders.

Modyfikujemy plik klasy webh2Handler.cls aby miał następującą postać (dokładniej, modyfikowana jest metoda HandleGet):

USING Progress.Lang.*.
USING OpenEdge.Web.WebResponseWriter.
USING OpenEdge.Net.HTTP.StatusCodeEnum.
USING OpenEdge.Web.WebHandler.
USING Progress.Json.ObjectModel.*.
USING customerBE.*.
USING orderBE.*.  

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS webh2Handler INHERITS WebHandler: 

 {"customerbe.i"}
 {"orderbe.i"}   	
		
	/*------------------------------------------------------------------------------
            Purpose: Handler for unsupported methods. The request being serviced and
            		 an optional status code is returned. A zero or null value means 
            		 this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
	METHOD OVERRIDE PROTECTED INTEGER HandleNotAllowedMethod( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
	
		/* Throwing an error from this method results in a 500/Internal Server Error response. 
        The web handler will attempt to log this exception.
 	    
        See the HandleGet method's comments on choosing a value to return from this method. */
        	
		UNDO, THROW NEW Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
	END METHOD.


	/*------------------------------------------------------------------------------
            Purpose: Handler for unknown methods. The request being serviced and an 
                     optional status code is returned. A zero or null value means 
                     this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
	METHOD OVERRIDE PROTECTED INTEGER HandleNotImplemented( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
	
		/* Throwing an error from this method results in a 500/Internal Server Error response. 
        The web handler will attempt to log this exception.
 	    
        See the HandleGet method's comments on choosing a value to return from this method. */	
		UNDO, THROW NEW Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
   	END METHOD.
 	
	
	/*------------------------------------------------------------------------------
            Purpose: Default handler for the HTTP GET method. The request being 
                     serviced and an optional status code is returned. A zero or 
                     null value means this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
 	METHOD OVERRIDE PROTECTED INTEGER HandleGet( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
 	
	
		DEFINE VARIABLE oResponse AS OpenEdge.Net.HTTP.IHttpResponse NO-UNDO.
        DEFINE VARIABLE oWriter   AS OpenEdge.Web.WebResponseWriter  NO-UNDO.
        DEFINE VARIABLE oBody     AS OpenEdge.Core.String            NO-UNDO.
       
        DEFINE VARIABLE jsonObj AS JsonObject NO-UNDO.
        DEFINE VARIABLE lcJSON AS LONGCHAR NO-UNDO.

        ASSIGN 
            oResponse            = NEW OpenEdge.Web.WebResponse()
            oResponse:StatusCode = INTEGER(StatusCodeEnum:OK).
        

        jsonObj = NEW JsonObject().
        
        IF ENTRY(2,poRequest:PathInfo,"/") = "Customer" THEN
        DO: 
            DEFINE VARIABLE beCustomer AS customerBE NO-UNDO.
            DEFINE VARIABLE hTTCustomer AS HANDLE NO-UNDO.
            
            beCustomer = NEW customerBE().
            beCustomer:ReadcustomerBE("",OUTPUT DATASET dsCustomer).
            hTTCustomer = TEMP-TABLE ttCustomer:HANDLE.
            hTTCustomer:WRITE-JSON ("JsonObject",jsonObj).
            lcJSON= jsonObj:GetJsonText().
            oBody = NEW OpenEdge.Core.String(lcJSON).
                      
            ASSIGN    
                oResponse:Entity = oBody
                oResponse:ContentType   = 'application/json':u
                oResponse:ContentLength = oBody:Size.
        END.
        ELSE
           IF ENTRY(2,poRequest:PathInfo,"/") = "Orders" THEN
           DO:
            DEFINE VARIABLE beOrder AS orderBE NO-UNDO.
            DEFINE VARIABLE hTTOrder AS HANDLE NO-UNDO.
            
            beOrder = NEW orderBE().
            beOrder:ReadOrderBE("",OUTPUT DATASET dsOrder).
            hTTOrder = TEMP-TABLE ttOrder:HANDLE.
            hTTOrder:WRITE-JSON ("JsonObject",jsonObj).
            lcJSON= jsonObj:GetJsonText().
            oBody = NEW OpenEdge.Core.String(lcJSON).
                      
            ASSIGN 
                oResponse:Entity = oBody
                oResponse:ContentType   = 'application/json':u
                oResponse:ContentLength = oBody:Size.
          END.
          ELSE
          DO:
            ASSIGN
                oBody = NEW OpenEdge.Core.String(
                                 'Hello '
                               + '~r~n':u   /*CRLF */
                               + 'This is the default message by th HandleGet in webh2Handler.'
                               ).
            ASSIGN
                oResponse:Entity        = oBody
                oResponse:ContentType   = 'text/plain':u
                oResponse:ContentLength = oBody:Size.                               
         END.
       
        ASSIGN 
            oWriter = NEW WebResponseWriter(oResponse).
        oWriter:Open().
        
        oWriter:Close().
        
        RETURN 0.
		
 	END METHOD. 
 	 	   	
	
END CLASS.

Po uruchomieniu serwera PASOE możemy przetestować działanie serwisu.
Wpisujemy w przeglądarce URL dla danych z tablicy Customer: http://localhost:8810/webh2/web/Customer.

Następnie dla Orders: http://localhost:8810/webh2/web/Orders.

A na końcu testujemy domyślny WebHandler: http://localhost:8810/webh2/web/webh2.

Jeszcze wspomnę, że dodawać WebHandlery i przypisywać do nich URI można na różne sposoby.
Jednym z nich, choć niepolecanym, jest plik konfiguracyjny instancji serwera aplikcaji openedge.properties (podkatalog conf).

O wiele lepszym sposobem jest interfejs webowy OpenEdge Explorer.
Wybieramy: Progress Application Server -> oepas1 -> ABL Application: oepas1 -> ABL WebApp: webh2 -> WEB Transport Configuration

PASOE i serwisy WebHandler cz. I

Deweloperzy aplikacji webowych z wykorzystaniem serwera PASOE mają od wersji 11.6.3 nową możliwość tworzenia serwisów typu Data Object. Są to tzw. WebHandlery, które działają z wykorzystaniem warstwy transportowej WEB (Data Object WebHandler Service).

WebHandler zapewnia bardziej wydajną i prostszą warstwę komunikacyjną niż REST. Jest napisany w ABL, łatwiej dopasować go do własnych potrzeb i ma ulepszone możliwości debugowania. Podstawową zaletą WebHandlera jest to, że daje programistom pełną kontrolę nad przychodzącymi i wychodzącymi danymi.

Warstwa WEB obsługuje żądania (requests) i odpowiedzi (responses), które używają standardowych czasowników HTTP (verbs). Obejmuje to interakcje z klientami, takimi jak WebSpeed i OpenHTTP.
Jeśli chcesz zmienić domyślny adres URL, możesz dodać dodatkowe WebHandlery i zmapować je na różne adresy URL lub zmienić odwzorowania domyślnych WebHandlerów.

Dodatkowe WebHandlery można dodać w pliku openedge.properties dla instancji OEPAS i zmapować je na określone adresy URL. Na przykład:

defaultHandler=OpenEdge.Web.CompatibilityHandler
webhandler1=MyHandler:/mycustomer
webhandler2=MyHandler:/mycustomer/{custid}

OpenEdge.Web.CompatibilityHandler zapewnia kompatybilność z aplikacjami WebSpeed SpeedScript i CGI Wrapper. Jest to domyślny handler używany w instancji w środowisku programistycznym.

Tworzenie tych serwisów zilustruję dwoma przykładami zaczerpniętymi z bazy wiedzy.

Zaczynamy od założenia, że mamy instancję serwera PASOE z podłączoną bazą danych sports2000.
Tworzymy nowy projekt OpenEdge. Nadajemy mu nazwę np. webh1, wybieramy typ Server i zaznaczamy warstwę transportową WEB.

Klikamy Next, sprawdzając czy do projektu jest podłączona instancją serwera aplikacji, a w ostatnim kroku upewniamy się, że jest przyłączona nasza baza danych.

Kreator tworzy od razu klasę WebHandlera, w tym przypadku webh1Handler.cls, zawierającą 3 metody: HandleNotAllowedMethod, HandleNotImplemented oraz HandleGet. Dwie pierwsze są związane z wychwytywaniem błędów. HandleGet to główna metoda, w której można przetwarzać dane otrzymane z odpowiedzi (response).

Stworzymy teraz obiekt danych. W Project Explorerze zaznaczamy nazwę projektu i prawym klikiem myszy wybieramy New -> Business Entity.


Podajemy nazwę customerBE, klikamy Next i wybieramy z bazy tabelę customer.

W efekcie jest utworzona znana z poprzednich artykułów klasa customerBE.cls.
Do klasy tej będziemy odnosić się z WebHandlera, czyli pliku webh1Handler.cls. Teraz należy zmodyfikować metodę HandleGet aby miała postać tak jak w poniższej klasie webh1Handler.cls (można skopiować cały plik klasy).

Widać, że oprogramowanie metod wymaga znajomości programowania obiektowego i odpowiednich referencji, ale daje bardzo duże możliwości. W poniższym przykładzie w odpowiedzi HTTP pobierana jest zawartość tablicy ttCustomer (z obiektu customerBE) i wyświetlana poprzez obiekt JSON.

USING Progress.Lang.*.
USING OpenEdge.Web.WebResponseWriter.
USING OpenEdge.Net.HTTP.StatusCodeEnum.
USING OpenEdge.Web.WebHandler.
USING customerBE.*.
USING Progress.Json.ObjectModel.*.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS webh1Handler INHERITS WebHandler: 

	    {"customerbe.i"}
		
	/*------------------------------------------------------------------------------
            Purpose: Handler for unsupported methods. The request being serviced and
                     an optional status code is returned. A zero or null value means 
            	     this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
	METHOD OVERRIDE PROTECTED INTEGER HandleNotAllowedMethod( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
	
        /* Throwing an error from this method results in a 500/Internal Server Error response. 
        The web handler will attempt to log this exception.
 	    
        See the HandleGet method's comments on choosing a value to return from this method. */
        	
		UNDO, THROW NEW Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
	END METHOD.


	/*------------------------------------------------------------------------------
            Purpose: Handler for unknown methods. The request being serviced and an 
                     optional status code is returned. A zero or null value means 
                     this method will deal with all errors.                                                               
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
	METHOD OVERRIDE PROTECTED INTEGER HandleNotImplemented( INPUT poRequest AS OpenEdge.Web.IWebRequest ):
	
	/* Throwing an error from this method results in a 500/Internal Server Error response. 
        The web handler will attempt to log this exception.
 	    
        See the HandleGet method's comments on choosing a value to return from this method. */	

		UNDO, THROW NEW Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
   	END METHOD.
 	
	
	/*------------------------------------------------------------------------------
            Purpose: Default handler for the HTTP GET method. The request being 
                     serviced and an optional status code is returned. A zero or 
                     null value means this method will deal with all errors.                                                           
            Notes:                                                                        
    ------------------------------------------------------------------------------*/
 	METHOD OVERRIDE PROTECTED INTEGER HandleGet( INPUT poRequest AS OpenEdge.Web.IWebRequest ): 	

	DEFINE VARIABLE oResponse AS OpenEdge.Net.HTTP.IHttpResponse NO-UNDO.
        DEFINE VARIABLE oWriter   AS OpenEdge.Web.WebResponseWriter  NO-UNDO.
        DEFINE VARIABLE oBody     AS OpenEdge.Core.String            NO-UNDO.
             
        DEFINE VARIABLE beCustomer AS customerBE NO-UNDO.
        DEFINE VARIABLE pcFilter AS CHAR NO-UNDO.
        DEFINE VARIABLE lcJSON AS LONGCHAR NO-UNDO.

        DEFINE VARIABLE jObj AS JsonObject.
        DEFINE VARIABLE htt AS HANDLE.
        
        jObj = NEW JsonObject().
        /* Get data from the BE */
        beCustomer = NEW customerBE().
        beCustomer:ReadcustomerBE(INPUT pcfilter,OUTPUT DATASET dsCustomer).
        htt = TEMP-TABLE ttCustomer:HANDLE.
        htt:WRITE-JSON("JsonObject", jObj).

        ASSIGN 
            oResponse            = NEW OpenEdge.Web.WebResponse()
            oResponse:StatusCode = INTEGER(StatusCodeEnum:OK)
                        .
        /* This body object can be a string or something else (JsonObject for instance) */
        lcJSON= jObj:GetJsonText().
        ASSIGN 
            oBody = NEW OpenEdge.Core.String(lcJSON).            
                              
        ASSIGN 
            oResponse:Entity        = oBody
            /* HTTP messages require a content type */
            oResponse:ContentType   = 'application/json':u
            /* ContentLength is good too */
            oResponse:ContentLength = oBody:Size
            .
        
        /* The WebResponseWriter ensures that the status line and
           all headers are writted out before the message body/entity. */
        ASSIGN 
            oWriter = NEW WebResponseWriter(oResponse).
        oWriter:Open().
        
        /* Finish writing the response message */
        oWriter:Close().
        
        /* A response of 0 means that this handler will build the entire response;
           a non-zero value is mapped to a static handler in the webapp's /static/error folder.
           The mappings are maintained in the webapps's WEB-INF/web.xml 
           A predefined set of HTTP status codes is provided in the OpenEdge.Net.HTTP.StatusCodeEnum 
           enumeration */
        RETURN 0.
		
 	END METHOD. 
 		
END CLASS.

W przeglądarce wpisujemu URL dla naszej instancji PASOE np.: http://localhost:8810/webh1/web/webh1 (lub https://localhost:8811/webh1/web/webh1 dla bezpiecznego połączenia).
W efekcie dostajemy ekran podzielony na 3 przekładki:
JSON

Raw Data

Headers

Change Data Capture cz. II

Kontynuujemy temat związany z praktycznym wykorzystaniem technologii Change Data Capture opisanej w poprzednim artykule.

Mamy zatem bazę danych myCDC z włączoną funkcją CDC i zdefiniowanymi obszarami dla danych i indexów CDC.

Wejdżmy jeszcze raz w OE Explorerze w opcję Database Administration.

W sekcji Storage Management znajdziemy opcje, w których możemy zmodyfikować listę tabel lub stworzyć/zmodyfikować zasady CDC (CDC policy).
W sekcji Data Administration znajdują się opcje dla zrzucenia lub załadowania zdefiniowanych zasad CDC.

Podczas tworzenia zasad CDC należy określić poziom (Level), który określa ilość zapisywanych danych. Ilustruje to poniższa tabela.

Poziom Opis Fieldmap Czy można zmienić poziom?
 0  Przechowywanie danych tylko w Tracking Table  Brak  Nie
 1   Przechowywanie danych tylko w Tracking Table. Zawiera Fieldmap  Odzwierciedla zmienione pola tylko dla aktualizacji  Tak
 2  Zapis aktualnych wartości (after) dla wszystkich operacji CUD (Create, Update, Delete)   Odzwierciedla zmienione pola tylko dla aktualizacji  Tak
 3   Zapis aktualnych wartości (after) oraz poprzednich (before) dla wszystkich operacji CUD
  Odzwierciedla zmienione pola tylko dla aktualizacji  Tak

Pole Fieldmap jest wykorzystywane wtedy gdy interesuje nas, które pola zastały zmienione, ale nie interesują nas same wartości.

Utwórzmy teraz zasadę CDC poziom 2.

Na stronie Data Administration, w sekcji Storage Management, wybieramy Create Change Data Capture policy.

Wprowadzamy dane: Policy name: CustomerPolicy, Table: PUB.Customer, Level: Medium(2). Wartość State zostawiamy jako Inactive. Zasadę uaktywnimy później.

W polach Data area i Index area podajemy nazwy zdefiniowanych w bazie obszarów, jak na powyższym rysunku.

Jeśli nie podamy wartości Change table, to przyjmie ono domyślna nazwę, tutaj: CDC_Customer. Wartośc pola Change table owner przyjmie wartość pub.

Jeśli chcemy wybrać pola, których wartości mają być przechwytywane, zaznaczamy Identifying fields.

Z listy poniższych pól wybieramy: City, Country, CustNum, Name (zaznaczamy checkbox w pierwszej kolumnie). Dla pola CustNum w kolumnie Enable identifying field wybieramy YES, a wartość pola Field order ustawiamy 1. Dla tego pola zostanie utworzony index w tabeli Change Table.

Przyciskiem SUBMIT (na górze strony) tworzymy zasadę CDC.

Powinniśmy teraz widzieć poniższy ekran.

Wybieramy w górnym menu: Database Administration i Go to Database Administration.

Widzimy zdefiniowaną zasadę CDC dla tabeli Customer. W kolumnie Policy state widać No Current Policy ponieważ nie uaktywniliśmy jeszcze tej zasady. Klikamy na nazwę CustomerPolicy w polu Pending policy.

Zaznaczamy Active i przycisk SUBMIT.

Zasada CustomerPolicy jest już aktywna. Zasadę mogliśmy uaktywnić oczywiście wcześniej, podczas definiowania.

Pora napisać jakiś program. Otwieramy klienta podłączonego do bazy myCDC (np. komenda prowin myCDC) i uruchamiamy “skomplikowany” program składający się z instrukcji CREATE customer.

Następnie uruchamiamy program Customer_CTT.p oparty na schemacie z tablicy Change Tracking Table.

// Customer_CTT.p

 FOR EACH _file, each _cdc-change-tracking WHERE _file._file-number = 
          _cdc-change-tracking._source-table-number AND _file._file-name="customer":
  CASE _operation:

    WHEN 1 THEN
      DISPLAY "Create"  _operation  _file._FILE-NAME.
    WHEN 2 THEN  
      DISPLAY "Delete"  _operation  _file._FILE-NAME.
    WHEN 3 THEN  
      DISPLAY "Before Update"  _operation  _file._FILE-NAME.
    WHEN 4 THEN  
      DISPLAY "After Update" _operation  _file._FILE-NAME.
  END CASE.
END.

Widzimy informację, że została wykonana operacja 1 (Create) i utworzony został jeden rekord Customer.

W drugim przykładzie Customer_CT.p korzystamy z danych zapisanych do Change table CDC_Customer.

// Customer_CT.p

FOR EACH cdc_customer :
  DISPLAY _operation country city custNum name.
end.

W Change table zapisywane są jedynie pola wybrane podczas tworzenia zasady (wartości pól CustNum i Country ustawiane są przez tryger Create).

Wprowadźmy następną zmianę w danych.np. pole name dla pierwszego Customera = “Lift Tours Corp.”.

Customer_CTT.p

Mamy tu już dwie operacje na tablicy Customer: Create i After Update.

Customer_CT.p

Widzimy nową wartość pola Name oraz wartość CustNum (jest to Identifying field). Wartości pozostałych pól nie zmieniły się i są wyświetlone jako ?.

Powyższe przykładowe proste programy ilustrują sposób odczytu zmodyfikowanych danych dla zasady na poziomie 2. Poziom można ustawić w zależności od potrzeb. Tak uzyskane dane moga słuzyć różnym celom. Może to być element systemu ETL (o czym wspomniałem w pierwszej części) czy np. naszego własnego systemu do replikacji wybranych informacji.

Change Data Capture cz. I

Progress OpenEdge Change Data Capture (CDC) jest mechanizmem śledzącym, który umożliwia aplikacjom zaimplementowanie procesu, który automatycznie rejestruje zmiany w tabelach użytkowników w bazie danych OpenEdge.

Obsługuje on również automatyczne kopiowanie podzbiorów danych, które uległy zmianie do specjalnych tablic zmian (Change Tables). Zebrane przez CDC dane mogą być wykorzystane przez narzędzia wspomagające procesy ETL (Extract, Transform, Load).

Jednym z przykładówich ich wykorzystania jest identyfikacja trendów na podstawie danych w hurtowni danych, która została zapełniona informacjami z CDC change tables przy użyciu procesu ETL.

Change Data Capture to nowy produkt w OE 11.7. Można go kupić oddzielnie lub razem z licencją OE Advance Enterprise RDBMS. Funkcje wspierające CDC można znaleźć w OpenEdge SQL, OpenEdge ABL, OpenEdge Management, OpenEdge Explorer i w innych obszarach.

Dane dostarczane przez proces przechwytywania znajdują się w źródłowej bazie danych i są przechowywane w formie relacyjnej.

OpenEdge CDC jest elastyczny, ponieważ:

  • Przechwycone dane są przechowywane w tej samej bazie danych
  • Przechwycone dane są utrzymywane w formie relacyjnej
  • Przechwycone dane są dostępne poprzez składnie SQL i ABL.

OpenEdge CDC jest skalowalny, ponieważ można zdefiniować zasady (policy) CDC takie, że:

  • Ilość przechwyconych danych może być różna dla różnych tabel. Można nie przechwytywać żadnych danych, wybrane dane lub cały rekord
  • Można indeksować dane w celu łatwiejszego wyszukiwania informacji
  • Ilość przechwyconych danych jest kontrolowana za pomocą zasad zdefiniowanych na poziomie tabeli i pola.

Niektóre zalety OpenEdge Change Data Capture to:

  • Identyfikacja i śledzenie wszystkich zmian danych w bazie OpenEdge
  • Gwarancja dokładności śledzenia wszystkich zmian danych bez względu na miejsce ich występowania
  • Zwiększenie efektywności i dostępności zmian dla ETL w celu synchronizacji określonych zmian z
    innymi źródłami danych, repozytoriami danych lub hurtowniami danych
  • Jedno miejsce konfiguracji niezależnie od ABL lub SQL
  • Możliwość aktywacji bez jakichkolwiek zmian w kodzie aplikacji, wystarczy tylko skonfigurować i uruchomić
  • Możliwość zarządzania całkowicie online – bez żadnych przestojów.

Włączenie mechanizmu CDC powoduje dodanie dwóch tabel do metaschematu bazy: _Cdc-Table-Policy oraz _Cdc-Field-Policy. Tabele te zawierają informacje o zasadach CDC dla tabel i powiązanych z nimi pól.

Oprócz tego dodawane są tabele: Change Tracking Table (_Cdc-Change-Tracking) i oddzielne tabele CDC dla każdej tabeli źródłowej, dla której chcemy przechwytywać zmiany danych (wspomniane wcześniej Change Tables).

Tabela _Cdc-Change-Tracking przechowuje zapis wszystkich działań związanych ze zmianami danych we wszystkich tablicach źródłowych, zgodnie z zasadami CDC. Przechwytywanie jest wykonywane poprzez specjalne trygery CDC, wbudowane w silnik bazy. Zawiera także informacje niezbędne do zabezpieczenia sekwencji transakcyjnej. Następnie dane te są zapisywane przez trygery do tabel Change Tables, o ile poziom zasad jest większy niż zero (w tym przypadku wszystkie dane znajdują się tylko w Change Tracking Table).

Przed włączeniem mechanizmu CDC należy przydzielić miejsce dla nowych tabel i indeksów. Ważnym zadaniem administratora jest monitorowanie przyrostu tych danych oraz zarządzanie nimi.

OK, po tym przydługawym wprowadzeniu zabierzmy się do pracy i stwórzmy bazę przygotowaną do CDC. Pamiętajmy, że aby CDC można było włączyć, baza musi zawierać obszary typu II.

Najpierw tworzymy nową bazę np. myCDC, kopię bazy sports2000 poleceniem:

prodb myCDC sports2000

Do bazy dodajemy obszary, w których będą przechowywane dane przechwycone przez CDC. Najpierw tworzymy plik add_CDC.st

#
d "CDC_Track_Data":20,64;512 .
#
d "CDC_Track_Index":21,1;64 .
#
d "CDC_Customer_Data":25,64;512 .
#
d "CDC_Customer_Index":26,1;64 .
#
d "CDC_Misc_Data":27,64;512 .
#
d "CDC_Misc_Index":28,1;64 .
#

Teraz uruchamiamy polecenie:

prostrct add myCDC add_CDC.st

którego efekt widać na poniższym obrazku.

Teraz skorzystamy z narzędzia webowego OE Explorer lub OE Management. Po zalogowaniu się wybieramy Resources -> Database. Pojawia się widok Database Migration Utility, w którym podajemy parametry utworzonej bazy myCDC wraz z numerem portu, np. 1005. Zaznaczamy Autostart database broker.

Po naciśnięciu SUBMIT powinien pojawić się poniższy widok.

Teraz w menu głównym klikamy Database Administration i Go to Database Administration.

Na liście baz danych klikamy naszą bazę myCDC.

Pojawia się ekran z kilkoma widokami. Po prawej stronie znajduje się widok Database Features, na której odnajdziemy wyłączona funkcję Change Data Capture.

Klikamy Enable.

Teraz musimy podać w jakich obszarach będą zapisywane dane i indeksy. Klikamy ikonki z lupą i wybieramy odpowiednie obszary.

Teraz klikamy Enable change data capture.

Powinien pojawić się poniższy komunikat.

Włączenie CDC możemy wykonać także bezpośrednio z linii komend np:

proutil myCDC -C enablecdc area Track_CDC_Data indexarea CDC_Track_Index

Na razie wystarczy. W następnym odcinku zdefiniujemy zasady CDC i napiszemy kilka prostych programów ilustrujących wykorzystanie informacji z change tables.

Tworzenie projektów w OE Developer Studio 11.7.2

Podczas kilku wcześniejszych artykułów pokazywałem jak utworzyć różne proste projekty w OpenEdge Developer Studio. Służyły one np. do wystawienia serwisów REST z wykorzystaniem obiektów JSDO i automatycznym generowaniem klas lub bardziej złożone z ręcznym mapowaniem parametrów. Przykłady te dotyczyły nowego serwera PASOE choć można było wykorzystać także klasyczny AppServer.

Takie tworzenie projektów odbywa się poprzez tzw. wizardy, których zadaniem jest “poprowadzenie za rękę” dewelopera, aby ułatwić mu życie. Jeśli idzie o intuicyjność wizardów, np. dla projektów typu OpenEdge, których jest kilka rodzajów, to pozostawiały one nieco do życzenia i bez znajomości zdobytej np. poprzez webinary lub dokukmentację łatwo można było popełnić błąd.

W ostatnich wersjach OpenEdge proces ten został poprawiony a jego klarowność jest zdecydowanie lepsza.

Tworząc nowy projekt typu OpenEdge od razu pojawia się okno (poniżej), w którym rodzaje projektów są pogrupowane w 3 grupy: Server, Client, General. Wybierając grupę i typ projektu w polu Description pojawia się krótki opis.


W grupie Server można zdecydować czy wykorzystany będzie serwer PASOE czy klasyczny AppServer.

Dla PASOE można dalej wybrać warstwę transportową: APSV, REST lub WEB.
Pierwsze dwie warstwy były omawiane na tym blogu. APSV służy do obsługi procedur ABL poprzez HTTP.
REST to projekt, w którym możemy wykorzystać wywołania RESTful API i ręcznie mapować wybrane pola. Jeśli zaznaczymy Create a DataObject Service, będzie to projekt z automatycznym mapowaniem struktur ProDataSet do JSDO.

WEB służy do utworzenia projektu z wykorzystaniem wywołań RESTful API przez klasę WebHandler. Ten rodzaj nie był jeszcze omawiany na blogu.

Jeśli zamiast PASOE wybierzemy klasyczny AppServer to będziemy miali do wyboru 2 rodzaje: APSV i REST.

Grupa Client to przede wszystkim projekty związane z budowaniem aplikacji desktopowej, może to być aplikacja GUI for .NET, z wykorzystaniem AppBuildera lub znakowa (ChUI). Można także budować interfejs webowy WEB UI lub klasyczny WebSpeed.

Grupa General, to podstawowy projekt oraz pozostałe z grupy OpenEdge, np. Dynamics lub (omawiany na Pizza & Beer) ABLUnit do przeprowadzania testów ABL. Można wreszcie stworzyć projekt Custom z konfiguracją dopasowaną do własnych potrzeb.

Procedury ABL i serwer aplikacji PAS cz.II

Kontynuując omawianie różnych zagadnień związanych z uruchamianiem aplikacji ABL na serwerze PAS zobaczmy jak osiągnąć połączenie w różnych trybach pracy, charakterystycznych dla klasycznego AppSerwera, co może być istotne podczas procesu migracji aplikacji.

W poprzedniej części zobaczyliśmy jak przyłączyć się do serwera PAS w modelu session-free i session-managed. Wybór modelu należy do klienta, a nie serwera aplikacji, którego pojedyncza instancja potrafi obsłużyć jednocześnie oba modele.

Dla session-free nie potrzebne są żadne zmiany w aplikacji poza samą instrukcją przyłączenia.

Dla session-managed mamy w AppSerwerze 3 tryby pracy:

  • state-reset
  • state-aware
  • stateless

Tryby te związane są z zarządzaniem kontekstem między żądaniami i wprowadzono je dla ułatwienia procesu programowania.

Przywiązanie procesu agenta do określonej sesji klienta powoduje zachowanie kontekstu, ale zmniejsza skalowalność. Różne tryby pracy dają więc programoście możliwość wyboru. Istotne jest, że tryby te w klasycznym AppSerwerze wybiera się podczas jego startu i nie mogą być zmienione jak długo serwer pracuje.

Jeśli więc będziemy potrzebować wszystkie trzy tryby jednocześnie, należy uruchomić trzy procesy AppServera. W serwerze PAS wystarczy jeden proces, a sposób pracy określa jedynie kod programu klienckiego.

Przypomnijmy, że dla serwera PAS połączenie z wybraną sesją ABL może być warunkowo związane/zwolnione poprzez ustawienie atrybutu SESSION:SERVER-CONNECTION-BOUND-REQUEST.

Zdefiniujmy teraz dwie procedury:
bound.p

/* bound.p  */
.....
SESSION:SERVER-CONNECTION-BOUND-REQUEST = TRUE.

oraz unbound.p

/* unbound.p  */
.....
SESSION:SERVER-CONNECTION-BOUND-REQUEST = FALSE.
QUIT.

Pierwsza procedura (patrz poprzedni artykuł) wiąże sesję klienta ABL z sesją PAS, drugą ją zwalnia.
Przejdżmy teraz do ustawień serwera PAS w OE Explorer -> Startup Parameters and Environment. Możemy podać tu nazwy procedur dla różnych operacji. Podobne ustawienia są możliwe na klasycznych AppSerwerze.

Dla trybu state-reset wystarczy podać nazwy tych procedur dla Connect i Disconnect jak na powyższym rysunku. Komenda QUIT powoduje zresetowanie sesji ABL. Jeśli procedury te zawierają inne Wasze instrukcje, to instrukcja wiążąca powinna być na początku w bound.p a procedura zwalniająca i QUIT na końcu unbound.p.

Dla trybu state-aware postępujemy analogicznie, tylko że w procedurze unbound.p NIE umieszczamy instrukcji QUIT.

Dla trybu stateless nie trzeba robić żadnych zmian ponieważ wiązanie i zwalnianie sesji może wystąpić w różnych miejscach logiki biznesowej i zależy od dewelopera aplikacji.

Load Balancing. Jeśli posiadamy produkt Name Server Load Balancer, to może konfigurować balansowanie obciążeniem klasycznego AppSerwera. Polega to z grubsza na dodaniu logicznej nazwy serwisu w kilku instancjach AppSerwera i określeniu procentu obciążenia w polu Weight.

Serwer aplikacji PAS nie łączy się jednak przez Name Server i nie da się tego tutaj tak zrobić. Load balancing dla PASOE można skonfigurować korzystając ze standardów HTTP takich jak: Apache proxy host load balancing, Tomcat load balancing, Amazon load balancing. Zainteresowanych tym tematem odsyłam do dokumentacji: Progress® Application Server for OpenEdge®: Administration Guide.

Procedury ABL i serwer aplikacji PAS cz.I

Od kilku miesięcy informujemy o nowych możliwościach serwera aplikacji PAS. Pytanie co z uruchamianiem procedur ABL, które działały do tej pory na klasycznym AppSerwerze? Czy trzeba je modyfikować? Czy migracja jest trudna dla przeciętnego programisty? Na te i podobne pytania postaram się odpowiedzieć w tym i następnym artykule.

Zacznijmy od uruchomienia instancji serwera aplikacji PAS o nazwie oepas1 (podobnie jak w poprzednich artykułach).
Przypominam, że PAS jest podłączony do bazy sportsdb, a w PROPATH znajduje się podkatalog openedge, do którego skopiujemy procedury abl (patrz poniżej).

Do uruchamiania procedur openedge potrzebny jest aktywny adapter, obsługujący warstwę transportową apsv. Sprawdzamy czy jest on aktywny w OpenEdge Explorerze lub wpisując w przeglądarce url: localhost:[port]/apsv. Jak widać poniżej adapter jest aktywny.


Teraz możemy uruchomić na kliencie program podłączający się do serwera aplikacji i uruchamiający tę samą procedurę FindCustomerByName, którą uruchamialiśmy dla web serwisów SOAP.
Najpierw zrobimy to dla modelu SESSION-FREE.

/* SESSION-FREE  */

DEFINE VARIABLE hServer AS HANDLE NO-UNDO.
DEFINE VARIABLE lReturn AS LOGICAL NO-UNDO.
DEFINE VARIABLE CustomerNumber AS CHARACTER NO-UNDO.

CREATE SERVER hServer.
lReturn = hServer:CONNECT("-URL http://localhost:8810/apsv
  -sessionModel Session-free"). 

IF NOT lReturn THEN DO:
  DELETE OBJECT hServer NO-ERROR.
  RETURN ERROR "Failed to connect to the ABL web application: " + RETURN-VALUE.
END.

RUN FindCustomerByName ON hServer (INPUT "Lift Tours", INPUT-OUTPUT CustomerNumber).

MESSAGE CustomerNumber
    VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.

lReturn = hServer:DISCONNECT().
DELETE OBJECT hServer NO-ERROR. 

Kod podłączenia i uruchomienia procedury jest bardzo podobny jak dla klasycznego AppServera.

W OpenEdge Explorerze w widoku Sessions widać otwartą sesję ze stanem Unbound (Bound=false), czyli sesja agenta nie jest powiązana z klientem.


Kod dla podłączenia w modelu SESSION-MANAGED różni się tylko dla metody CONNECT. Jego działanie jest identyczne jak w powyższym przykładzie.

/* SESSION-MANAGED */

DEFINE VARIABLE hServer AS HANDLE  NO-UNDO.
DEFINE VARIABLE lReturn AS LOGICAL NO-UNDO.
DEFINE VARIABLE CustomerNumber AS CHARACTER NO-UNDO.

CREATE SERVER hServer.
lReturn = hServer:CONNECT
  ("-URL http://localhost:8810/apsv","","").

IF NOT lReturn THEN DO:
  DELETE OBJECT hServer NO-ERROR.
  RETURN ERROR "Failed to connect to the ABL web application: " + RETURN-VALUE.
END.

RUN FindCustomerByName ON hServer (INPUT "Lift Tours", INPUT-OUTPUT CustomerNumber).

MESSAGE CustomerNumber
    VIEW-AS ALERT-BOX INFORMATION BUTTONS OK.

lReturn = hServer:DISCONNECT().
DELETE OBJECT hServer NO-ERROR. 

Ponieważ uruchomiłem 3 takie same programy na kliencie, to w widoku Agents mam co prawda dalej tylko jednego agenta, ale 3 jego sesje.


Żeby powiązać daną sesję agenta z klientem stosujemy tę samą sztuczkę jak w przypadku klasycznego AppSerwera: w procedurze uruchamianej na serwerze aplikacji dodajemy komendę:

SESSION:SERVER-CONNECTION-BOUND-REQUEST = TRUE.

Powiązanie stosuje się aby łatwiej zarządzać kontekstem, gdyż ta sama sesja agenta może obsłużyć kilka/wiele żądań pomiędzy aplikacją kliencką a daną sesją. Zwolnienie sesji odbywa się zazwyczaj przez uruchomienie tej samej komendy z wartością FALSE.  Poniżej, w widoku Sessions widać 3 sesje ze stanem Bound (Bound=true).


Ciąg dalszy niebawem w drugiej cześci…

1 2 3 4 5