Skip to content

Commit

Permalink
Merge pull request #357 from cmeeren/improve-dynamic-db-docs
Browse files Browse the repository at this point in the history
Improve dynamic DB docs
  • Loading branch information
smoothdeveloper authored Oct 31, 2019
2 parents b3896d5 + ba659d7 commit c8148eb
Showing 1 changed file with 94 additions and 48 deletions.
142 changes: 94 additions & 48 deletions docs/content/dynamic local db.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,89 +6,135 @@
Dynamic creation of offline MDF
===============================
Sometimes you don't want to have to be online just to compile your programs. With FSharp.Data.SqlClient you can use a local
.MDF file as the compile time connection string, and then change your connection string at runtime when you deploy your application.
Sometimes you don't want to have to be online just to compile your programs, or
you might not have access to your production database from your CI systems. With
FSharp.Data.SqlClient you can use a local .MDF file as the compile-time
connection string, and then use a different connection string when you deploy
your application.
A connection string to a local .MDF file might look like this:
*)

open FSharp.Data

[<Literal>]
let connectionString = @"Data Source=(LocalDB)\v12.0;AttachDbFilename=C:\git\Project1\Database1.mdf;Integrated Security=True;Connect Timeout=10"
let compileConnectionString =
@"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=C:\git\Project1\Database1.mdf;Integrated Security=True"

(**
However, binary files like this are difficult to diff/merge when working with multiple developers. For this reason wouldn't it be nice
to store your schema in a plain text file, and have it dynamically create the MDF file for compile time?
However, binary files like this are difficult to diff/merge when working with
multiple developers, so you might not want to check them in. Wouldn't it be nice
to store your schema in a plain text file, and have it dynamically create the
MDF file for compile time?
Well the following scripts can do that for your project.
First create a file called `createdb.ps1`:
First create a file called `createDb.ps1` and place it in an `SQL` subfolder in
your project (you can place it in the project root to, if you want):
# this is the name that Fsharp.Data.SqlClient TypeProvider expects it to be at build time
$new_db_name = "Database1"
param(
[Parameter(Mandatory=$true)][String]$DbName,
[Parameter(Mandatory=$true)][String]$DbScript
)
$detach_db_sql = @"
use master;
GO
EXEC sp_detach_db @dbname = N'$new_db_name';
GO
IF (SELECT COUNT(*) FROM sys.databases WHERE name = '$DbName') > 0
EXEC sp_detach_db @dbname = N'$DbName'
"@
$detach_db_sql | Out-File "detachdb.sql"
sqlcmd -S "(localdb)\v11.0" -i detachdb.sql
Remove-Item .\detachdb.sql
sqlcmd -S "(LocalDB)\MSSQLLocalDB" -i "detachdb.sql"
Remove-Item "detachdb.sql"
Remove-Item "$new_db_name.mdf"
Remove-Item "$new_db_name.ldf"
if (Test-Path "$PSScriptRoot\$DbName.mdf") { Remove-Item "$PSScriptRoot\$DbName.mdf" }
if (Test-Path "$PSScriptRoot\$DbName.ldf") { Remove-Item "$PSScriptRoot\$DbName.ldf" }
$create_db_sql = @"
USE master ;
GO
CREATE DATABASE $new_db_name
ON
( NAME = Sales_dat,
FILENAME = '$PSScriptRoot\$new_db_name.mdf',
SIZE = 10,
MAXSIZE = 50,
FILEGROWTH = 5 )
LOG ON
( NAME = Sales_log,
FILENAME = '$PSScriptRoot\$new_db_name.ldf',
SIZE = 5MB,
MAXSIZE = 25MB,
FILEGROWTH = 5MB ) ;
GO
CREATE DATABASE $DbName
ON (
NAME = ${DbName}_dat,
FILENAME = '$PSScriptRoot\$DbName.mdf'
)
LOG ON (
NAME = ${DbName}_log,
FILENAME = '$PSScriptRoot\$DbName.ldf'
)
"@
$create_db_sql | Out-File "createdb.sql"
sqlcmd -S "(localdb)\v11.0" -i createdb.sql
Remove-Item .\createdb.sql
sqlcmd -S "(LocalDB)\MSSQLLocalDB" -i "createdb.sql"
Remove-Item "createdb.sql"
sqlcmd -S "(localdb)\v11.0" -i schema.sql
sqlcmd -S "(LocalDB)\MSSQLLocalDB" -i "$DbScript"
$detach_db_sql | Out-File "detachdb.sql"
sqlcmd -S "(localdb)\v11.0" -i detachdb.sql
Remove-Item .\detachdb.sql
sqlcmd -S "(LocalDB)\MSSQLLocalDB" -i "detachdb.sql"
Remove-Item "detachdb.sql"
Then change your connection string to look like this
*)

