Home

Awesome

LiteDB.FSharp Build Status Nuget

F# Support for LiteDB

This package relies on LiteDB 4.14 >= version > 5.0 Support for v5 is work in progress and might require a full rewrite.

LiteDB.FSharp provides serialization utilities making it possible for LiteDB to understand F# types such as records, unions, maps etc. with support for type-safe query expression through F# quotations

Usage

LiteDB.FSharp comes with a custom BsonMapper called FSharpBsonMapper that you would pass to a LiteDatabase instance during initialization:

open LiteDB
open LiteDB.FSharp
open LiteDB.FSharp.Extensions

let mapper = FSharpBsonMapper()
use db = new LiteDatabase("simple.db", mapper)

LiteDB.FSharp is made mainly to work with records as representations of the persisted documents. The library requires that records have a primary key called Id or id. This field is then mapped to _id when converted to a bson document for indexing.

type Genre = Rock | Pop | Metal

type Album = {
    Id: int
    Name: string
    DateReleased: DateTime
    Genre: Genre
}

Get a typed collection from the database:

let albums = db.GetCollection<Album>("albums")

Insert documents

let metallica = 
    { Id = 1; 
      Name = "Metallica";
      Genre = Metal;
      DateReleased = DateTime(1991, 8, 12) }

albums.Insert(metallica)

Query one document by Id

// result : Album
let result = albums.findOne <@ fun album -> album.Id = 1 @> 

// OR
let id = BsonValue(1)
// result : Album
let result = albums.FindById(id)

Query many documents depending on the value of a field

// metallicaAlbums : Seq<Album>
let metallicaAlbums = albums.findMany <@ fun album -> album.Name = "Metallica" @>
// OR
let name = BsonValue("Metallica")
let query = Query.EQ("Name", name)
// metallicaAlbums : Seq<Album>
let metallicaAlbums = albums.Find(query)

Query documents by value of discriminated union

// find all albums where Genre = Rock
// rockAlbums : Seq<Album>
let rockAlbums = albums.findMany <@ fun album -> album.Genre = Rock @>

// OR 

let genre = BsonValue("Rock")
let query = Query.EQ("Genre", genre)
// rockAlbums : Seq<Album>
let rockAlbums = albums.Find(query)

Query documents between or time intervals

// find all albums released last year
let now = DateTime.Now
let dateFrom = DateTime(now.Year - 1, 01, 01) |> BsonValue
let dateTo = DateTime(now.Year, 01, 01) |> BsonValue
let query = Query.Between("DateReleased", dateFrom, dateTo)
// albumsLastYear : Seq<Album>
let albumsLastYear = albums.Find(query)

Customized Full Search using quoted expressions

// Filtering albums released a year divisble by 5
// filtered : Seq<Album>
let filtered = 
    albums.fullSearch 
        <@ fun album -> album.DateReleased @> 
        (fun dateReleased -> dateReleased.Year % 5 = 0)

Customized Full Search using Query.Where

The function Query.Where expects a field name and a filter function of type BsonValue -> bool. You can deserialize the BsonValue using Bson.deserializeField<'t> where 't is the type of the serialized value.

// Filtering albums released a year divisble by 5
let searchQuery = 
    Query.Where("DateReleased", fun bsonValue ->
        // dateReleased : DateTime
        let dateReleased = Bson.deserializeField<DateTime> bsonValue
        let year = dateReleased.Year
        year % 5 = 0
    )

let searchResult = albums.Find(searchQuery)

Query.Where: Filtering documents by matching with values of a nested DU

type Shape = 
    | Circle of float
    | Rect of float * float
    | Composite of Shape list

type RecordWithShape = { Id: int; Shape: Shape }

let records = db.GetCollection<RecordWithShape>("shapes")

let shape = 
    Composite [ 
      Circle 2.0;
      Composite [ Circle 4.0; Rect(2.0, 5.0) ]
    ]

let record = { Id = 1; Shape = shape }
records.Insert(record) |> ignore

