Źródła danych obiektu¶
Czasami bywa tak, że mamy zdefiniowany obiekt biznesowy oparty o tabelę, utworzone okna do przeglądania i edycji danych, ale pojawia się potrzeba, aby do grida albo do okna edycyjnego dodać dodatkowe dane, pochodzące z innych tabel lub wymagające zadania zapytania SQL. Dla neosów starszych niż 6.0 można to było rozwiązać następująco:
-
dodać pole wyliczane wyrażeniem SQL. Jednak w ten sposób prosto się dodaje tylko jedno pole. Jeśli trzeba dodać więcej pól, to należy napisać wyrażenie do każdego pola z osobna, przez co zapytanie końcowe staje się niewydajne.
-
dodać pole z parametrem jedynie na oknie edycyjnym / szczegółow, a ten parametr wyliczać metodą na przeliczenie, zadając w tej metodzie dodatkowe zapytanie do bazy danych. Ten sposób jest bardzo niezalecany, gdyż metoda na przeliczenie może się wykonywać wiele razy i baza danych będzie odpytywana bardzo często. Ponadto takiej informacji nie da się umieścić jako kolumny w gridzie.
-
zamienić źródło danych obiektu na zapytanie SQL i dodać w zapytaniu SQL dodatkowe pola i złączenia do innych tabel. Jest to dobry sposób, gdyż wszystkie dane pozyskujemy z jednego zapytania do bazy danych, ale ma wadę polegającą na tym, że tracimy naturalną funkcjonalność edycji i zapisu zmodyfikowanych danych do tabeli. Aby w dalszym ciągu dało się zapisywać dane, należało przeciążyć metodę PostRecord() i samodzielnie napisać kod, służący do zapisania danych zmodyfikowanych w wyniku edycji.
Obiekty z zapytania SQL ze wskazaną tabelą natywną¶
Od wersji 6.0 technologii Neos została dodana możliwość definiowania obiektów biznesowych opartych o zapytanie SQL, ale ze wskazaną tzw. tabelą natywną. Wystarczy jako źródło danych wybrać: "Dane z zapytania (modyfikowalne)" i oprócz treści zapytania SQL wypełnić także pole: "Nazwa natywnej tabeli w BD". Dzięki temu, można odczytać z bazy danych wiele pól z różnych tabel, ale te pola, które należą do tabeli natywnej można edytować i podlegają automatycznemu zapisowi po wywołaniu PostRecord() bez potrzeby pisania kodu w C#. Zaleca się szeroko stosować tego typu obiekty biznesowe, gdyż:
-
niskim nakładem pracy można przerobić obecne obiekty oparte o tabelę na obiekty oparte o zapytanie SQL z tabelą natywną, na których wszystko działa tak samo, bez pisania dodatkowego kodu C#
-
wszelkie dane uzyskujemy jednym zapytaniem SQL, co pozytywnie wpływa na wydajność
-
dodatkowe pola (te, których nie ma w tabeli) mogą być widoczne zarówno na oknach edycyjnych jak i kolumny w gridzie
-
po napisaniu niewielkiej ilości kodu c#, te dodatkowe pola także możemy edytować i zapisać ich wartości do bazy danych
-
programista, który korzysta z tak napisanego obiektu biznesowego jako z programowego źródła danych nie musi wiedzieć, jakie pola modelu danych pochodzą z jakiej tabeli w bazie danych, gdyż po prostu korzysta z pól modelu danych tego obiektu biznesowego
Neos w oparciu o definicję zapytania i tabeli natywnej sam określa, które pola są jedynie do odczytu, a które pola pochodzą z tabeli natywnej i będą automatycznie zapisywane po edycji. Po wygenerowaniu listy pól modelu danych obiektu biznesowego, we właściwościach danego pola (po prawej stronie) pojawił się dodatkowy checkbox "Pole zapisywane w tabeli (natywnej)". Jeśli ten checkbox jest zaznaczony, to pole podlega automatycznemu zapisowi, a jeśli odznaczony, to pole jest tylko do odczytu. Znacznik ten można edytować i samodzielnie określić, które pola podlegają automatycznemu zapisowi. Może to być potrzebne w przypadku gdybyśmy chcieli dodatkowe pola, które domyślnie są tylko do odczytu, udostępnić do edycji i samodzielnie obsłużyć zapis wartości tych pól gdzieś do bazy danych. Aby poprawnie obsłużyć zapis takich pól należy odziedziczyć metody logiki biznesowej InternalCreate(), InternalUpdate() i InternalDelete() i napisać w nich kod, który zapisuje je do bazy danych.
Przykład
W ramach wdrożenia u klienta ABC chcemy dodać dodatkowe pole opisowe do zamówień. Ponieważ tabela NAGZAM ma już bardzo dużo pól i nie chcemy dodawać kolejnych, to planujemy założyć tabelę X_NAGZAM z polami REF i DESCRIPT. Pole REF będzie przechowywało REF odpowiedniego zamówienia. Zakładamy, że elementy indywidualne dla tego klienta przechowujemy w projekcie ABC.
Aby zrealizować to wymaganie wykonujemy następujące czynności:
-
Dodajemy tabelkę
X_NAGZAMzawierającą polaREFiDESCRIPT. -
Dodajemy w projekcie ABC obiekt
X_NAGZAMoparty o tabelęX_NAGZAM. Ten obiekt po niczym nie dziedziczy, służy jedynie po to aby posiadać dostęp do tej tabeli poprzez warstwę logiki biznesowe. -
W projekcie ABC dziedziczymy obiekt
TD.NAGZAMw trybie przykrycia. Otrzymujemy obiekt ABC.NAGZAM. -
W obiekcie ABC.NAGZAM zmieniamy źródło danych na zapytanie modyfikowalne, wskazując tabelę NAGZAM jako natywną. Definiujemy zapytanie SQL w tym obiekcie. Klauzula "select" przyjmuje postać:
select nagzam.*, x_nagzam.DESCRIPT
Klauzula "from" przyjmie postać:
from NAGZAM left join X_NAGZAM on X_NAGZAM.REF=NAGZAM.REF
Po wygenerowaniu pól modelu danych, będziemy mieć wszystkie pola tabeli NAGZAM (updatable) oraz pole DESCRIPT z dołączonej tabeli. Pole takie może być użyte w gridzie, na oknie edycji itp. Na razie takie pole jest dostępne jedynie w trybie tylko do odczytu.
Edycja danych w obiektach z zapytania SQL¶
Pola pochodzące z tabeli natywnej są automatycznie zapisywane w tej tabeli w wyniku edycji danych. Jednak pola dodane zapytaniem SQL nie pochodzą z tabeli natywnej i należy samodzielnie obsłużyć ich zapis. Jeśli np pole DESCRIPT chcę mieć edytowane, to jego zapis muszę obsłużyć w metodach InternalCreate(), InternalUpdate() i InternalDelete() obiektu ABC.NAGZAM:
protected void InternalCreate(TModel model)
{
base.InternalCreate(model);
CreateXNagzam(model);
}
private void CreateXNagzam(TModel model)
{
var x_nagzam_logic = new LOGIC.X_NAGZAM();
var x_nagzam_model = new MODEL.X_NAGZAM();
x_nagzam_model.REF = model.REF;
x_nagzam_model.DESCRIPT = model.DESCRIPT;
x_nagzam_logic.Create(x_nagzam_model);
}
protected void InternalUpdate(TModel model)
{
base.InternalUpdate(model);
var x_nazam_logic = new LOGIC.X_NAGZAM();
var x_nagzam_model = x_nagzam_logic.Get(model.REF.Value);
if(x_nagzam_model!=null)
{
x_nagzam_model.DESCRIPT = model.DESCRIPT;
x_nagzam_logic.Update(x_nagzam_model);
}
else
CreateXNagzam(model);
}
protected void InternalDelete(TModel model)
{
base.InternalDelete(model);
new LOGIC.X_NAGZAM().Delete(model.REF.Value);
}
Po takich modyfikacjach, pole DESCRIPT może być edytowane w oknie EDIT, tak samo jak każde inne pole. Ponieważ zapis danych odbywa się poprzez warstwę logiki biznesowej, to zapis do tabeli X_NAGZAM jest realizowany w tej samej transakcji, co zapis do tabeli NAGZAM. Jeśli zapis do jednej z tych tabel się nie uda, wyjątek spowoduje wycofanie zapisu do obu tabel, to umożliwi zachowanie spójności danych.
Jak widać w powyższym przykładzie, metoda InternalUpdate() została napisana w taki sposób, aby tolerować stan, w którym istnieją rekordy w tabeli NAGZAM, dla których jeszcze nie istnieje odpowiedni rekord w tabeli X_NAGZAM. Rekordy w X_NAGZAM będą dodawane w miarę potrzeby.
Warto zauważyć, że programista aplikacji, który będzie korzystał z obiektu ABC.NAGZAM nie musi wiedzieć w jakich tabelach są przechowywane poszczególne pola. Jeśli programista chciałby w sposób programowy zmienić jakieś pola na zamówieniu, to może napisać następujacy kod:
var nagzam_logic = new ABC.LOGIC.NAGZAM();
var zamowienie = nagzam_logic.Get(ref_zamowienia);
if(zamowienie!=null)
{
zamowienie.UWAGI = ...;
zamowienie.DESCRIPT = ...;
nagzam_logic.Update(zamowienie);
}
Jak widać, programista traktuje obiekt ABC.NAGZAM jako całość, dzięki czemu może zmieniać wartości dowolnych pól modelu danych bez świadomości szczegółów implementacyjnych tego obiektu. Faktycznie pole UWAGI zostanie zapisane w tabeli NAGZAM, a pole DESCRIPT w tabeli X_NAGZAM. Takie podejście ułatwia znacznie późniejszą refaktoryzację obiektów biznesowych i struktur danych. Możemy np zrefaktoryzować obiekt ABC.NAGZAM aby dodać kolejne pola, albo rozrzucić obecne pola po innych tabelach. Kod który wykorzystuje obiekt ABC.NAGZAM pozostanie bez zmian.
Obiekty oparte o programowe źródło danych¶
Od wersji Neosa 6.0 zostały wprowadzone obiekty biznesowe, których źródło danych w ogóle nie pochodzi z zapytań SQL, ale jest generowane w samym C#. Z takich obiektów biznesowych warto skorzystać w następujących przypadkach:
-
chcemy całkowicie przestać korzystać ze skomplikowanych zapytań lub procedur SQL, aby uniezależnić się od silnika bazodanowego
-
chcemy pobrać dane łącząc źródła z kilku baz danych i możemy je połączyć dopiero jako kolekcje danych w pamięci serwera aplikacji
-
chcemy wygenerować sztuczne dane i nie potrzebujemy ich pobierać z żadnej bazy danych (np ciągi pseudolosowe, kolejne liczby, kolejne daty, itp)
-
chcemy pobrać dane z zewnętrznego WebAPI i zwrócić je jako źródło danych, po to aby np wyświetlić je w gridzie
Przykład
W projekcie WORKFLOW posiadamy tabelę WFFOLDER zawierającą strukturę folderów do przechowywania dokumentów. Jednak w oknie chcemy wyświetlić nie tylko dane z tej tabeli, ale w szczególnych przypadkach dodać wirtualny węzeł główny drzewa (root) oraz uzupełnić w modelu danych pola, które nie istnieją w tabeli, a których chcemy użyć w oknie. W tym celu pozostawiamy obiekt WFFOLDER jako obiekt oparty o tabelę WFFOLDER i nie ingerujemy w ten obiekt, natomiast zakładamy drugi obiekt o nazwie WFFOLDERLIST. W obiekcie tym wypełniamy w szczególności:
-
nazwa obiektu - "WFFOLDERLIST"
-
źródło danych - "Programowe źródło danych"
-
w zakładce "Programowe źródło danych" jako nazwę tabeli natywnej podajemy "WFFOLDER".
Tabelę natywną podajemy wtedy, kiedy obiektu WFFOLDERLIST będziemy chcieli użyć jako relacji w innym obiekcie i w oparciu o tą relację zbudować pole słownikowane (np. DROPDOWN, COMBOBOX lub EDIT). Dzięki wskazaniu tabeli natywnej system jest w stanie poprawnie wygenerować zapytanie SQL obiektu słownikowanego, gdyż takie zapytanie musi mieć kaluzule "join" do tabeli słownika. Nie da się zrobić "join" do programowego źródła danych.
Następnie w metodach logiki pokazujemy metody dziedziczone i dziedziczymy metodę Fill(). Wypełniamy ją na przykład tak:
protected IEnumerable<TModel> Fill(Contexts context)
{
//generujemy wirtualny rekord z węzłem drzewa folderów
var x = new TModel();
if(context\["_showmainfolder"]=="1")
{
x.REF = null;
x.NAME = "[Folder główny]";
}
else
{
x.REF = 0;
x.NAME = "[Inne dokumenty]";
}
x.PARENT = null;
x.ICON = "MI_2";
yield return x;
//zwracamy wszystkie rekordy tabeli WFFOLDER uzupełniając pole ICON
foreach(var f in new LOGIC.WFFOLDER().Get())
{
x = new TModel();
x.REF = f.REF;
x.NAME = f.NAME;
x.PARENT = f.PARENT;
x.ICON = "MI_2";
yield return x;
}
}
Powyższa metoda ma dostęp do wszystkich parametrów własnego obiektu biznesowego, które otrzymuje w parametrze context, a więc może zwracać różne dane w zależności od parametrów.
Dzięki założeniu instancji klasy TModel, wypełnieniu jej i zwróceniu poprzez yield return x można w metodzie Fill generować sztuczne rekordy źródła danych.
Aby z kolei przejrzeć zawartość jakiejś tabeli w bazie danych i zwrócić jej rekordy wystarczy przeiterować np po logice biznesowej obiektu WFFOLDER, a w przypadku, gdy dane z bazy danych potrzebujemy wyciągnąć bardziej złożonym zapytaniem SQL, można użyć iteracji po wyniku działania metody CORE.QuerySQL(...). Powyższa pętla przy użyciu QuerySQL może wyglądać tak:
foreach(var f in CORE.QuerySQL(WORKFLOW.ProjectInfo.DefaultDatabaseAlias, "select REF,NAME,PARENT from WFFOLDER"))
{
x = new TModel();
x.REF = f["REF"].AsInteger;
x.NAME = f["NAME"].AsString;
x.PARENT = f["PARENT"].IsNull? null : (int?)f["PARENT"];
x.ICON = "MI_2";
yield return x;
}
Należy uważać na pola, które przyjmują wartość NULL. Jeśli chcemy NULL przekazać do obiektów modelu, to należy używać konstrukcji jak przy polu x.PARENT. W przeciwnym razie wartości NULL są zamieniane na 0 lub puste stringi.
Staramy się nie używać w ogóle funkcji OpenTable, gdyż ta funkcja działa jedynie w celu kompatybilności kodu ze starszymi wersjami Neosa. W ogóle wszędzie tam, gdzie da się użyć logiki obiektów biznesowych unikajmy pisania bezpośrednio zapytań w języku SQL. Użycie SQL-a silnie uzależnia nas od modelu danych w bazie danych, a logika biznesowa w Neosie oddziela nas od tych struktur, ułatwia ich zmianę i uniezależnia nas od silnika bazy danych Firebird.
Obiektów opartych o programowe źródła danych możemy używać zarówno do budowania okien jak i programowo. Przykład programowego użycia danych zwracanych przez obiekt WFFOLDERLIST znajduje się w metodzie GetFullPath(...) tego obiektu. Jej początek wygląda następująco:
public string GetFullPath(int wffolderref, int showmainfolder = 0)
{
var p = new WFFOLDERLISTParameters();
p._showmainfolder = showmainfolder.ToString();
var folders = new LOGIC.WFFOLDERLIST().WithParameters(p);
...
}
W pierwszej kolejności powołujemy automatycznie wygenerowany obiekt WFFOLDERLISTParameters. Obiekt taki zawiera wszystkie parametry obiektu WFFOLDERLIST. Do dowolnych parametrów możemy przypisać wartości. Następnie aby przekazać te wartości do nowo utworzonego obiektu logiki używamy metody WithParameters(...). Metoda ta tworzy kopię obiektu logiki z przekazanymi parametrami. Możemy niezależnie pracować na obiekcie logiki bez parametrów i z parametrami, a nawet na kilku obiektach z różnymi wartościami parametrów, gdyż każdorazowo jest to inna instancja obiektu logiki.
var folders_without_parameters = new LOGIC.WFFOLDERLIST();
var folders_with_parameters = folders_without_parameters.WithParameters(p);
Edycja danych w obiektach opartych o programowe źródło danych¶
Obiekty oparte o programowe źródło danych działają zupełnie inaczej od obiektów opartych o tabele lub zapytania SQL. W szczególności edycja danych w oknach takiego obiektu odbywa się w całkowitym odseparowaniu od jakiejkolwiek tabeli i fizycznej bazy danych. Przypomina to nieco edycję danych w obiekcie opartym jedynie o parametry.
Aby więc edytować dane w takim obiekcie należy programowo obsłużyć zapis danych po dodaniu, poprawieniu i usunięciu rekordów. W tym celu należy odziedziczyć metody logiki biznesowej takie jak: InternalCreate(), InternalUpdate() i InternalDelete().
Przykład
Obiekt WORKFLOW.WFFOLDERLIST zawiera okna w których możemy dodawać nowy folder, edytować go, jak również obsługuje akcję usuwania folderu. Ponieważ jest to obiekt oparty o programowe źródło danych, powyższe czynności należy oddelegować do obiektu WFFOLDER, który jest oparty o tabelę WFFOLDER. Aby to oddelegowanie wykonać poprawnie napisano poniższy kod w odziedziczonych metodach:
protected void InternalCreate(TModel model)
{
new LOGIC.WFFOLDER().CreateFolder(model.NAME,model.PARENT.GetValueOrDefault());
}
protected void InternalDelete(TModel model)
{
new LOGIC.WFFOLDER().Delete(model.REF.Value);
}
protected void InternalUpdate(TModel model)
{
var wffolder_logic = new LOGIC.WFFOLDER();
var wffolder = wffolder_logic.Get(model.REF.Value);
wffolder.NAME = model.NAME;
wffolder.PARENT = model.PARENT;
wffolder_logic.Update(wffolder);
}
W niektórych przypadkach biznesowych pojawia się potrzeba, aby zapis rekordu miał niestandardowe komunikaty, lub odwoływał się do elementów interfejsu, które nie są dostępne z poziomu modelu, a co za tym idzie metody InternUpdate(). Naturalnym podejściem w takich przypadkach jest przeciążenie metody PostRecord, która odpowiada za interfejsową część procesu zapisu danych. W przypadku potrzeby skorzystania z parametrów lub innych elementów interfejsu w metodzie zapisu mamy dwie możliwości:
Sposób pierwszy¶
Przeciążamy metodę PostRecord(), implementujemy niestandardowe zachowania podczas zapisu i wywołujemy metodę logiki, w której odbywa się bezpośredni zapis do bazy. Do takiej metody możemy przekazać dowolne parametry, które będą odwzorowywały elementy interfejsu niezbędne do zapisu danych. Po metodzie zapisującej dane musimy wywołać metodę CancleRecord(), która wyjdzie z trybu edycji.
!!! Warning Uwaga! Należy pamiętać, że metoda CancleRecord(), nie wykonuje pewnych operacji, które wykonywane są w ramach standardowej metody PostRecord(). Przy takim sposobie implementacji należy pamiętać, aby po wyjściu z edycji rekordu zaopiekować się następującymi operacjami: - Walidacja danych przed zapisem - Zamknięcie okna po zapisie, jeżeli tego chcemy (ustawienie tej opcji na właściwościach formy, w tym przypadku jest pomijane) - CancleRecord pomija również wykonanie metody AfterPost(), więc jeżeli mamy ją przeciążoną, to ona również się nie wykona
Sposób drugi¶
Przeciążamy metodę PostRecord() i InternalUpdate(). W metodzie PostRecord implementujemy niestandardowe zachowania podczas zapisu (pomijamy zapis danych, tym zajmie się base.PostRecord()) i wywołujemy base.PostRecord() - oczywiście tylko w ścieżce, gdy chcemy dokonać zapisu. Jeżeli do zapisu danych potrzebujemy np. parametrów ustawionych po stronie interfejsu, to powinniśmy dodać Pola modelu danych, do których będziemy mogli przekazać niezbędne informacje. Na końcu przeciążamy metodę InternalUpdate(), w tej metodzie wywołujemy naszą metodę logiki, która zapisuje informacje do bazy, w tym momencie dzięki dodatkowym polom modelu danych mamy informacje przekazane z części interfejsowej i możemy je pobrać z modelu.
Zwracanie pól ze słowników w obiektach opartych o programowe źródło danych¶
W przypadku źródeł danych pochodzących z tabeli lub z zapytania SQL serwer Neos sam dba o to, aby w przypadku pól na których zdefiniowano relację do słowników, tak zmodyfikować zapytanie SQL o źródło danych, aby dodatkowo zwrócić w wyniku zapytania także pola pochodzące z tabel słowników. Aby uzyskać ten sam efekt w przypadku, gdy używamy programowego źródła danych, należy pamiętać o 2 krokach:
- Standardowo, jak w przypadku innych źródeł danych musimy ustawić relację na polach modelu danych do obiektu którym słownikujemy.
- Następnie w metodzie Fill tworząc klasę modelu uzyskujemy dostęp do modelu słownika który musimy wypełnić danymi. Słownik możemy wypełnić tylko tymi wartościami z którymi chcemy pracować, pozostałe wartości mogą pozostać puste.
Słownikowanie programowych źródeł danych działa tylko do pierwszego poziomu (analogicznie jak słownikowanie w obiektach z tabel). W poniższym przykładzie rozbudowujemy model danych o model słownika GRUPA_Model. Tutaj obiekt słownika zostaje zainicjowany podczas tworzenia modelu.
protected IEnumerable<TModel> Fill(Contexts context)
{
var list = new List<TModel>();
foreach(var k in API.QuerySQL("select k.REF, k.NIP, k.NAZWA, k.GRUPA, g.OPIS from klienci k join grupykli g on (k.GRUPA = g.REF)"))
{
var item = new TModel()
{
REF = k["REF"].AsInteger,
NAZWA = k["NAZWA"],
NIP = k["NIP"],
GRUPA = k["GRUPA"].AsInteger,
GRUPA_Model = {REF = k["GRUPA"].AsInteger, OPIS = k["OPIS"]}
};
list.Add(item);
}
return list;
}
Drugą możliwością jest stworzenie nowego obiektu słownika i przypisanie go.
protected IEnumerable<TModel> Fill(Contexts context)
{
var list = new List<TModel>();
foreach(var k in API.QuerySQL("select k.REF, k.NIP, k.NAZWA, k.GRUPA, g.OPIS from klienci k join grupykli g on (k.GRUPA = g.REF)"))
{
var item = new TModel()
{
REF = k["REF"].AsInteger,
NAZWA = k["NAZWA"],
NIP = k["NIP"],
GRUPA = k["GRUPA"].AsInteger
};
item.GRUPA_Model = new MODEL.GRUPYKLI() {
REF = k["GRUPA"].AsInteger, OPIS = k["OPIS"]
};
list.Add(item);
}
return list;
}
Note
W przypadku gdy jest używane programowe źródło danych i typem danych jest relacja, to znacznik "Słownik z obcej bazy danych" jest zawsze traktowany jako zaznaczony.