2008-05-17

Bygga Database Project i TFS

Att få igång byggen på Team Foundation Server 2008 med Team Build där Database Project ingår i en solutionfil är inte helt enkelt. Själv har jag inte kört Database Project något, än. I projektet jag jobbar i behövde vi få igång dagliga byggen så det föll på min lott att få det att funka.

Utöver att kompilera så bygger vi och deployar databas och webappen till en server. Det här inlägget beskriver vad vi gjorde för att få igång det. Observera att vi inte kickat igång unit-tester än, så jag vet inte hur det här fungerar ihop med det.

Skapa ett vanligt bygge

Steg ett är att skapa ett bygge i TFS 2008 på vanligt sätt och peka ut solution-filen man vill bygga.

Ange databas där bygget ska ske

När man bygger ett dabasprojekt i Visual Studio 2008 så byggs databasen enl. de inställningar man gjort i VS för det projektet. Vi låter varje utvecklare köra mot en egen instans av Sql. Dessa inställningar lagras inte i dbproj-filen, utan i en fil som slutar på dbproj.user. Denna fil är unik för varje användare och checkas därför inte in i Source Control.

På något sätt måste vi ersätta det som står i dbproj.user-filen och tala om för byggservern var den ska bygga någonstans. Det är TargetDatabase, TargetConnectionString, eller DefaultDataPath som måste anges och det finns lite olika sätt att göra det på. Buck Hodges har beskrivit hur man får igång byggen och han nämner 3 sätt att göra det på (två i texten och en i kommentarerna).

  1. Ange värdena direkt i .dbproj-filen
    Checka ut dbproj-filen, öppna den som xml-fil och lägg in värdena för fälten. Enklast är att modifiera värdena från dbproj.user-filen.
    <PropertyGroup Condition=" '$(Configuration)' == 'Default' ">
    <TargetDatabase>MyProject_Daily</TargetDatabase>
    <TargetConnectionString>Data Source=.\SQLEXPRESS;Integrated Security=True;Pooling=False</TargetConnectionString>
    <DefaultDataPath>d:\data\</DefaultDataPath>
    </PropertyGroup>



  2. Skicka in värdena som command line-argument till MSBuild, antingen i Queue Build-fönstret eller genom att lägga dem TFSBuild.rsp-filen. T.ex.
    /p:DefaultDataPath=<path>; TargetDatabase=<databaseName>;TargetConnectionString="<connection string>"



  3. Skapa en egen targets-fil och inkludera den precis innan dbproj.user importeras.

    Skapa en fil .dbproj.BuildDefaults:

    <?xml version="1.0" encoding="utf-8"?>
    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup Condition=" '$(Configuration)' == 'Default' ">
    <TargetDatabase>MyProject_Daily</TargetDatabase>
    <TargetConnectionString>Data Source=.\SQLEXPRESS;Integrated Security=True;Pooling=False</TargetConnectionString>
    <DefaultDataPath>d:\DATA\</DefaultDataPath>
    </PropertyGroup>
    </Project>

    Lägg sedan till de två översta raderna nedan i dbproj filen precis innan importen av Microsoft.VisualStudio.TeamSystem.Data.Tasks.targets.

    <!-- Import default settings -->
    <Import Condition="Exists('$(MSBuildProjectFullPath).BuildDefaults')" Project="$(MSBuildProjectFullPath).BuildDefaults" />
    <!--Import the settings-->
    <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v9.0\TeamData\Microsoft.VisualStudio.TeamSystem.Data.Tasks.targets" />

    Eftersom Microsoft.VisualStudio.TeamSystem.Data.Tasks.targets importerar dbproj.user-filen (om den finns) så kommer det som finns i BuildDefaults att ersättas av det som står i dbproj.user filen när man bygger lokalt. På byggservern finns inte dbproj.user-filen och därför kommer värdena från BuildDefaults-filen att gälla.



Alternativ 1 och 3 innebär att man (i princip) talar om i källkoden var man ska bygga. Inte så snyggt. Inte så flexibelt. Bygg-inställningarna vill man hålla borta från koden i enlighet med Separation of Concerns-tänket.



Alt. 2 är rätt ok, men jag föredrar att hålla alla bygg-inställningar i TFSBuild.proj-filen  Det enklaste är faktiskt att skicka med parametrarna när man specificerar vilken solution-fil som ska byggas (i TFSBuild.proj):



<SolutionToBuild Include="$(BuildProjectFolderPath)/../../MyProject.sln">
<Targets></Targets>
<Properties>TargetDatabase=MyProject_Daily;DefaultDataPath=e:\data;TargetConnectionString=Data Source=.\SQLEXPRESS</Properties>
</SolutionToBuild>



HACK: Observera Target Connection-strängen. Jag utnyttjar default-värden för Integrated Security. Om man lägger in semikolon, oavsett om man gör det som ; eller %3B kommer dessa värden att tolkas som egna properties med skumma fel som följd.




