Triggery logiki biznesowej¶
Jak pisać "triggery" w C# a nie w SQL?¶
W Neosie najczęściej używamy obiektów biznesowych opartych o tabele w bazie danych. Takie obiekty służą do odczytu i zapisu danych (np przez okna edycji). Rzadziej używamy obiektów opartych o zapytanie SQL, które zwykle służą jedynie do odczytu (prezentacji) danych. W związku z powyższym, obiekty oparte o tabele stanowią niejako logiczne odzwierciedlenie samych tabel w bazie danych. Ponieważ do tej pory w bazie danych pisaliśmy triggery SQL na konkretnych tabelach, to w Neosie 6.0 pojawiły się metody logiki biznesowej symulujące zachowanie triggerów. Dzięki temu:
-
unikamy pisania logiki biznesowej w bazie danych. Co prawda nie powinniśmy jej przenosić bezpośrednio na triggery w C#, ale możemy na tych triggerach zgłosić zdarzenia EDA, w wyniku których, asynchronicznie wydarzą się kolejne operacje, ważne z punktu widzenia logiki biznesowej
-
mamy możliwość wykonania dodatkowej, aczkolwiek prostej logiki w C# przy okazji dodawania / poprawiania / usuwania rekordów bez otaczania metod zawierających
PostRecord()iDeleteRecord()dodatkowym kodem, a więc nie zmuszamy programisty, który ma jedynie użyć danego obiektu biznesowego, aby wiedział, że coś jeszcze powinien wykonać. Zalecenia odnośnie tego, co powinniśmy a czego nie powinniśmy pisać w triggerach C# opisano poniżej.
Każdy obiekt biznesowy ma wbudowane w standardzie następujące metody logiki biznesowej:
-
public void OnBeforeCreate(TModel @new)
-
public void OnAfterCreate(TModel @new)
-
public void OnBeforeUpdate(TModel @old, TModel @new)
-
public void OnAfterUpdate(TModel @old, TModel @new)
-
public void OnBeforeDelete(TModel @old)
-
public void OnAfterDelete(TModel @old)
-
public void OnValidate(TModel @old, TModel @new)
-
public void InternalCreate(TModel model)
-
public void InternalUpdate(TModel model)
-
public void InternalDelete(TModel model)
Powyższe metody możemy dziedziczyć i pisać w nich własny kod. W poniższej tabeli zebrano podstawowe zasady, mówiące o tym, kiedy należy dziedziczyć konkretną metodę i jaki kod w niej pisać.
| Metoda | Przeznaczenie |
|---|---|
| OnBeforeCreate OnBeforeUpdate |
Służy do ustawienia wartości pewnych pól modelu przed ich zapisem do bazy danych. W klasie bazowej ta metoda nie robi nic, czyli jakąkolwiek implementację uzyskujemy dopiero w wyniku dziedziczenia. Przykładem jest inicjacja wartości pól modelu danych podczas dodawania nowego rekordu w obiekcie `WORKFLOW.WFACTIVITY`:
|
| OnBeforeDelete | Służy do sprawdzenia, czy na pewno dany rekord wolno usunąć. Jeśli nie, należy zgłosić wyjątek. Bazowa metoda nie robi nic. |
| OnAfterCreate OnAfterUpdate OnAfterDelete |
Służy np do zgłoszenia zdarzeń EDA oznajmiających, że dane powiązane z obiektem biznesowym uległy zmianie. Można także bezpośrednio wywołać inne metody realizujące pewną logikę biznesową, ale jeśli są to złożone operacje upewnijmy się, że wywołują się one asynchronicznie, najlepiej poprzez zlecenie komend EDA. Bazowa metoda nie robi nic. Przykłady:
- po aktualizacji pozycji wywołujemy tu metodę przeliczającą nagłówek
- wywołujemy tu metodę indeksacji tego rekordu w silniku GQS. Taką implementację mamy w WORKFLOW.WFFOLDER:
|
| OnValidate | Służy do walidacji modelu przed dodaniem lub zmodyfikowaniem rekordu w bazie danych. Jeśli uznajemy, że warunki walidacji nie są spełnione, należy zgłosić wyjątek. Bazowa metoda nie robi nic. |
| InternalCreate InternalUpdate InternalDelete | Bazowo te metody realizują fizyczny zapis danych do bazy danych dla obiektów opartych o tabele oraz dla obiektów opartych o zapytanie SQL ze wskazaną tabelą natywną. Dziedziczymy je wtedy, kiedy w modelu posiadamy dodatkowe pola, które nie są automatycznie zapisywane w tabeli natywnej i piszemy kod, który te pola “gdzieś” zapisuje. Możemy także całkowicie przejąć kontrolę nad zapisem i nie wywoływać metod “base.Internal…”. W przypadku obiektów opartych o programowe źródło danych (metoda Fill), dla których chcemy mieć możliwość dodawania, edycji i usuwania rekordów należy obowiązkowo odziedziczyć te metody i napisać kod, który obsługuje zapis do bazy danych i usuwanie rekordów z bazy danych. |
Kod tych metod zachowuje się jak kod triggerów w bazie danych. Podana jako przykład metoda OnBeforeCreate odwołuje się do pól klasy TModel, która została przekazana w parametrze. Klasa ta jest generowana niezależnie dla każdego obiektu biznesowego i zawiera wszystkie pola modelu danych danego obiektu. Typy pól są typami języka C# odpowiednimi dla typów bazodanowych. Np polu w bazie danych typu INTEGER odpowiada typ int? w jezyku C#. Pytajnik na końcu typu mówi o tym, że jest to typ "nullable", czyli mogący przechować albo konkretną wartość, albo wartość "null". Umożliwia to reprezentowanie wartości NULL znanej z języka SQL. Obiekty @new i @old są równoznaczne z new i old w triggerach pisanych w SQL.
Inny przykład podany wyżej dotyczy zastosowania triggera w C# do wywołania metody odpowiedzialnej za zindeksowanie rekordu w silniku wyszukiwania GQS. Jeśli sami napiszemy odpowiednią metodę Index...InGQS, to najlepiej wywołać ją właśnie w metodach OnAfterCreate(), OnAfterUpdate() i OnAfterDelete(). Pamiętajmy jednak o tym, aby te metody nie realizowały skomplikowanej logiki biznesowej w sposób synchroniczny. Najlepiej aby jedynie generowały odpowiednie komendy EDA.
Implementując kod metod triggerów pamiętajmy o wywoływaniu metod bazowych. Co prawda ich standardowe implementacje są puste, ale jeśli będziemy dziedziczyć obiekty biznesowe, to wywołanie base.OnBeforeCreate i podobnych spowoduje wykonanie triggera obiektu po którym dziedziczymy. To od programisty zależy, czy wywołanie base będzie na początku, na końcu czy w środku metody. Wszystko zależy od tego, czy kod który piszemy traktujemy jako ważniejszy niż kod przodka (np jeśli zarówno nasz kod jak i przodka nadaje wartość jakiegoś pola modelu), czy mniej ważny.
Jak zachowują się triggery w C#?¶
Aby kod triggerów C# w pełni odpowiadał działaniu triggerów w SQL, przyjęto założenie, że cała operacja dodania / poprawienia lub usunięcia rekordu odbywa się całkowicie po stronie serwera neos w jednej transakcji bazodanowej. Przykładowo jeśli edytujemy dane na oknie kartoteki klienta i naciśniemy przycisk "Zapisz", to Neos wykona metodę PostRecord(), która na poziomie logiki biznesowej wykona następujące czynności:
-
Otwarcie transakcji bazodanowej do procesu zapisu
-
Odczytanie z bazy danych aktualnej postaci zapisywanego rekordu i umieszczenie go w modelu
@old. -
Odfiltrowanie faktycznych zmian dokonanych na oknie w procesie edycji i umieszczenie ich w modelu
@new. -
Wykonanie metody
OnBeforeUpdate(@old, @new) -
Wykonanie metody
OnValidate(@old, @new) -
Wykonanie metody
InternalUpdate(model), w ramach której odbywa się fizyczny zapis do bazy danych (update TABELA set ...) -
Odświeżenie w modelu
@newwartości pól klucza głównego, jeśli uległy zmianie -
Wykonanie metody
OnAfterUpdate(@old, @new) -
Zacommitowanie transakcji procesu zapisu.
Z powyższego opisu wynika kilka wniosków:
-
Wywołanie wyjątku na dowolnym etapie (zarówno przez samą bazę danych jak i przez dowolną z wywoływanych metod C#) spowoduje przerwanie całej operacji zapisu i rollback transakcji w bazie danych. Zatem zapis jest wykonywany zgodnie z zasadą wszystko albo nic.
-
Fizyczny zapis do bazy danych od Neosa 6.0 jest dokonywany przez serwer Neos a nie przez klienta VCL. Jest to istotna różnica w działaniu serwera Neos względem wcześniejszych wersji. Do tej pory Neos wysyłał informację do klienta VCL o konieczności zapisania zmian i to klient VCL wykonywał zapis do bazy danych. Od wersji 6.0 klient VCL zajmuje się jedynie odczytem z bazy danych, natomiast wszelki zapis jest wykonywany w ramach osobnej puli połączeń przez serwer Neos.
-
Model
@oldi@newnie zawsze zawierają dokładnie te same wartości pól, które użytkownik widział na oknie zaraz przed naciśnięciem przycisku "Zapisz". Jeśli w międzyczasie inny użytkownik zmodyfikował w bazie danych dane tego rekordu, to warstwa logiki do modelu@oldpodczyta aktualną wartość rekordu z bazy danych. Jest to zachowanie zgodne z działaniem triggerów w języku SQL. -
W samej bazie danych nie należy pisać triggerów, za wyjątkiem tych, które na podstawie wartości z generatora inicjują kolejną wartość pola klucza głównego. Odpowiedzialność nadawania kolejnej wartości klucza powinna pozostać po stronie bazy danych.