{{< alert class="info" >}} You can read the source code for this article. {{< /alert >}}
For some time now, I've been waiting for an opportunity to use SqlFun1, a data access library for F#. Yesterday, it finally happened when a friend of mine decided to give F# a go.
What's the deal with this library? You can watch the talk "F#, SQL and lot of fun F# SQL micro orm" to understand some of its design decisions. However, summarizing what it does in practice: it provides a quasi static analysis of your SQL queries. This is a vague statement, yes, but it means that the library will check, before you run any query, if you object is correctly mapping with your query result, if the query has correct parameters and so on.
While this might be less powerful than something as FSharp.Data.Npgsql that provides actual compile-time guarantees for PostgreSQL databases. It has the advantage of keeping your build time dependencies less complex, as you don't need to deal with DLLs or have a running database during build-time.
Previously, the main problem I had with the library was writing code
that didn't depend on module state such as the project's README.
While I, personally, don't like this approach, it does have an
advantage that I was not able to fully reproduce on my approach. When
you hold the state on your module, the only thing you need to worry
about is to actually use the module once. In SqlFun's case, this would
mean that a simple open
statement would validate the queries.
Let's deep dive into the code. In order to make things easier to deal with in a F# script, the database of choice will be SQLite. Install the needed packages and open them:
#r "nuget: SqlFun, 2.2.1"
#r "nuget: Microsoft.Data.Sqlite, 8.0.0-preview.5.23280.1"
open SqlFun
open SqlFun.GeneratorConfig
open SqlFun.Queries
open Microsoft.Data.Sqlite
This is all we need to have access to SQLite and SqlFun on our script. SqlFun needs some boilerplate code to make our lives easier:
module Database =
// string -> unit -> SqliteConnection
let createConnection connectionStr: unit -> SqliteConnection =
(fun () -> new SqliteConnection(connectionStr))
// (unit -> 'a (requires :> IDbConnection)) -> GeneratorConfig
let generatorConfig connection = createDefaultConfig connection
// (unit -> 'a) -> string -> 'b
let sql connection commandText = sql (generatorConfig connection) commandText
// string -> (IDataContext -> 'a) -> 'a
let run connectionStr f =
DbAction.run (createConnection connectionStr) f
This is trivial, we receive a connection string, open the database, create the tables and close the database. Now comes the interesting part! SqlFun needs some boilerplate code to make our lives easier:
Breaking this code apart, we have four functions:
createConnection
: this function receives a connection string and returns another function that, when called, returns a SQLite connection.generatorConfig
: looking at the function signature, theconnection
parameter expects the exact result ofcreateConnection
when you pass just the connection string. It then returns a configuration record.sql
: here, again, we receive a connection string and a SQL query string.run
: different from the other function,run
expects an opened connection and a function that we will define later in our repositories.
Now, let's write a small piece of code to create our database tables:
module Infrastructure =
let createDatabase (connectionStr) =
let db = new SqliteConnection(connectionStr)
db.Open()
let cmd = new SqliteCommand("
CREATE TABLE user (
ID INTEGER NOT NULL,
NAME TEXT NOT NULL
)
", db)
cmd.ExecuteNonQueryAsync() |> ignore
db.Close() |> ignore
Define a domain record to be used on our repository:
module Domain =
type User = { Id: string; Name: string }
Here comes the time to define our Repository
module:
module Repository =
open Database
type User(connection) =
member val Get: int -> DbAction<Domain.User> =
sql connection "select id, name from user where id = @id"
type Repository =
{ User: User }
let create connectionStr =
let db = createConnection connectionStr ()
let connection = (fun _ -> db)
{ User = User(connectionStr) }
The Repository
module contains a User
class that receives a unit -> IDbConnection
as a constructor parameter. This connection is then
passed to the sql
function inside each member of the class. Note
that the Get
member is a val
, not a method. This is important
because it allows us to evaluate the sql
function during the class
instantiation.
The sql
connects to the database and generates the code needed to
process the query. However, note that this code generation is stored
on the class member. Everything is ready for you to start calling your
query through your repository methods.
As previously mentioned, the disadvantage of the class approach
compared to the module one is that you have to keep track of your
instance throughout your application. Thankfully, it's possible to
borrow something I learned on Alex Edwards' Let's Go book. In Go you
would store your repositories on a single struct
that would be
passed around your application as a dependency:
type Models struct {
User UserRepository
}
func NewModels(db *sql.DB) {
return Models{
User: NewUserRepository(db)
}
}
It's easy to do the same in F#, but using records instead of structs.
This is exactly what the Repository
record represents!
Putting this code to use looks like the following:
let connectionStr = "Data Source=sqlite.db"
Infrastructure.createDatabase connectionStr
let repository = Repository.create connectionStr
The funny part is that running this code actually showed an error on my program:
SqlFun.Exceptions.CompileTimeException: Error generating function Int32 -> IDataContext -> User for sql command select id, name from user where id = @id
---> System.Exception: No column found for Id field. Expected: Id
---> System.InvalidOperationException: No coercion operator is defined between types 'System.Int64' and 'System.String'.
Our domain is incorrect, User
has an int
Id, not a string Id! The
correct implementation is this:
module Domain =
type User = { Id: int; Name: string }
We are almost there
One thing to remember is that SqlFun needs to actually run a query to
determine if the return type should be an option
or not.
let repository = Repository.create connectionStr
repository.User.Get 123 |> Database.run connection
System.Exception: Value does not exist. Use option type.
Plugging our repository to ASP.NET
So far this library seems to be groovy but how could we proceed to plug it on an ASP.NET application? This can be done through ASP.NET's own Dependency Injection mechanism.
let configureServices config (services: IServiceCollection) =
let repository = Repository.create config.SqlConnectionString
services.TryAddSingleton<Repository.Repository>
(fun _ -> repository) |> ignore
services
The repository is instantiated allowing SqlFun to validate the queries contained on the inner repositories as previously explained. After this, adding a Singleton service one for the repository. Why singleton? Because using the same instance throughout the lifetime of our web application avoids validating the queries again after startup.
A Giraffe handler would look like this:
let getUser config userId : HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) -> task {
let repository = ctx.GetService<Repository.Repository>()
let user =
repository.User.Get userId
|> Database.run config.SqlConnectionString
return!
match user with
| Some _ -> text "user found" next ctx
| None -> RequestErrors.notFound (text "user not found") next ctx
}
-
Actually, there was a previous attempt, but I failed miserably. This article is my redemption arc with the library. 🥲↩