Ostatnio zacząłem pisać aplikację webową w F# i Suave, w której korzystam z bazy danych. Poniżej opiszę dwie metody, za pomocą których można się odwołać do danych z bazy.
SQLTypeProvider
Istnieje kilka frameworków, które na podstawie bazy danych są w stanie wygenerować kod klas i metod, które zapewnią dostęp do danych. Najpopularniejszym jest Entity Framework. Jednak na dobrą sprawę EF jest dość skomplikowany i generowany klasy stają się częścią naszego kodu, który commitujemy. Kiedy baza danych się zmieni musimy ręcznie wygenerować ponownie kod dostępu.
Idea Type Providerów w F# jest prosta. Podczas kompilacji (lub uruchomienia w FSI) Type Provider na podstawie źródła danych i swojej implementacji generuje typ (klasę), która zawiera definicje obiektów opisanych w danych. Dzięki w miarę dynamicznemu generowaniu, w naszym kodzie definicja typu będzie tylko jedną linijką. Natomiast konieczne jest aby w trakcie kompilacji istniał dostęp do źródła danych.
Aby skorzystać z Type Providera dla baz SQL musimy zainstalować paczkę nugetową
nuget install SQLProvider
# lub
PM> Install-Package SQLProvider
Następnie, w zależności od bazy z jakiej chcemy korzystać, musimy pobrać odpowiednią bibliotekę DLL z implementacją klas ADO.NET dla tej bazy danych. W moim przypadku jest to MySQL z paczką MySql.Data
.
W naszym kodzie dodajemy
open FSharp.Data.Sql
let [<Literal>] resolutionPath = __SOURCE_DIRECTORY__
type mySql = SqlDataProvider<
ConnectionString = "Server=localhost;Uid=root;Pwd=root;",
DatabaseVendor = Common.DatabaseProviderTypes.MYSQL,
ResolutionPath = resolutionPath,
UseOptionTypes = true >
SqlDataProvider
inicjalizowany jest za pomocą paru argumentów. ConnectionString
określa lokalizację servera, bazę danych na serwerze oraz login i hasło. DatabaseVendor
określa nam jaka to jest baza danych, na tej podstawie zostanie przeszukana ResolutionPath
aby znaleźć odpowiednią bibliotekę DLL. W tym przypadku szukamy w __SOURCE_DIRECTORY__
, czyli w miejscu z którego został uruchomiony FSI lub w miejscu gdzie znajdują się nasze pliki fs/fsx. Ostatnia opcja UseOptionTypes
określa, że kiedy w bazie danych napotkamy null
to nasz typ zwróci nam opcję o wartości None
.
Kiedy mamy zdefiniowany typ dostępu, to czas na otwarcie kontekstu
let ctx = mySql.GetDataContext()
Kontekst służy do dostępu do bazy danych. A jak go użyć? Przykład poniżej
let entities =
query {
for entity in ctx.Database.Table do
select entity
}
query
to fsharpowe wyrażenie LINQ (Language Integrated Query) które przekształca obiekty typu Seq
(System.Collections.Generic.IEnumerable<'T>
). W tym przypadku z bazy o nazwie Database
i tabeli Table
pobieramy wszystkie elementy.
Na stronie Query Expressions znajdziecie wszystkie wyrażenia którymi można filtrować elementy typu Seq
. Tak jak w C#, LINQ możemy stosować do list i tablic.
LINQ jest leniwy. To znaczy, że powyższy przykład nie sposowoduje pobrania danych z bazy w miejscu deklaracji. Dopiero kiedy po raz pierwszy będziemy chcieli użyć danych w entities
to zostanie wysłanie zapytanie SQL do bazy danych.
Seq.iter (fun e -> printfn "%A" e) entities
Ta linijka wypisze nam wszystkie elementy entities
na standardowe wyjście.
Dapper
W moim projekcie tak się złożyło, że nie mogę korzystać z Type Providera, bo w czasie kompilacji nie mam dostępu do bazy, z której moja aplikcja ma korzystać. W związku z tym posłużę się innym narzędziem - Dapperem.
Dapper jest to mini ORM (Object-Relational Mapper), czyli narzędzie które mapuje to co zwróci baza danych na obiekty. Entity Framework i NHibernate to również przykłady ORMów, chociaż te są ogromnie rozbudowane.
Dapper jest dość niewielki i prosty w użyciu. Do niego również potrzebujemy dodatkowej biblioteki DLL z bazą danych, tylko tym razem musimy dodać do niej referencję do naszego projektu.
Aby korzystać z Dappera, a dokładniej ze statycznych metod, które operują na obiekcie DbConnection
, musimy najpierw otworzyć połączenie z bazą danych.
open MySql.Data.MySqlClient
let connectionStr = "Server=localhost;Uid=root;Pwd=root;"
let cnn = new MySqlConnection(connectionStr)
cnn.Open()
Następnie zdefiniujemy sobie typ danych z naszej bazy danych
type Entity = {Id: int; Name: string}
Oraz użyjemy Dappera, żeby pobrać dane
open Dapper
let query = "SELECT id, name FROM Entities;"
let entities = Dapper.SqlMapper.Query<Entity>(cnn, query)
Voilà! Więc wystarczy, że znamy trochę SQLa i możemy w łatwy sposób dostać się do naszych danych.
Dodatkowo możemy nasze zapytania parametryzować
type QueryParam = {Number: int}
let query = "SELECT id, name FROM Entities WHERE id > @Number;"
let entities = Dapper.SqlMapper.Query<Entity>(cnn, query, {Number = 10})
Analogiczną metodą do Query, która nie zwraca 'T Seq
tylko ilość zmienionych rekordów jest Execute.
do Dapper.SqlMapper.Execute(cnn, "DELETE FROM Entities") |> ignore
Pokazałem tu dwa sposoby komunikowania się z bazą danych w F#. Który z nich wolicie? Ja trochę preferuje ten z Dapperem, ale może przekonam się do używania Type Providerów, bo jest to niesamowicie użyteczna technologia.