NET 5 WebAPI - filtry, sortowania, stronnicowanie, kolumny po stronie serwera

0

Witam.
Walczę z konfiguracją widoków w aplikacji. Chciałbym, aby filtry, sortowanie i tego typu informacje były trzymane w bazie. Zależy mi, aby konfiguracja widoków była taka sama na obojętnie jakiej maszynie. LocalStorage niestety w tym momencie odpada, ponieważ to działa w obrębie jednej przeglądarki co jest jeszcze gorsze.

Problem zaczyna się gdy filtry dla poszczególnych typów danych są różne.
Wideo pokazujące możliwości filtracji

Towary oprócz filtrów mają też wybór widoku (kafelki, tabela). Każdy widok ma wybór z pośród swoich kolumn. Dokumenty mimo, iż to jest jeden "typ" to WZ nie ma kolumny informującej czy dokument został rozliczony.

Na tę chwilę opcje mam dwie:

  1. Stworzyć tabele dla każdej opcji, każdego typu - ProductsFilters, CustomersFilters, ProductsColumns, CustomersColumns itp
  2. Stworzyć jedną tabelę, a w kolumnach trzymać JSONa z konkretnym obiektem
class UserFilter
{
    public int Id {get;set;}
    public int UserId {get;set;}
    public string Name {get;set;}
    public string Type {get;set;}
    public string JsonValue {get;set;}
}

Oczywiście filtry byłyby pobierane na podstawie UserId. Jeśli nie ma takiego to wrzuci domyślny. W innych przypadkach z każdym zapytaniem filtr byłby aktualizowany w bazie. Tyczy się wszystko również paginacji, sortowania, wybranych kolumn oraz wybranego widoku. Wydajność mnie nie martwi, bo filtrując i tak odpytuje o dane. Wyjątki też jestem w stanie ogarnąć żeby nie przeszkadzało to w pracy. Lepiej jakby jakiegoś ustawienia nie zapisał niż jakby nie mógł pracować.

Macie jakieś swoje, lepsze rozwiązania?

1

Może zrób osobny endpoint na zapis filtrów. I z frontu wywołuj go tylko jak się coś zmieni. Ja bym poszedł w jedną tabele.

0

Czyli jak osobny endpoint to najpierw update filtrów, a później odpytując o dane już tych filtrów nie przekazuje? A po stronie odpytywania o dane, pobieram zapisany filtr z bazy i wyświetlam odpowiednio? To teraz jeszcze kwestia aktualizacji frontu w momencie refreshu albo innych zabiegów. Czy wraz z danymi powinienem też zwracać obiekt filtrów, aby UI pokazywało to co aktualnie jest zafiltrowane czy też osobny endpoint?

1

W aplikacjach zazwyczaj korzystam z czegoś co trzyma mi stan aplikacji( wybory uzytkownika w filtrach, sortowanie, jak miałem panoramy to zoom i kierunek). Stan zapisuje na backendzie podczas określonych akcji użytkownika. A potem jak loguje się w innej przeglądarce to odtwarzam sam zapis stanu a front "automagicznie" na podstawie tych informacji odtwarza wszystko. Wymaga to pewnego określonego stlyu tworzenia frontu. W którym edytujesz stan a komponent odwzorowuje stan.
Moją ulubioną libką to zarządzania stanem aplikacji na froncie jest: https://datorama.github.io/akita/.

0

Łoo masakro. Jakoś nie potrafię zrozumieć co podmiot liryczny ma na myśli w tej bibliotece https://datorama.github.io/akita/. Chyba za głupi jestem na to ale się nie poddaje. Na tę chwilę w tej kwestii mam spory bałagan, bo jeszcze się tego uczę i dobrze by było znaleźć coś co to uporządkuje i nauczy na przyszłość.

Miałem dzisiaj masę pomniejszych głupot do zrobienia i skończyło się na klasie

public class UserFilter
{
     public int Id {get;set;}
     public string Type {get;set;}
     public int UserId {get;set;}
     public string JsonValue {get;set;}
}

Próbuje to jakoś znormalizować żeby nie klepać pierdyliarda klas. Propertka Type przyjmuje ścieżkę do konkretnego typu danych. Na Githubie to widziałem. Choć pewnie na GH wykorzystują to do czegoś innego, to mnie w jakiś sposób nakierowało na problem różnych danych, a jak tych samych danych (dokumenty), to różnych typów.

PRZYKŁADY

{
  "Id": 1,
  "UserId": 1,
  "Type": "filter:documents:308",
  "JsonValue": "{}"
}

{
  "Id": 1,
  "UserId": 1,
  "Type": "column:documents:308",
  "JsonValue": "{}"
}

{
  "Id": 1,
  "UserId": 1,
  "Type": "filter:products",
  "JsonValue": "{}"
}

Zapis typu filter:documents:308 znaczy, że chodzi o filtry dokumentów typu 308 (w Comarch Optima to są Rezerwację odbiorcy). Analogicznie z kolumnami column:documents:308. Tutaj też musi być typ dokumentu, ponieważ, według uznania, użytkownik nie musi chcieć tych samych kolumn na każdym typie dokumentów. Rozwiązanie wymaga trochę "ifologii" ale lepiej jest sprawdzić stringa, zrobić Split() niż klepać dla każdego typu osobną klasę. Na pewno dla mnie lepiej. Co sądzicie? Będą ze mnie ludzie? 😎

0

IMO niepotrzebnie rozbijasz to aż tak bardzo relacyjnie. I za każdym razem musisz agregować ustawienia. Podejście dokumentowe w trzymaniu statu wydaje się rozsądniejsze.
Jak masz bazę sql, wrzuciłbym coś w stylu UserId którybyłby kluczem i trzymał state w jsonie cały. Poza tym wróce do tej akity jeżeli front będzie używał bazował na state z akity. tu to jest abrdziej opisane https://datorama.github.io/akita/docs/ui
To potem wystarczy że zrobisz:

update({ ui: { jsonProstoZBazy } } )

I w idealnym świecie front powinen się ustawić "automagicznie" jak dobrze zakodzisz :)

1

Podejście nr 2 i definicja w JSON. Zawczasu pomyśl o fallback, czyli sytuacji kiedy użytkownik nie posiada zdefiniowanego własnego widoku dla danego typu dokumentu.
A w przyszłości można rozbudować o więcej niż jeden widok dla danego typu per użytkownik :)

0

Mam zdefiniowany domyślny widok, który wpisuje do bazy w momencie kiedy żadnego nie ma. Walczę jeszcze z jakimś state managementem ale mam wrażenie, że to jest kompletna strata czasu w sposób w jaki proponują.

1

A nie myślałeś o takich "query" jak np. w Azure DevOps? Tworzysz sobie "query". Query ma swoją nazwę, filtr i sortowanie. Jest też połączone z konkretnym widokiem i konkretnym użytkownikiem. Query jest w bazie danych, np. w takiej postaci:

query(Id, Name, Filter, Sort)

Rekord może wyglądać tak:

Id: 1
Name: "Pracownicy z mojego działu"
Filter: "WHERE deptId = 5" albo całość "SELECT * FROM employees WHERE deptId = 5"
Sort: "Name|ASC"

Myślę, że widać o o co chodzi.
Taki filtr w postaci tekstowej można przełożyć w prosty sposób na LINQ za pomocą Roslyn.

0

Też ciekawe rozwiązanie ale to nie jest przypadkiem tak, że różnie bywa z wydajnością jak się coś robi do LINQ? Miałem taki problem w jednym projekcie, może ja coś źle robiłem. Bo żeby coś pofiltrować w LINQ to musisz mieć całe dane w obiekcie (IEnumerable, List) i dopiero wtedy na tym zastosować LINQ. Na tę chwilę, w tym projekcie, mam filtry, strony i sortowanie zrobione za pomocą lambda. Ale jak chciałem to samo zaimplementować w projekcie gdzie tych danych jest nieco więcej to zaczęło się bardzo długo ładować.

W tym projekcie też mam zamiar przejść na

OFFSET @Page ROWS 
FETCH NEXT @Limit ROWS ONLY;

W innym (testowym) projekcie korzystam z SqlKata i na tę chwilę sobie chwalę.

0

Przecież jak korzystasz z EF to póki nie zmaterializujesz kolekcji poprzez użycie np. ToList() czy AsIEnumerable() to wszystkie filtry zrobione przez LINQ są tłumaczone na SQL i wykonywane po stronie bazy. Przynajmniej wszystkie, które da się przetłumaczyć na SQLa ;)

0

Ja nigdzie nie napisałem, że korzystam z EF 😏 i od razu odpowiadam - nie korzystam, ponieważ baza nie jest moja. Robię panel www do Comarch Optima i w tym przypadku najlepszy był Dapper (z nim mam największe doświadczenie). Od wersji "Core" EF nie wspiera database first. Oprócz tego jest jeszcze brak możliwości odpytania "ogólnie" bazy. W EF Core musi być encja - dbContext.FromRawSql(), nie ma czegoś takiego. Też już gdzieś o tym pisałem.

0

Przede wszystkim masz IQueryable. IQueryable nie pobiera żadnych danych. Ale możesz już walić tam warunki. Jeśli korzystasz z Dappera, to Twój przypadek jest jeszcze prostszy, bo nie musisz używać Roslyn, żeby zmienić stringa na Linq, wystarczy, że tego stringa dorzucisz w zapytaniu w Dapperze.

1 użytkowników online, w tym zalogowanych: 0, gości: 1