Dostęp do danych w F#

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.