SQL Server på svenska
En svensk blogg om SQL Server, skriven på svenska. Kodexempel, fördjupningar och reflexioner på vad som händer i SQL Server-världen
Microsoft.SqlServer.Management.Smo
Missvisande felmeddelande, WITH DROP_EXISTING och rensning i MSDB


.NET (1) BLOB (1) Bloggen (2) DBA (2) Design (1) Felhantering (1) Installation (1) Optimering (4) Säkerhet (1) SQL Server 2000 (2) SQL Server 2005 (2) SQL Server 2008 (1)


 Thursday, February 18, 2010
Microsoft.SqlServer.Management.Smo

Tänk dig att du får följande uppgift:

“Vi behöver göra en liten ändring i den här databasen, som har hundratals tabeller och tusentals kolumner. Vi behöver ett bättre stöd för olika språk i applikationen så alla kolumner måste ändras till unicode (dvs varchar blir nvarchar, char blir nchar osv).”

Med små datamängder kan man alltid använda SQL Server Management Studio och göra ändringarna där. Men det är knappast ett alternativ när man har utvecklingsmiljöer, test-miljöer och produktionsmiljöer med flera installerade instanser av liknande databaser osv. När produktionssättningen av en sådan här ändring ska göras vill man inte behöva stänga sina system två veckor för att manuellt ändra kolumner.

En andra tanke skulle vara att just göra manuellt ändringar i en test-miljö och via Script-knappen i Management Studio få det SQL-script som används för att göra förändringen och spara ändringen till en text-fil. Men det skulle också vara extremt tidsödande. Jag skrev ovan om produktionsmiljöer med liknande databaser. Det är ett problem med ändra-manuellt-i-Management-Studio-och-spara-undan-scriptet-metoden. Är inte databaserna identiska måste samma procedur utföras för varje databas.

En tredje idé är ju förstås att scripta ut databaserna och göra ändringar med Sök/Ersätt i valfri text-editor. Så tänkte jag först när jag ställdes inför uppgiften. Men ganska snabbt slog jag idén ur hågen. I mitt fall har flera tabeller kolumnnamn som är datatyper (“Text” till exempel). Det gjorde enkel Sök/Ersätt i text ganska svårt. Dessutom skulle scriptet behöva omstruktureras en hel del för att saker och ting skulle göras i rätt ordning (droppa constraints och index, ändra tabellerna, återskapa constraints och index).

Därför testade jag mig fram med Microsoft.SqlServer.Management.Smo i VB.NET och kom fram till ganska lite kod som gör ett väldigt stort jobb. Jag älskar verkligen SMO! På första försöket blev jag 95% färdig. Två-tre försök senare hade jag en lösning som fungerade “universellt” för alla databaser.