[<Literal>]
let connectionStringForCompileTime = @"Data Source=(LocalDB)\v12.0;AttachDbFilename=" + __SOURCE_DIRECTORY__ + @"\Database1.mdf;Integrated Security=True;Connect Timeout=10"
let compileConnectionString =
@"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=" + __SOURCE_DIRECTORY__ + @"\Database1.mdf;Integrated Security=True;Connect Timeout=10"

type Foo = SqlCommandProvider<"SELECT * FROM Foo", connectionStringForCompileTime>
type Foo = SqlCommandProvider<"SELECT * FROM Foo", compileConnectionString>

let myResults = (new Foo("Use your Runtime connectionString here")).Execute()

(**
Lastly, edit your `.fsproj` file and add the following to the very end right before `</Project>`
<Target Name="BeforeBuild">
<Message Text="Building out SQL Database: Database1.mdf" Importance="High" />
<Exec Command="PowerShell -NoProfile -ExecutionPolicy Bypass -Command &quot;&amp; { $(ProjectDir)Createdb.ps1 }&quot;" />
Lastly, edit your `.fsproj` file and add the following to the very end right
before `</Project>`:
<ItemGroup>
<SqlFiles Include="**\*.sql" />
<BuildDbPsScript Include="SQL\createDb.ps1" />
<BuildDbSqlScripts Include="SQL\create_myDb1.sql" DbName="Db1" />
<BuildDbSqlScripts Include="SQL\create_myDb2.sql" DbName="Db2" />
<UpToDateCheckInput Include="@(SqlFiles)" />
<UpToDateCheckInput Include="@(BuildDbPsScript)" />
<UpToDateCheckInput Include="@(BuildDbSqlScripts)" />
<UpToDateCheckInput Include="@(BuildDbSqlScripts -> 'SQL\%(DbName).mdf')" />
<UpToDateCheckInput Include="@(BuildDbSqlScripts -> 'SQL\%(DbName).ldf')" />
</ItemGroup>
<Target Name="BuildDb" BeforeTargets="BeforeBuild" Inputs="@(BuildDbSqlScripts);@(BuildDbPsScript)" Outputs="SQL\%(BuildDbSqlScripts.DbName).mdf;SQL\%(BuildDbSqlScripts.DbName).ldf">
<Message Text="DB files missing or outdated. Building out database %(BuildDbSqlScripts.DbName) using script %(BuildDbSqlScripts.Identity)" Importance="High" />
<Exec Command="PowerShell -NoProfile -ExecutionPolicy Bypass -Command &quot;&amp; { @(BuildDbPsScript) -DbName %(BuildDbSqlScripts.DbName) -DbScript %(BuildDbSqlScripts.Identity) }&quot;" />
</Target>
Now when you build, it will create a database named `Database1` and then look for a file called `schema.sql` which will be used
to create the database. It will then compile against this dynamically generated MDF file so you'll get full static type checking
without the hassle of having to have an internet connection, or deal with binary .MDF files!
<Target Name="TouchProjectFileIfSqlOrDbChanged" BeforeTargets="BeforeBuild" Inputs="@(SqlFiles);@(BuildDbPsScript);@(BuildDbSqlScripts)" Outputs="$(MSBuildProjectFile)">
<Message Text="SQL or DB files changed. Changing project file modification time to force recompilation." Importance="High" />
<Exec Command="PowerShell -NoProfile -ExecutionPolicy Bypass -Command &quot;(dir $(MSBuildProjectFile)).LastWriteTime = Get-Date&quot;" />
</Target>
*)
Now when you build, it will create the databases `SQL\Db1.mdf` and `SQL\Db2.mdf`
using the scripts `SQL\create_myDb1.sql` and `SQL\create_myDb2.sql`. It will
then compile against this dynamically generated MDF file so you'll get full
static type checking without the hassle of having to have an internet
connection, or deal with binary .MDF files!
Furthermore, the `.fsproj` edits above give the following benefits:
* The DBs are rebuilt if their corresponding SQL scripts have changed, or if the
PowerShell script has changed
* The project is rebuilt if the PowerShell script has changed
* The project is rebuilt if any SQL file has changed (both the database creation
scripts, and any other SQL scripts that SqlClient might use though the
`SqlFile` type provider)
* Incremental build - each database is only built if its corresponding SQL
script or the PowerShell script has changed
When it comes to actually making the database creation scripts (such as the
`create_myDb1.sql` in the example above), you can do this if you use SQL Server
Management Studio (SSMS):
* Connect to the database you want to copy
* Right-click the database and select Tasks -> Generate scripts
* Select what you need to be exported (for example, everything except Users).
* If SqlClient throws errors when connecting to your local database, you might
be missing important objects from your database. Make sure everything you need
is enabled in SSMS under Tools -> Options -> SQL Server Object Explorer ->
Scripting. For example, if you have indexed views and use the `WITH
(NOEXPAND)` hint in your SQL, you need the indexes too, which are not enabled
by default. In this case, enable "Script indexes" under the "Table and view
options" heading.
*)

0 comments on commit c8148eb

Please sign in to comment.