Dlaczego nie powinieneś tworzyć Extensions do Business Central?

Obecnie chcąc rozszerzyć funkcjonalność Dynamics 365 Business Central modyfikujesz standardowe obiekty w Development Environment. Niemniej od jesieni czekają Cię spore zmiany, na Directions ASIA Microsoft zapowiedział, że wraz z kolejną edycją nie będzie już Development Environment, ani klienta RTC. Pozostanie Ci korzystanie z Visual Studio Code i nowoczesnych klientów opartych o technologię WEB. Co gorsza czeka Cię także niespodzianka w postaci przekonwertowanej aplikacji do poziomu Extension. Wierząc w to co mówi Microsoft powinniśmy mieć dostęp do kodów źródłowych. Niemniej poruszanie się w tylu obiektach z poziomu VS Code może nie być przyjemne. Nie oszukujmy się, nie jest to środowisko tak przyjazne jak stary poczciwy Development Environment, który był dedykowany Dynamics NAV właśnie. Może to wymuszać tworzenie mniejszych Extensions celem dostosowania systemu, zamiast modyfikacji przepastnych kodów źródłowych systemu. Tutaj czają się dwie pułapki o których mało kto wspomina…

Pułapka nr 1 – modyfikacja zachowania systemu

Tworząc Extension – możemy rozszerzać działanie systemu, ale nie możemy modyfikować jego standardowych mechanizmów. Jeżeli MS napisze coś co Ci się nie podoba używając Extension nie zmodyfikujesz ich kodu oraz nie wyłączysz go. Przykładem niech będzie komunikat dodany w którejś z wersji – „Zamówienie nie zostało zrealizowane, czy na pewno chcesz je zamknąć?” (teraz komunika można już wyłączyć w ustawieniach). Błahy komunikat spowodował, że tysiące (jak nie setki tysięcy) użytkowników musiały bezsensownie dzień w dzień klikać potwierdzenie oczywistej dla nich akcji. Sam w wielu firmach wyłączałem ten komunikat – ale możliwość taką dawała tylko ingerencja w kod źródłowy systemu.

Pułapka nr 2 – wydajność

Tutaj zaczyna się prawdziwy problem z Extensions, który oddziałuje na dwóch poziomach. Pierwszym jest obsługa zdarzeń zamiast ciągłość kodu. Jak się pewnie domyślasz trudno aby oba te podejścia były tak samo wydajne jeżeli chodzi o obsługę transakcji bazodanowych. Drugi problem postaram Ci się uzmysłowić na przykładzie. Załóżmy, że zaczynasz wdrożenie i w celu raportowania musisz dodać pole do zapisów księgi głównej. Myślę, że przykład ten nie jest niczym nadzwyczajnym, ale równie dobrze tyczy się dodania każdego pola do dowolnej tabeli, które będzie później wykorzystywane do filtrowania bądź wyliczania danych. Jako programista masz już dziś możliwość aby zrobić to dwojako – modyfikując standardowo tabelę G/L Entry, bądź tworzą Extension rozszerzające tą tabelę o nowe pole. Prześledźmy teraz oba scenariusze, dla wygody zrobię to jednak nie na G/L Entry a na nowej tabeli zapisów, którą wcześniej utworzyłem (jako pierwsze Extension).

Krok 1 – dodajemy nowe pole modyfikując obiekt

Do testów utworzyłem tabelę “Test Entry Table” tabela ta jest bardzo prosta, a dodane pole symbolizuje „Test Field 1”. Później dodamy analogiczne pole z tym, że za pomocą Extension.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
table 50100 "Test Entry Table"
{
    DataClassification = OrganizationIdentifiableInformation;

    fields
    {
        field(1; "Entry No."; Integer)
        {
            Caption = 'Entry No.';
            DataClassification = OrganizationIdentifiableInformation;
        }
        field(2; "Posting Date"; Date)
        {
            Caption = 'Posting Date';
            DataClassification = OrganizationIdentifiableInformation;
        }
        field(3; "Customer No."; Code[20])
        {
            Caption = 'Customer No.';
            DataClassification = OrganizationIdentifiableInformation;
            TableRelation = Customer;
        }
        field(4; "Amount"; Decimal)
        {
            Caption = 'Amount';
            DataClassification = OrganizationIdentifiableInformation;
        }
        field(50000; "Test Field 1"; Code[10])
        {
            Caption = 'Test Field 1';
            DataClassification = OrganizationIdentifiableInformation;
        }
    }

    keys
    {
        key(PK; "Entry No.")
        {
            Clustered = true;
        }
        key(PostingDate; "Posting Date")
        {
            SumIndexFields = Amount;
        }
        key(PostingDateTestField; "Posting Date", "Test Field 1")
        {
            SumIndexFields = Amount;
        }
        key(TestFieldKey; "Test Field 1")
        {
            SumIndexFields = Amount;
        }
    }
}