let searchQuery = 
    Query.Where("Shape", fun bsonValue -> 
        let shapeValue = Bson.deserializeField<Shape> bsonValue
        match shapeValue with
        | Composite [ Circle 2.0; other ] -> true
        | otherwise -> false
    )
records.Find(searchQuery)
|> Seq.length
|> function 
    | 1 -> pass() // passed!
    | n -> fail()

Id auto-incremented

Add CLIMutableAttribute to record type and set Id 0

[<CLIMutable>]
type Album = {
   Id: int
   Name: string
   DateReleased: DateTime
   Genre: Genre
}
let metallica = 
   { Id = 0; 
     Name = "Metallica";
     Genre = Metal;
     DateReleased = DateTime(1991, 8, 12) }

DbRef

just as https://github.com/mbdavid/LiteDB/wiki/DbRef

open LiteDB.FSharp.Linq

[<CLIMutable>]
type Company=
  { Id : int
    Name : string}   

[<CLIMutable>]    
type Order=
  { Id :int
    Company :Company }
    
let mapper = FSharpBsonMapper()
mapper.DbRef<Order,_>(fun c -> c.Company)

Inheritence

Item1 and Item2 are inherited from IItem

we must register the type relations first globally

FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item1>()
FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item2>()

By conversion, The inherited type must has mutable field for serializable and deserializable

val mutable Id : int

Note: Because json converter find inherited type by comparing the fields names from inherited type and database

let findType (jsonFields: seq<string>) =
    inheritedTypes |> Seq.maxBy (fun tp ->
        let fields = tp.GetFields() |> Seq.map (fun fd -> fd.Name)
        let fieldsLength = Seq.length fields
        (jsonFields |> Seq.filter(fun jsonField ->
            Seq.contains jsonField fields
        )
        |> Seq.length),-fieldsLength
    )        

This means that we should not implement the some interface with different fields For example,we should not do below implementations

type Item1 =

    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number

/// unexpected codes
type Item2 =

    val mutable Id2 : int
    val mutable Art2 : string
    val mutable Name2 : string
    val mutable Number2 : int

    interface IItem with 
        member this.Art = this.Art2
        member this.Id = this.Id2
        member this.Name = this.Name2
        member this.Number = this.Number2

/// expected codes
type Item2 =

    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number

Full sample codes:

/// classlibray.fs
[<CLIMutable>]    
type EOrder=
  { Id: int
    Items : IItem list
    OrderNumRange: string }   


/// consumer.fs
type Item1 =
    /// val mutable will make field serializable and deserializable
    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    
    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number
    val mutable Barcode : string

    interface IBarcode with 
        member this.Barcode = this.Barcode    

    /// type constructor 
    new (id, art, name, number, barcode) =
        { Id = id; Art = art; Name = name; Number = number; Barcode = barcode }

type Item2 = 
    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number

    val mutable Size : int
    interface ISize with 
        member this.Size = this.Size 
    val mutable Color : string

    interface IColor with 
        member this.Color = this.Color 

    new (id, art, name, number, size, color) =
        { Id = id; Art = art; Name = name; Number = number; Size = size; Color = color }

FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item1>()
FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item2>()

let item1 = 
    Item1 ( 
        id = 0,
        art = "art",
        name = "name",
        number = 1000,
        barcode = "7254301" 
    )

let item2 = 
    Item2 ( 
        id = 0,
        art = "art",
        name = "name",
        number = 1000,
        color = "red" ,
        size = 39 
    )

let eorder = { Id = 1; Items = [item1;item2];  OrderNumRange = "" }

let queryedEOrder =
    db 
    |> LiteRepository.insertItem eorder
    |> LiteRepository.query<EOrder> 
    |> LiteQueryable.first

match queryedEOrder.Items with 
| [item1;item2] -> 
    match item1,item2 with 
    | :? IBarcode,:? IColor -> 
        pass()
    | _ -> fail()    
| _ -> fail()