diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index fcb2bd7..fab808b 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -27,6 +27,7 @@ SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzo | Serwisy biznesowe (App / Database / Login / Session scope) | [references/services.md](references/services.md) | | Tłumaczenia (Translate, TranslateIgnore), ILogger, ActSource | [references/translations-logging.md](references/translations-logging.md) | | Action result zwracany przez worker / extender / Command - raporty, dialogi, nawigacja | [references/action-result.md](references/action-result.md) | +| RowCondition - serwerowe warunki LINQ, filtrowanie SubTable / View / Query | [references/rowcondition.md](references/rowcondition.md) | | Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | ## Architektura warstw diff --git a/soneta-programming/references/rowcondition.md b/soneta-programming/references/rowcondition.md new file mode 100644 index 0000000..664c7df --- /dev/null +++ b/soneta-programming/references/rowcondition.md @@ -0,0 +1,305 @@ +# RowCondition - serwerowe filtrowanie danych + +`RowCondition` to mechanizm tworzenia warunków filtrujących wczytywane wiersze **na poziomie serwera SQL**. Warunki są tłumaczone na klauzulę `WHERE` zapytania SQL, dzięki czemu z bazy do pamięci aplikacji trafiają wyłącznie wiersze spełniające kryteria. Jest to **podstawowy sposób efektywnego odczytu danych** w logice biznesowej. + +Najwygodniejsze API to budowa warunku z wyrażeń LINQ (`Expression>`) przez `RowCondition.FromExpression(...)` oraz aplikowanie wyrażeń bezpośrednio do `SubTable` i `View` przez indeksator i `AddExpression(...)`. + +## Najważniejsze zasady + +* W wyrażeniu LINQ można odwoływać się **wyłącznie do pól bazodanowych** (kolumn tabeli, pól złożonych, kolekcji powiązanych, cech). Próba użycia pola niebazodanowego rzuca `LinqConditionException`. +* Po lewej i prawej stronie operatora porównania może wystąpić wyrażenie liczone po stronie klienta lub pole bazodanowe - kolejność jest dowolna (`row.Prop == wyr` lub `wyr == row.Prop`). +* Po stronie klienta można używać dowolnych pól, właściwości, wywołań metod, wyrażeń arytmetycznych - są one zewaluowane przed wysłaniem do SQL i wstawione jako stała. +* Wszystkie porównania tekstowe są **case-insensitive**. +* Wyrażenie LINQ jest tłumaczone na SQL, ale **kompilator C# wymusza poprawność typów** - błędy wykrywane są na etapie budowy projektu, a nie wykonania. + +## Wzorce użycia w kodzie + +### 1. Indeksator `SubTable[expression]` - logika biznesowa + +Najwygodniejszy sposób odczytu odfiltrowanych danych w kodzie biznesowym. Zwraca nowy `SubTable` zawierający tylko pasujące wiersze: + +```csharp +var st = Session.GetHandel().DokHandlowe.WgMagazyn[ + dok => dok.Kontrahent.Kod == "ABC" +]; + +foreach (DokumentHandlowy dok in st) { + // ... operacje na odfiltrowanych dokumentach +} +``` + +Generowane SQL łączy tabele referencyjne automatycznie (`LEFT OUTER JOIN`) i osadza warunek w `WHERE`: + +```sql +select * from DokHandlowe t0 +left outer join Kontrahenci t1 on t0.Kontrahent = t1.ID +where t1.[Kod] = @? +order by t0.[Magazyn], t0.[Data], t0.[Czas], t0.[ID] +``` + +Sortowanie pochodzi z klucza wybranego indeksatorem (tu: `WgMagazyn`). + +### 2. `View.AddExpression(...)` - listy w UI + +`View` to obiekt warstwy UI używany do prezentacji list. Można na niego nakładać kolejne warunki, które są łączone operatorem `AND`: + +```csharp +View view = Session.GetTowary().Towary.PrimaryKey.CreateView(); + +// Generyczna wersja - jawny typ wiersza +view.AddExpression(t => LinqConditionMethods.Like(t.Kod, "B*")); + +// Wersja z jawnym castem w wyrażeniu - przydatna dla cech (indeksatora `row["..."]`) +view.AddExpression((Towar t) => t["Asortyment"] == null); +``` + +`AddExpression` można wywoływać wielokrotnie - każdy warunek dokłada koniunkcję do zapytania. + +### 3. `Query.Table.AddExpression(...)` - zapytania niskopoziomowe + +Stosowane w niskopoziomowym budowaniu zapytań (`Query`). API identyczne jak dla `View`. + +### 4. `RowCondition.FromExpression(...)` - jawne budowanie warunku + +Gdy warunek ma być przekazywany jako wartość (parametr, zwracany z metody), buduje się go jawnie: + +```csharp +var cond = RowCondition.FromExpression( + t => t.Dostawca.Kod == "ABC" && t.Typ == TypTowaru.Towar +); +``` + +Powstały obiekt można potem podać do `SubTable`, `View`, `Query`. Tłumaczenie na string SQL-like uzyskuje się przez `cond.ToString()` - przydatne w testach jednostkowych. + +## Zakres możliwych wyrażeń + +Poniższe sekcje opisują, co można umieścić wewnątrz `Expression>`. Każda pozycja ma przykład C# oraz - tam gdzie to istotne - postać po `ToString()` (zgodną z konwencją trzymaną w testach). + +### Odwołania do pól + +```csharp +// Pole proste +t => t.Kod == "ABC" + +// Pole referencyjne (JOIN po referencji) +t => t.Dostawca.OddzialFirmy.Nazwa == "Nazwa" +// -> [Dostawca.OddzialFirmy.Nazwa]='Nazwa' + +// Łańcuch referencji z polem złożonym +t => t.Dostawca.Kontakt.EMAIL == "a@a.com" +// -> [Dostawca.Kontakt.EMAIL]='a@a.com' + +// Pole prywatne / protected - dostęp przez indeksator z castem +op => (string)op["Password", RowVersion.Original] == "ABC" +// -> Password='ABC' + +// Cecha (feature) - indeksator po nazwie cechy, prawa strona jako (object) +op => op["NAME"] == (object)"VALUE" +// -> [Features.NAME]='VALUE' + +// Cecha na obiekcie referencyjnym +t => t.Dostawca["NAME"] != (object)"VALUE" +// -> [Dostawca.Features.NAME]<>'VALUE' + +// Porównanie dwóch kolumn tego samego wiersza +op => op.Name == op.FullName +// -> [Name]=[FullName] +``` + +### Wartości po stronie klienta + +Można odwoływać się do pól/zmiennych/metod zdefiniowanych po stronie kodu - są one liczone raz, przed wysłaniem do SQL, i osadzane jako stała: + +```csharp +private readonly string FieldString = "ABC"; +private string PropertyString => "ABC"; + +op => op.Name == FieldString // Name='ABC' +op => op.Name == PropertyString // Name='ABC' +op => op.Name == PropertyString.Substring(1, 2) // Name='BC' +``` + +### Typy proste, enum, int + +```csharp +op => op.ID == 123 // ID='123' +op => op.ID != 123 +op => op.ID > 123 +op => op.ID >= 123 +op => op.ID < 123 +op => op.ID <= 123 + +// Kolejność może być odwrócona +op => 123 > op.ID // ID<'123' + +// Enum +t => t.Typ == TypTowaru.Receptura // Typ=Receptura +``` + +### Bool + +```csharp +op => op.Locked // Locked +op => !op.Locked // NOT Locked +``` + +### String + +Wszystkie porównania są case-insensitive. + +```csharp +op => op.Name == "ABC" // Name='ABC' +op => "ABC" != op.Name // Name<>'ABC' + +// Większość/mniejszość przez CompareTo - na obiekcie lub statycznie: +op => op.Name.CompareTo("ABC") > 0 // Name>'ABC' +op => op.Name.CompareTo("ABC") >= 0 +op => op.Name.CompareTo("ABC") < 0 +op => op.Name.CompareTo("ABC") <= 0 +op => 0 > op.Name.CompareTo("ABC") // Name<'ABC' (kolejność odwrócona) +op => string.Compare(op.Name, "ABC") > 0 // Name>'ABC' +op => 0 < string.Compare(op.Name, "ABC") // Name>'ABC' +op => 0 < string.Compare("ABC", op.Name) // Name<'ABC' + +// Dopasowanie podciągu / prefiksu / sufiksu - generuje SQL LIKE z escapowaniem znaków '*' i '%' +op => op.Name.Contains("A*B%C") // Name like '%A[*]B[%]C%' +op => op.Name.StartsWith("A*B%C") // Name like 'A[*]B[%]C%' +op => op.Name.EndsWith("ABC") // Name like '%ABC' + +// Pełna maska SQL LIKE (znaki '%' i '_' traktowane jako wildcardy) +op => LinqConditionMethods.Like(op.Name, "AB%C") // Name like 'AB%C' +``` + +### Null / not null + +```csharp +op => op.Name == null // Name Is Null +op => op.Name != null // Name Is Not Null +``` + +### Referencje + +```csharp +t => t.Dostawca == null +t => t.Dostawca != null + +// Weryfikacja typu referencji (sprawdzenie tabeli docelowej polimorficznej referencji) +t => t.Jednostka is Jednostka // Jednostka typeof Jednostki + +// Warunek na obiekcie wskazywanym przez referencję - alternatywa do "t.Ref.Pole == ..." +t => LinqConditionMethods.Join(t.Dostawca, k => k.Kod == "ABC") +// -> join Dostawca where (Kod='ABC') +``` + +### Operator IN - przynależność do zbioru + +`LinqConditionMethods.In` zastępuje rozpisany ciąg `pole == v1 || pole == v2 || ...`. Argumenty można podać jako `params` lub jako tablicę. Działa dla różnych typów - intów, dat, procentów, stringów, referencji: + +```csharp +// Stringi (params) +t => LinqConditionMethods.In(t.Kod, new[] { "BIKINI", "XYZ" }) + +// Daty +d => LinqConditionMethods.In(d.Data, Date.Today.PrevDay, Date.Today, Date.Today.NextDay) + +// Inty - params +d => LinqConditionMethods.In(d.ID, 1, 2) + +// Inty - tablica +d => LinqConditionMethods.In(d.ID, new[] { 1, 2 }) + +// Procenty +d => LinqConditionMethods.In(d.Rabat, Percent.Parse("10%"), Percent.Parse("20%")) + +// Referencje +t => LinqConditionMethods.In(t.Dostawca, kontra1, kontra2) +t => LinqConditionMethods.In(t.Dostawca, kontraArr) +``` + +### Operatory logiczne i wyrażenia złożone + +```csharp +// AND, OR, NOT +op => op.Locked && op.ID > 234 // Locked and ID>'234' +op => !op.Locked || op.Name == "ABC" // NOT Locked or Name='ABC' + +// Operator warunkowy ?: - rozwijany do dwóch wykluczających się gałęzi +op => op.Locked ? op.Name == "ABC" : op.FullName == "DEF" +// -> Locked and Name='ABC' or NOT Locked and FullName='DEF' + +// Nawiasy - normalne nawiasy C# sterują grupowaniem +t => (t.Typ == TypTowaru.Towar || t.Typ == TypTowaru.Usluga) && t.Dostawca != null +``` + +### Pola złożone (Quantity, Currency, FromTo) + +Można porównywać całe pole złożone albo pojedyncze elementy (`.Value`, `.Symbol`, `.From`, `.To`, ...): + +```csharp +// Porównanie całego Quantity +t => t.MasaNetto == new Quantity(123, "kg") // MasaNetto='123 kg' + +// Składowe Quantity +t => t.MasaNetto.Value == 123 // [MasaNetto.Value]='123' +t => t.MasaNetto.Symbol == "szt" // [MasaNetto.Symbol]='szt' + +// FromTo - cały zakres +e => e.Okres == new YearMonth(2020, 2).ToFromTo() // Okres='...' + +// FromTo - składowa +e => e.Okres.From == new Date(2020, 2, 2) // [Okres.From]='...' + +// FromTo - przynależność daty do zakresu +e => e.Okres.Contains(new Date(2020, 2, 2)) +// -> [Okres.From]<='...' and [Okres.To]>='...' + +// FromTo - przynależność zakresu do zakresu +e => e.Okres.Contains(new YearMonth(2020, 2).ToFromTo()) + +// FromTo - przecięcie zakresów (część wspólna niepusta) +e => e.Okres.IsIntersected(new YearMonth(2020, 2).ToFromTo()) +``` + +### Kolekcje powiązane (podlisty) + +Dla pól reprezentujących powiązaną podlistę: + +```csharp +// Podlista pusta / niepusta +t => t.AdresyWWW.IsEmpty // NOT exists AdresWWW.Zapis +t => t.AdresyWWW.Any // exists AdresWWW.Zapis +t => t.AdresyWWW.Any() // exists AdresWWW.Zapis + +// Egzystencjalny - istnieje przynajmniej jeden element spełniający warunek +t => t.AdresyWWW.Any(adr => adr.Domyslny) +// -> exists AdresWWW.Zapis where (Domyslny) + +// Egzystencjalny przez referencję +t => t.Dostawca.AdresyWWW.Any(adr => adr.Domyslny) +// -> exists AdresWWW.Zapis=Dostawca where (Domyslny) + +// Uniwersalny - wszystkie elementy spełniają warunek (równoważne: NOT exists element spełniający NEG) +t => t.AdresyWWW.All(adr => adr.Domyslny) +// -> NOT exists AdresWWW.Zapis where (NOT Domyslny) + +t => t.Dostawca.AdresyWWW.All(adr => adr.Domyslny) +// -> NOT exists AdresWWW.Zapis=Dostawca where (NOT Domyslny) +``` + +## Ograniczenia - co się nie skompiluje do SQL + +* **Pola niebazodanowe** - próba użycia właściwości obliczanej w C# (np. `op.NewPassword`) rzuca `LinqConditionException`. +* **Referencje przez pole nie-bazodanowe** - np. `t.Dostawca.Adres.Faks` gdy `Adres` nie jest fizyczną referencją w bazie - również `LinqConditionException`. +* W praktyce: jeśli pole jest deklarowane jako property w `business.xml`, jest bazodanowe; jeśli jest dopisywane jako property w klasie partial - nie jest. + +## Kiedy używać czego + +| Cel | API | +|---|---| +| Odczyt odfiltrowanego zbioru danych w logice biznesowej | `SubTable[t => ...]` | +| Filtrowanie listy w UI | `View.AddExpression(...)` | +| Przekazywanie warunku jako wartości / kompozycja warunków | `RowCondition.FromExpression(...)` | +| Niskopoziomowe zapytanie (Query) | `Query.Table.AddExpression(...)` | + +**Kod biznesowy nie powinien używać `View`** (to obiekt UI). Kod biznesowy filtruj przez `SubTable[expression]` lub +`RowCondition.FromExpression`.