Jag har även använt SMO och reguljära uttryck för att ändra alla stored procedures i en databas till att använda unicode-ekvivalenter, men den funktionen har så mycket buggar än så länge, så jag väntar lite med den. De två största problem jag haft med den är: Jag har inte ett korrekt reguljärt uttryck för att känna igen strängar. Det blir fel när det finns fyra eller sex enkla citattecken invid varandra i koden. Jag har inte heller fått till igenkänning av alla variabler och parametrar. Parametrarna är OK, det är rätt enkelt att matcha “@någonting<ett eller flera blanksteg>variabeltyp”. Men det är inte alls lika enkelt att hitta alla förekomster av varchar, text eller char i löpande SQL-kod (exempelvis “SELECT CAST(kolumnen AS varchar(10)) FROM tabellen” osv. Men jag kanske finslipar lite, och återkommer när den är “fulländad” (as if..). Tills vidare körde jag min funktion och blev sedan sittande i nästan två dagar med rättning av koden. Men det sparade ändå massor av tid, jämfört med att helt manuellt göra förändringarna.

Här är VB.NET-koden för att ändra tabeller till unicode. En klass, med en publik och en privat metod:

Imports Microsoft.SqlServer.Management
Public Class TableUnicodyfier 
    'Sex globaler, två för att lagra fel och constraints, två som är namn på SMO-objekt 
    'och två som är självaste SMO-objekten. Inga publika variabler. 
    Private clConstraints As Collection 
    Private clErrors As Collection 
    Private s As Smo.Server 
    Private d As Smo.Database 
    Private sDBName As String 
    Private sInstanceName As String 
    Public Sub New(ByVal DBName As String, ByVal InstanceName As String
        sDBName = DBName 
        sInstanceName = InstanceName 
    End Sub 
    'Publik metod som returnerar ett SQL-script för att ändra alla text-kolumner till Unicode. 
    Public Function ReplaceAllTablesWithUnicode() As String 

        'Variabler 
        Dim sError As String = "" 
        Dim j As Integer = 0 
        Dim i As Integer = 0 
        Dim str As String = "" 

       
'Initiera object 
        clConstraints = New Collection() 
        clErrors = New Collection() 
        s = New Smo.Server(sInstanceName) 
        d = s.Databases(sDBName)

        'Viktigt!! Jag föredrar att kunna granska alla ändringar i text innan de genomförs (jag kanske är feg?). 
        'Om inte raden nedan finns med görs alla ändringar "på riktigt". Finns den med sparas bara alla SQL-anrop 
        'och kan hämtas sedan. 
        s.ConnectionContext.SqlExecutionModes = Common.SqlExecutionModes.CaptureSql 
 

        'Loopa igenom alla tabeller och kör replaceTableWithUnicode 
        For Each t As Smo.Table In d.Tables 
            If Not t.IsSystemObject Then 
                replaceTableWithUnicode(t.Name, True
            End If 
            i += 1 
            Application.DoEvents() 
        Next 
 

        'Eftersom replaceTableWithUnicode bara droppar Foreign Keys men inte återskapar dem så gör vi det sist av allt. 
        'Anledningen är förstås att alla tabeller måste ändras innan vi kan återskapa FK:n igen. Annars riskerar vi 
        'att försöka skapa en FK mellan en varchar-kolumn i tabell A och en nvarchar-kolumn i tabell B. 
        'Därför loopar vi igenom clConstraints och kör Create på varje FK. 
        For Each f2 As Object In clConstraints 
            f2.create() 
        Next 
 

        'Loopa igenom all SQL-text som SMO sparat åt oss istället för att genomföra förändringarna på riktigt. 
        'Bygg upp en resultat-sträng 
        For Each str2 As String In s.ConnectionContext.CapturedSql.Text 
            str &= str2 & vbCrLf & "GO" & vbCrLf 
        Next 
 

        'Om några fel uppstod under körningen så har de objekten lagrats i clErrors. Loopa igenom den och 
        'lägg till även den SQL-koden sist i resultat-strängen. 
        If clErrors.Count > 0 Then 
            str &= "--Errors occured while saving scripts" & vbCrLf 
            For Each o As Object In clErrors 
                For Each strScript As String In o.script() 
                    str &= strScript & vbCrLf 
                Next 
            Next 
        End If 

        'str innehåller nu: 
        '- Drop statements för alla Foreign Key constraints 
        '- Drop-statements för alla index, primärnycklar, defaults och triggers 
        '- Alter statements för alla tabeller 
        '- Create-statements för alla index, primärnycklar, defaults och  triggers 
        '- Create-statements för alla Foreign Key constraints 
        Return str 
    End Function

    Private Function replaceTableWithUnicode(ByVal tableName As String, ByVal commit As Boolean) As String 
        Dim cl As New Collection 
        Dim bChange As Boolean 
        Dim t As Smo.Table = d.Tables(tableName) 
        Dim c As Smo.Column 
        Dim str As String = "" 


 
        'Loopa igenom alla kolumner. Ändra dem till respektive Unicode-ekvivalent 
        For Each c In t.Columns 
            bChange = False 
            Select Case c.DataType.Name 
                Case "varchar" 
                    bChange = True 
                    If c.DataType.MaximumLength > 4000 Then 
                        c.DataType = Smo.DataType.NVarCharMax 
                    Else 
                        c.DataType = Smo.DataType.NVarChar(c.DataType.MaximumLength) 
                    End If 
                Case "char" 
                    bChange = True 
                    c.DataType = Smo.DataType.NChar(c.DataType.MaximumLength) 
                Case "text" 
                    bChange = True 
                    c.DataType = Smo.DataType.NVarCharMax 
            End Select 
            'Notera att vi inte har gjort Alter på tabellen ännu. Än så länge har vi bara ändrat i kolumn-objekten. 
            'Om någon kolumn ska ändras så är det säkrast att återskapa alla default-constraints, annars kommer 
            'SQL Server att gnälla. Vi loopar igenom dem, lägger till dem i cl och droppar dem sedan. På så sätt 
            'genererar ett DROP-statement till vår SQL-kod, och vi kan göra Create senare när tabellen är ändrad. 
            If bChange Then 
                If Not c.DefaultConstraint Is Nothing Then 
                    cl.Add(c.DefaultConstraint) 
                    c.DefaultConstraint.Drop() 
                End If 
            End If 
        Next 

         Try 
            'Nu ska vi hitta alla FK-constraints som refererar till den här tabellen. 
            'För varje FK som refererar till den här tabellen - lägg till den i både funktionens interna 
            'cl2 och i klassvariablen clConstraints 
            'Sist - loopa igenom alla constraints i cl2 och droppa. Resultatet blir att alla FK kommer att droppas 
            'men själva objekten kommer att finnas kvar i clConstraints, för att anropande funktion ska kunna återskapa dem 
            For Each t2 As Smo.Table In d.Tables 
                Dim cl2 As New Collection 
                For Each f2 As Smo.ForeignKey In t2.ForeignKeys 
                    If f2.ReferencedTable.ToUpper() = tableName.ToUpper() Then 
                        cl2.Add(f2) 
                        clConstraints.Add(f2) 
                    End If 
                Next 
                For Each f2 As Smo.ForeignKey In cl2 
                    f2.Drop() 
                Next 
            Next 
            'Droppa även alla FK som är skapade i aktuell tabell och som refererar andra tabeller 
            Do Until t.ForeignKeys.Count = 0 
                clConstraints.Add(t.ForeignKeys(0)) 
                t.ForeignKeys(0).Drop() 
            Loop 
            'Droppa alla index. Lägg först till indexet i den privata variabeln cl 
            Do Until t.Indexes.Count = 0 
                'Om inte den här raden är med funkar inte koden för Primärnycklar. Raden gör ingenting mer än att 
                'läsa indextypen, men tydligen görs något i själva Index-objektet som initierar den korrekt för primärnycklar. 
                Dim it As Smo.IndexKeyType = t.Indexes(0).IndexKeyType 
                cl.Add(t.Indexes(0)) 
                t.Indexes(0).Drop() 
            Loop
            'Och slutligen samma sak med alla triggers i tabellen 
            Do Until t.Triggers.Count = 0 
                cl.Add(t.Triggers(0)) 
                t.Triggers(0).Drop() 
            Loop 

            'Sådärja, då är alla beroende objekt droppade. Då gör vi Alter på tabellen för att ge genomslag 
            'åt våra förändringar av kolumn-datatyper 
            Try 
                t.Alter() 
            Catch ex As Exception 
                'Och det här kan ju gå fel om vi glömt något, så då sparar vi i sådant fall undan tabellen i clErrors 
                MsgBox("Error when altering table " & t.Name) 
                clErrors.Add(t) 
            End Try

            'Nu är tabellen ändrad, men den har varken index, primärnycklar defaults eller triggers. 
            'Tur att vi sparat undan de objekten i privata variabeln cl. Loopa och återskapa. 
            For Each o As Object In cl 
                Try 
                    o.Create() 
                Catch ex As Exception 
                    'Det här är fult, men jag vill direkt få reda på när något går fel, och få se en Stacktrace. 
                    'I normala fall ska naturligtvis inte en sådan här klass försöka sig på att kommunicera interaktivt. 
                    'Den som orkar fixar självklart en ErrorMessages-collection som public property i klassen. 
                    MsgBox("Error when creating object(s)" & vbCrLf & ex.ToString()) 
                    clErrors.Add(o) 
                End Try 
            Next 
        Catch ex As Exception 
            MsgBox(ex.ToString()) 
        End Try 
        Return str 
    End Function
End Class

Andra intressanta blogginlägg om: , , ,



Comments [0]   Kategorier: .NET | DBA

 Monday, October 19, 2009
Missvisande felmeddelande, WITH DROP_EXISTING och rensning i MSDB

Jag har upptäckt att MSDB i en server jag hanterar är ALLDELES för välfylld. Att droppa en databas har tagit flera timmar om jag valt att radera backup-historik samtidigt. Därför googlade jag och fann en användbar SP: spDeleteBackupRestoreHistory hos DBA and SysAdmin workd. Det är en procedur som rensar backuphistorik ändre än X dagar. Mycket användbar SP som redan hamnat som ett veckojobb på en testmaskin och som jag körde över helgen på en produktionsmaskin, för att den skulle rensa bort all historik som är äldre än två år (jag hade sex år gammal historik sparad).

Det hade gått alldeles utmärkt om inte nedanstående “trevliga” lilla felmeddelande uppenbarat sig i Query Analyzer strax efter att jag lämnat datorn att jobba över helgen:

[Microsoft][ODBC SQL Server Driver][DBNETLIB]ConnectionCheckForData (CheckforData()).Server: Msg 11, Level 16, State 1, Line 0
Generalnetwork error. Check your network documentation.Connection Broken.

Jobbigt tänkte jag, men det är säkert något tillfälligt – någon som i helgen ryckte och drog i någon sladd. Så jag testade att köra igång den igen, och fick direkt samma fel. Jag bytte databas och testade lite olika queries utan att få något felmeddelande. Jag gick tillbaka till MSDB och körde igen, och fick samma fel. Andra queries gick utmärkt att köra i MSDB, det var bara spDeleteBackupRestoreHistory som ballade ur.

Jag loggade in direkt på servern och öppnade en anslutning mot Localhost och körde spDeleteBackupRestoreHistory och fick åter samma fel. Ganska säker på att det verkligen inte är ett nätverksproblem körde jag DBCC CHECKDB på databasen och upptäckte ett fel på indexet backupsetuuid i tabellen backupset.

Nu när jag visste vad felet var kunde jag åtgärda det genom att bygga om indexet. Tabellen innehåller ungefär en miljon rader, och att bygga om indexet tog fem sekunder. “Förarbetet” – det vill säga att hitta vad som faktiskt var fel – tog däremot ungefär en timme. Nu är felet åtgärdat, och spDeleteBackupRestoreHistory är igång igen. Återstår att söka efter orsaken till att ett index gått sönder från början.

CREATE
  INDEX [backupsetuuid]
ON backupset ([backup_set_uuid])
  WITH DROP_EXISTING




Comments [0]   Kategorier: DBA | SQL Server 2000


Top Datorer bloggar
Bloggar.Topplista.se - topplistan med de b�sta bloggarna - l�gg till din blogg du ocks�!
BlogRankers.com
Bloggtoppen.se
Datorer
BloggRegistret.se




Total Posts: 14
This Year: 2
This Month: 0
This Week: 0
Comments: 64