Poza polami zdefiniowałem klucze wraz z SumIndexFields. Ważnym jest, że dodając pole do tabeli w ten sposób mam możliwość zdefiniowania kluczy składających się z istniejących i nowo dodanych pól. Brak jest tej możliwość w przypadku Extension, ale o tym za chwilę.

Nasza nowa tabela prezentuje się następująco:

Krok 2 – dodajemy nowe pole tworząc Extension

Teraz dodamy analogiczne do poprzedniego kroku pole „Test Field 2”, ale zrobimy to tworząc Extension do tabeli z poprzedniego kroku.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tableextension 50100 "Test Entry Table Ext" extends "Test Entry Table"
{
    fields
    {
        field(50001; "Test Field 2"; Code[10])
        {
            Caption = 'Test Field 2';
            DataClassification = OrganizationIdentifiableInformation;
        }
    }

    keys
    {
        key(NewKey; "Test Field 2")
        {

        }
    }
}

Po stronie Business Central widzimy to jako jedną zgrabną tabelę, ale rzeczywistość jest trochę inna.

Myślę, że zdajesz sobie sprawę z tego, że każde rozszerzenie przechowuje swoje dane w dedykowanych tabelach. Mimo wielu zalet rodzi to pewną nieprzyjemną konsekwencję. Teraz na naszą tabelę składają się dwie tabele (jedna z pierwszego – bazowego Extension i druga z rozszerzającego je nowego Extension). Niesie to za sobą poważne konsekwencja – brak możliwości definiowania kluczy i widoków indeksowanych (SIFT) zawierających wszystkie pola w tabeli. Ma to potencjalnie ogromny wpływ na rozwiązania, które będziesz tworzyć.

Oto jak wyglądają tabele z poziomu SQL Management Studio:

Krok 3 – Test wydajności

Chciałbym teraz pokazać Ci jak wielki problem powoduje dodanie nowego pola do tabeli za pomocą Extension. Porównajmy dwa fragmenty kodu:

1
2
3
4
5
TestEntry.reset;
TestEntry.SetCurrentKey("Posting Date", "Test Field 1");
TestEntry.SetRange("Posting Date", PostingDate);
TestEntry.SetRange("Test Field 1", '001');
TestEntry.CalcSums(Amount);

oraz

1
2
3
4
5
TestEntry.reset;
TestEntry.SetCurrentKey("Posting Date", "Test Field 2");
TestEntry.SetRange("Posting Date", PostingDate);
TestEntry.SetRange("Test Field 2", '901');
TestEntry.CalcSums(Amount);

Te dwa fragmenty kodu są identyczne, i mają zrobić dokładnie to samo – wyliczyć sumę pola Amount, w obu przypadkach korzystamy z filtorwania po prawie takich samych polach. Wielu programistów będzie spodziewało się, że wydajność obu tych fragmentów będzie identyczna, to jednak jest dalekie od prawdy.

O dziwo w drugim przypadku kompilator przyjął klucz „Posting Date”, „Test Field 2” – który nie istnieje, ale gdyby dało się go stworzyć to byłby on idealny do tej operacji. Tym razem jednak został on zignorowany przez kompilator i system.

Po odpaleniu tych fragmentów kodu w Business Central i skorzystaniu z SQL Profilera sprawdziłem na jakie zapytania języka SQL konwertowane są te instrukcje w obu przypadkach, oto rezultaty:

Zapytanie 1:

1
2
3
SELECT SUM("SUM$Amount")
FROM "CRONUS".dbo."CRONUS International Ltd_$Test Entry Table$3ec26948-c0c0-4a0e-bf1b-76973cc297dd$VSIFT$PostingDateTestField" WITH(READUNCOMMITTED,NOEXPAND)  
WHERE ("Posting Date"='20200607' AND "Test Field 1"='001') OPTION(OPTIMIZE FOR UNKNOWN)

Zapytanie 2:

1
2
3
4
5
SELECT SUM("50100"."Amount")
FROM "CRONUS".dbo."CRONUS International Ltd_$Test Entry Table$3ec26948-c0c0-4a0e-bf1b-76973cc297dd" "50100"  WITH(READUNCOMMITTED)  
JOIN "CRONUS".dbo."CRONUS International Ltd_$Test Entry Table$edde125d-9df7-4214-948c-2c8a1072a618" "50100_e1"  WITH(READUNCOMMITTED)  
ON ("50100"."Entry No_" = "50100_e1"."Entry No_")
WHERE ("50100"."Posting Date"='20200607' AND "50100_e1"."Test Field 2"='901') OPTION(OPTIMIZE FOR UNKNOWN)

Jak widać pierwsze zapytanie korzysta z technologii SIFT do szybkiego wyliczenia sumy, wynika to z faktu, iż była możliwość zdefiniowania odpowiedniego klucza z wartością SumIndexFields. Drugie zapytanie to coś z goła innego. Jest to JOIN dwóch tabel, wystarczy spojrzeć na porównanie wykonania obu zapytań:

Zapytanie 1:

Table ‚CRONUS International Ltd_$Test Entry Table$3ec26948-c0c0-4a0e-bf1b-76973cc297dd$VSIFT$PostingDateTestField’. Scan count 0, logical reads 2, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Zapytanie 2:

Table ‚CRONUS International Ltd_$Test Entry Table$3ec26948-c0c0-4a0e-bf1b-76973cc297dd’. Scan count 1, logical reads 2746, physical reads 3, read-ahead reads 2729, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Table ‚CRONUS International Ltd_$Test Entry Table$edde125d-9df7-4214-948c-2c8a1072a618’. Scan count 1, logical reads 101, physical reads 2, read-ahead reads 248, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Zapytnie 1 vs. Zapytanie 2

Dodam, że przed każdym wywołaniem danego zapytania czyszczone były dane w cache SQLa.

Patrząc na powyższe obrazki nawet niewtajemniczone osoby powinny zauważyć, że pierwsze zapytanie było optymalne i bardzo szybkie, a drugie było jego przeciwieństwem. Różnica czasów trwania to 15 do 147, czyli nieomalże dziesięciokrotność. Wynika to z braku możliwości skorzystania z odpowiednich kluczy i widoków indeksowanych.

Podsumowanie

Mimo, że jestem entuzjastą obecnych zmian, uwielbiam Visual Studio Code, Extension, Gita i całą resztę udogodnień, to ciągle z obawami patrzę w przyszłość. Powodów jest wiele, ale nie w tym rzecz, bo wpis ten miał tak naprawdę na celu uświadomienie Ci faktu, że nie zawsze tworzenie Extension będzie najlepszym wyjściem. Business Central doskonale sprawdza się także w dużych wysokowydajnych wdrożeniach, ale dzieje się tak ze względu na możliwość modyfikacji standardowego kodu systemu. Dynamics NAV był najlepszą platformą biznesową – nie ze względu na swoje funkcje – ale ze względu na możliwość ich modyfikacji. Potencjał był naprawdę spory, zwłaszcza jeżeli chodzi o poprawę wydajności i modelowanie procesów biznesowych. Dopóki mamy dostęp do kodów źródłowych i Business Central w wersji on-Premis możemy spać spokojnie, ale boję się, że nadejdzie dzień w którym Microsoft zabierze nam tę możliwość, a partnerzy zostaną sprowadzenie do roli handlowców oferujących Business Central w chmurze. Nie wiem na ile Microsoft zdaje sobie sprawę z tego co było najmocniejszą stroną Dynamics NAV, ale na pewno nie chodziło tutaj o możliwość tworzenia nowych funkcji. Także zanim zdecydujesz się na wdrożenie danej funkcji poprzez tworzenie Extension upewnij się, że nie jest ona newralgiczna z punktu widzenia wydajności całego systemu i procesów biznesowych klienta.

3 Komentarze

  1. Świetna analiza wykonana z Twojej strony. Temat jest imo w sporym stopniu powiązany z wątkiem https://experience.dynamics.com/ideas/idea/?ideaid=5a2cfc87-1f52-e911-b047-0003ff68d113. Pytanie, czy próbowałeś zgłaszać Twój, bardziej ogólny, temat na portalu https://experience.dynamics.com? Wydaje się on być faktycznie sporawym zagrożeniem.

    1. rafal

      Nie zgłaszałem, ale jak słusznie zauważyłeś takie zgłoszenie jest. Na chwilę obecną też trudno mi sobie wyobrazić aby przy obecnej architekturze Extension dało się to w pełni wydajnie rozwiązać. Można stworzyć osobne tabele indeksujące, ale nigdy nie będzie to tak elastyczne i wydajne rozwiązanie jak indeksowanie na jednej tabeli. Ogólnie na chwilę obecną MS skupił się na innych kwestiach zostawiając wydajność trochę z tyłu. Poszli raczej w wygodę użytkowania aniżeli wydajność. Ma to też swoje plusy, ale jest pewnym ciosem dla wszystkich, którym zależało na wydajności, dotyczy to zwłaszcza powolnego interfejsu.

      1. Adrian Bystrek

        Pozostaje nam zatem na ten moment chyba tylko śledzić temat i mieć nadzieję, że Microsoft nie zignoruje problemu

Zostaw odpowiedź

Twój adres e-mail nie zostanie opublikowany Wymagane pola są zaznaczone *