Search code examples
f#type-providers

F# TypeProviders, how to Change Database?


Am using F# TypeProviders for processing SQL data from two different servers but many databases. It is going fine except for the speed now which is becoming very slow as I add more files. As at now each file has its own connection string. I was thinking of extracting the connection string and Type provider to an external project with the ability to pass database as an argument. Trying to achieve this, I attempt the following, following the 3rd answer from the following link F# Type Provider, create with string variable:

open System.Configuration
open System.Data.SqlClient
open Microsoft.FSharp.Data.TypeProviders
open System.Data.SqlClient

let con = ConfigurationManager.AppSettings.Item("conDebug")
let setDB dbName =
    let cn = new SqlConnectionStringBuilder(con)
    cn.InitialCatalog <- dbName
    cn.ConnectionString

[<Literal>]
let conStr = setDB "AnotherDB"

type selectedDb = SqlDataConnection<conStr>

but kept on getting the error: This is not a valid constant expression Is it also possible to use the ChangeDatabase member in the SqlDataConnection?

Update: The answer suggested won't solve my problem in this case. Below is what is similar to what am doing for different tables from two different servers:

[<Literal>]
let sourceConStr = @"sourceCon"

type sourceDB = SqlDataConnection<sourceConStr>

type person =
    { Id : string
      birthDate : Nullable<DateTime>
      firstName : string
      lastName : string
      dateCreated : DateTime }

type processPeople()
    member this.makePerson(oldPerson: source.ServiceTypes.Person) =
        { Id = oldPerson.oldId
        ......some other processing of old data
        }

     member this.transferPeople conStr =
        use source = sourceDB.GetDataContext()
        use sw = File.AppendText("./ImportLog.txt")
        use targetCon = new SqlConnection(conStr)
        targetCon.Open()
        let query = "insert query"            

        let insert person =
            try
                use cmd = new SqlCommand(query, targetCon)
                ....setting parameters stuff......                
                cmd.ExecuteNonQuery() |> ignore
            with sx -> sw.WriteLine(sx.Message)
        source.source.ServiceTypes.Person
        |> Seq.map this.makePerson
        |> Seq.filter (fun p -> p.Id <> null && p.birthDate.HasValue )
        |> Seq.iter insert
        printfn "Done"

Now I need to do this for every table i.e class per table and as the files grow compilation and for intellisense to start takes so long.


Solution

  • There is a fundamental flaw in the approach you're trying to take. You want getting a connection string from your application configuration at run time and offer SqlDataConnection type provider to make its magic with underlying database.

    But this type provider simply cannot do anything at run time stage of workflow as its job has to be already completed at compile time on some known at compile time database.

    Then, you may ask, what is the point of using type provider if we want to make our code, after being compiled once, being able to work with database(s) configurable in run-time?

    Right, but we do expect results of type provider work being applicable to structurally the same databases, do we?

    So, the way out is offering type provider to do its job upon a database compileTimeDB with literally known at compile time connection string compileTimeCC, and consider all goodies we getting (type checks, Intellisense,...) parameterized upon a connection string. This connection string parameter value runTimeCC can be set at run time in any desirable way as long as it points to a database runTimeDB with the same schema as compileTimeDB .

    Illustrating this fundamental principle with a bit of code below:

    [<Literal>]
    let compileTimeCC = @"Data Source=(localdb)\ProjectsV12;Initial Catalog=compileTimeDB;Integrated Security=True;"
    .....
    type MySqlConnection = SqlDataConnection<ConnectionString = compileTimeCC>
    // Type provider is happy, let it help us writing our DB-related code
    .....
    let db = MySqlConnection.GetDataContext(runTimeCC)
    // at run time db will be a runTimeDB set by connection string runTimeCC that can be any
    // as long as runTimeDB and compileTimeDB has same schema
    .....
    

    UPDATE as question author made his problem context clearer I may suggest more specific recommendation upon approaching this with the given TP. As SO answers should be reasonably concise let's limit consideration by two legacy Person types OldPersonT1 and OldPersonT2 as data sources and one contemporary ModernPerson type as destination. I'm talking types here, it can be as many as you want instances of such around your DB farm.

    Now, let's create a single DB at your localdb named CompileTypeDB and run Sql scripts for creating tables corresponding to OldPersonT1,OldPersonT2, and ModernPerson(it's one time exercise and no real data moving will be involved). This will be a single source of type info for SqlDataConnection TP.

    Having this ready, let get back to the code:

    type CTSqlConn = SqlDataConnection<ConnectionString = @"Data Source=(LocalDB)\Projectsv12;Initial Catalog=myCompileTimeDB;Integrated Security=True">
    
    type OldPersonT1 = CTSqlConn.ServiceTypes.OldPersonT1 // just for brevity
    type OldPersonT2 = CTSqlConn.ServiceTypes.OldPersonT2
    type ModernPerson = CTSqlConn.ServiceTypes.ModernPerson
    

    Then, augment each of legacy types with the following static member (given below for OldPersonT1 only for brevity):

    type CTSqlConn.ServiceTypes.OldPersonT1 with
        static member MakeModernPersons(rtConn: string) =
            let projection (old: OldPersonT1) =
                // Just a direct copy, but you may be very flexible in spreading verification
                // logic between query, projection, and even makeModernPersons function
                // that will be processing IQueryable<ModernPerson>
                let mp = ModernPerson()
                    mp.Id <- old.Id
                    mp.birthDate <- old.birthDate
                    mp.firstName <- old.firstName 
                    mp.lastName <- old.lastName 
                    mp.dateCreated <- old.dateCreated
                    mp 
            query {
                for oldPerson in (CTSqlConn.GetDataContext(rtConn)).OldPersonT1 do
                select (projection oldPerson)
            }
    

    Now you can get hold on IQueryable<ModernPerson> from any data source of type OldPersonT1 by merely evaluating

    OldPersonT1.MakeModernPersons("real time connection string to any DB having OldPersonT1 table")
    

    For this to work real time DB may be not identical to compile time DB, it just should contain everything that OldPersonT1 has and depends upon. Similarly would be true for OldPersonT2 or any other variation type: by implementing MakeModernPersons once per variation type you get all data source instances covered.

    Doing with data destination requires a single function with signature

    let makeModernPersons destinationConnStr (source: IQueryable<ModernPerson>) =
    ...
    

    that now covers all possible combinations of Person data sources and destinations just by manipulating values of two real-time connection strings.

    Very rough cut, but the idea seems quite clear.