Vi har ställt in solution-filen på att bygga databas projektet även för Debug och Release. Om man inte gör det måste man se till att bygga Default-flavor också (Buck Hodges förklarar under Issue #3).



Nu ska allt bygga. Om du får problem, kolla Buck Hodges text.



Deploya



För att deploya databasen till en server måste man bygga databasen för den servern också. Vi har valt att köra ytterligare ett byggomgång för databasprojeket och sen efter att allt droppats så deployar vi.



Bygg för deploy



För att bygga databasprojektet använder man en MSBuild Task:



<MSBuild Projects="$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj" Targets="Rebuild" Properties="TargetDatabase=MyDb; TargetConnectionString=Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B;DefaultDataPath=d:\data\"/>


För att få den att köra måste man lägga den i en Target och se till att den exekveras ihop med den övriga kompileringen:



<Target Name="AfterCompile" DependsOnTargets="$(AfterCompile);BuildDatabaseForDeploy"/>
<Target Name="BuildDatabaseForDeploy">
<MSBuild Projects="$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj" Targets="Rebuild" Properties="TargetDatabase=MyDb; TargetConnectionString=Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B;DefaultDataPath=d:\data\"/>
</Target>


Deploy



För att deploya till en sql server kör man MSBuild igen, men med Deploy som Targets. Detta görs efter det att bygget droppats.



<Target Name="AfterDropBuild" DependsOnTargets="$(AfterDropBuild);DeployDatabase" />
<Target Name="DeployDatabase" DependsOnTargets="BuildDatabaseForDeploy">
<MSBuild Projects="$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj" Targets="Deploy" Properties="TargetDatabase=MyDb; TargetConnectionString=Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B;DefaultDataPath=d:\data\"/>
</Target>


Snygga till det hela



Koden ovan är det minimala man måste göra, för att få det hela lite snyggare bör man samla alla inställningar på ett ställe. När man bygger för deploy så skapas ingen sql-fil i drops-katalogen. Man får heller inget meddelande om att stegen körs. Nedanstående fixar till det. Lägg in det före </Project> i TFSBuild.proj.



<PropertyGroup>
<DbDeployTargetConnectionString Condition=" '$(DbDeployTargetConnectionString)'==''">Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B</DbDeployTargetConnectionString>
<DbDeployTargetDatabase Condition=" '$(DbDeployTargetDatabase)'==''">MyDb</DbDeployTargetDatabase>
<DbDeployDefaultDataPath Condition=" '$(DbDeployDefaultDataPath)'==''">d:\data\</DbDeployDefaultDataPath>
<DbDeployBuildScriptName Condition=" '$(DbDeployBuildScriptName)'==''">$(DbDeployTargetDatabase)_Deploy.sql</DbDeployBuildScriptName>
<DatabaseProjectFilePath>$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj</DatabaseProjectFilePath>
</PropertyGroup>

<PropertyGroup>
<DbForDeployProperties Condition=" '$(DbForDeployProperties)'==''">
TargetDatabase=$(DbDeployTargetDatabase);
TargetConnectionString=$(DbDeployTargetConnectionString);
DefaultDataPath=$(DbDeployDefaultDataPath);
BuildScriptName=$(DbDeployBuildScriptName);
OutputPath=$(BinariesRoot)
</DbForDeployProperties>
</PropertyGroup>

<Target Name="AfterCompile" DependsOnTargets="$(AfterCompile);BuildDatabaseForDeploy"/>
<Target Name="BuildDatabaseForDeploy">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Message="Building database for deployment.">
<Output TaskParameter="Id" PropertyName="StepId" />
</BuildStep>
<MSBuild Projects="$(DatabaseProjectFilePath)" Targets="Rebuild" Properties="$(DbForDeployProperties)"/>
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StepId)"
Status="Succeeded" />

<OnError ExecuteTargets="DbMarkBuildStepAsFailed" />
</Target>

<Target Name="AfterDropBuild" DependsOnTargets="$(AfterDropBuild);DeployDatabase" />
<Target Name="DeployDatabase" DependsOnTargets="BuildDatabaseForDeploy">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Message="Deploying database.">
<Output TaskParameter="Id" PropertyName="StepId" />
</BuildStep>
<MSBuild Projects="$(DatabaseProjectFilePath)" Targets="Deploy" Properties="$(DbForDeployProperties)"/>
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StepId)"
Status="Succeeded" />

<OnError ExecuteTargets="DbMarkBuildStepAsFailed" />
</Target>

<Target Name="DbMarkBuildStepAsFailed">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StepId)"
Status="Failed" />
</Target>




Så här ser det ut när det byggs:

Steps .



Att deploya applikationen är en annan historia. Vi deployar webapparna med en Copy Task och sen har vi en egenutvecklad Task som ändrar värden i web.config-filerna så att det passar miljön applikationen ligger i.

Inga kommentarer: