6th sposób komunikacji z relacyjną bazą danych
Nareszcie! Po wielu latach poszukiwań, testowania wielu innych, trafiła do mnie ta jedna jedyna, która rządzi wszystkimi pozostałymi. Panie i Panowie, przedstawiam New Kid on The Block o nazwie FSharp.Data.SqlClient, bibliotekę, która umożliwia komunikację z relacyjną bazą danych MS SQL Server.
Zanim przejdziemy do szczegółów, pozwól, że nakreślę Ci pewien kontekst. Tworząc systemy informatyczne mamy duży wybór, jeśli chodzi o projektowanie, programowanie, testowanie oraz wdrażanie rozwiązań. Niektóre elementy pasują do siebie idealnie, niektóre trochę mniej. Dwa przykłady:
-
Jeśli wychodzisz od danych, to interesuje Cię model systemu oraz jego reprezentacja w bazie danych. Dopiero potem skupiasz się na zachowaniu, które będzie operować na zdefiniowanym modelu.
-
Jeśli wychodzisz od zachowania, to interesują Cię akcje, jakie system powinien wykonywać. Dopiero potem skupiasz się na danych oraz ich reprezentacji w bazie danych.
W kontekście operacji na relacyjnej bazie danych MS SQL Server, w pierwszym przypadku zapewne rozważysz wykorzystanie któregoś z frameworków ORM np. Entity Framework. W drugim przypadku możesz chcieć sięgnąć po coś “lżejszego” np. Dapper’a.
Nic nie stoi na przeszkodzie, aby odwrócić wybór. W pierwszym przypadku zrezygnować z ORM’a, dojdzie Ci wtedy dodatkowy kod do napisania. W drugim przypadku sięgnąć po ORM’a, ale…możesz nie mieć modelu, wtedy “na siłę” musisz go stworzyć!
W moim przypadku podejście nr 1. nigdy się nie sprawdziło, natomiast podejście nr 2. zaskoczyło momentalnie, zwłaszcza wtedy, kiedy zacząłem realizować funkcjonalności na podstawie stylu architektonicznego Message Queuing. W podejściu tym komponent zawierający Message Handler odpowiedzialny jest za realizację konkretnego kawałka funkcjonalności. Przykład przetwarzania Message’a wygląda mniej więcej tak:
Mając dobrze wydzielony komponent oraz jasno określoną jego odpowiedzialność, operacje na bazie danych sprowadzają się do prostych select’ów, update’ów oraz insert’ów. Skomplikowany model nie jest potrzebny. Co więcej, jeśli zdefiniowane operacje są specyficzne dla tej konkretnej funkcjonalności, to nie potrzebna jest żadna zewnętrzna abstrakcja jak IRepository, IDatabaseContext, IDatabaseMapper itp. Całość można zamknąć w samym komponencie. Takie podejście minimalizuje pokusę (re)użycia kodu w innym miejscu (stworzenia zależności), co mocno upraszcza wprowadzanie zmian.
Uwalniając się od konieczności tworzenia abstrakcji oraz skomplikowanego modelu, powstaje pytanie, w jaki sposób zaprogramować operacje na bazie danych? W przypadku zmiany stanu może to być funkcja, która w parametrach przyjmuje wartości do zmiany. W przypadku pobierania danych również może to być funkcja, która w parametrach przyjmuje wartości to filtrowania (użyte w warunku where języka T-SQL), a jako wynik zwraca odpytane dane. W tym momencie zaczyna robić się ciekawie. Zwrócone dane trzeba jakoś reprezentować. Jeśli do implementacji użyjemy Entity Framework’a to musimy stworzyć klasę reprezentującą pobrane dane - taki mini model. Jeśli użyjemy Dapper’a to również musimy stworzyć taką klasę lub użyć typu dynamic, ale wtedy tracimy pomoc kompilatora w wykrywaniu ewentualnych błędów.
Obecnie programując w języku C# w pierwszej kolejności wybieram Dapper’a. Za każdym razem, kiedy piszę nowe zapytanie do bazy, zatrzymuję się na decyzji o tym, czy tworzyć klasę, czy użyć typu dynamic - “Przecież to zapytanie jest takie proste, po co mi ta klasa? No tak, ale fajnie jest mieć wsparcie kompilatora, podpowiadanie składni itp.” Czy jest jakiś sposób na pogodzenie tych dwóch wymagań?
Okazuje się, że jest i nazywa się FSharp.Data.SqlClient. Zobaczmy więc, co potrafi.
FSharp.Data.SqlClient w praktyce
Tak, jak w poprzednim artykule, zaprogramujmy funkcjonalności operowania na danych dotyczących informacji o graczu tenisa:
- pobranie informacji o graczu - select z widoku bazodanowego
- dodanie nowego gracza - insert danych do tabeli
- ustawienie trenera gracza - update wybranych danych z Optimistic Offline Lock
Zacznijmy od operacji select:
Trzy pierwsze właściwości, na które warto zwrócić uwagę to:
-
Prosty kod
Definiujemy trzy elementy:
ConnectionString
- nazwa w pliku konfiguracyjnym lub pełny wpissql
- kod bazodanowy do wykonaniaSqlCommandProvider
- instancja umożliwiająca wykonanie kodu sql poprzez wywołanie funkcjiExecute
-
Brak konieczności definiowania zwracanego typu
- Typ generowany jest automatycznie na podstawie danych zwracanych w zapytaniu SQL!:
- Kod jest w pełni kompilowany, ale uwaga, nie tylko języka F#, ale też języka T-SQL! Dodatkowo biblioteka wykrywa nazwy parametrów wejściowych oraz nazwy zwracanych pól, weryfikując je na poziomie kompilacji kodu. Zobaczmy, jak to wygląda w Visual Studio:
Nie wiem jak u Ciebie, ale u mnie wywołało to efekt WOW!
Wracając do Message Handler’a, to jest właśnie to, czego potrzebowałem. Przykład wykorzystania powyższego kodu z prostą logiką wypisującą informacje o graczu na standardowe wyjście może wyglądać tak:
Value playerInfo
posiada typ, wygenerowany przez FSharp.Data.SqlClient
. Podpowiadanie składni działa bez zarzutu:
A jak może wyglądać implementacja zmiany stanu w bazie danych? Zobaczmy to na przykładzie operacji insert:
Schemat kodu jest dokładni taki sam, jak dla operacji typu select. Wszystkie wcześniej wymienione właściwości również mają zastosowanie.
Na koniec popatrzmy na operację typu update:
Ten sam wzorzec z zachowanymi, wcześniej wymienionymi właściwościami.
Całą implementację możesz zobaczyć na moim GitHub’e.
FSharp.Data.SqlClient - dodatkowe właściwości
Biblioteka posiada o wiele więcej możliwości. Koncepcyjnie bliżej jej do wcześniej wspomnianego Dapper’a. Wydajnościowo również wypada bardzo dobrze. Szczegóły możesz zobaczyć na stronie dokumentacji. Kilka dodatkowych właściwości, na które zwróciłem uwagę to:
- możliwość asynchronicznego wykonania kodu -
asyncExecute
- obsługa pliku
app.config/web.config
nawet w .NET Core - możliwość “wyniesienia” kodu SQL do osobnego pliku
- wsparcie dla procedur składowanych
- możliwość przekazania transakcji z zewnątrz
Podsumowanie
Jeśli jesteś osobą, która poszukuje prostych rozwiązań dla skomplikowanych problemów, to biblioteka FSharp.Data.SqlClient powinna przypaść Ci do gustu. Jeśli chodzi o mnie, to wszystko wskazuje na to, że zostanę z nią na dłużej. Poszukiwane przeze mnie właściwości: prosty kod oraz brak konieczności definiowania własnych typów, ale z pełnym wsparciem kompilatora, zostały odnalezione. Ekstra dodatkiem jest kompilacja kodu języka T-SQL. Pozostaje teraz zgłębiać wiedzę na temat samej biblioteki.
=