Enhver, der nogensinde har udviklet applikationer, der bruger en database, har sandsynligvis stået over for problemet med at opdatere databasestrukturen, når applikationen implementeres og opdateres.
Den mest almindelige tilgang er at oprette et sæt SQL-scripts for at ændre databasestrukturen fra version til version. Selvfølgelig er der betalte værktøjer, men de løser ikke altid problemet med fuld automatisering af opdateringen.
Migreringsteknologien, der først blev introduceret i Hibernate ORM og implementeret i Linq, er meget god og praktisk, men den indebærer en "kode først"-strategi til udvikling af en databasestruktur, hvilket er meget besværligt for eksisterende projekter, og brugen af triggere, lagrede procedurer og funktioner i en database gør overgangen til "kode først" strategien næsten umulig.
Denne artikel foreslår en alternativ tilgang til at løse dette problem – lagring af en referencedatabasestruktur i en XML-fil og automatisk generering af et SQL-script baseret på sammenligningen af referencen og eksisterende struktur. Så lad os begynde...
Generer XML-fil med databasestruktur
Vi vil bruge DbSyncSample-databasen. Scriptet til oprettelse af databasen er vist nedenfor.
USE [DbSyncSample] GO /****** Object: Table [dbo].[Orders] Script Date: 06/01/2017 10:37:43 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Orders]( [Id] [int] IDENTITY(1,1) NOT NULL, [OrderNumber] [nvarchar](50) NULL, [OrderTime] [datetime] NULL, [TotalCost] [decimal](18, 2) NOT NULL, CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [IX_Orders_OrderNumber] ON [dbo].[Orders] ( [OrderNumber] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO /****** Object: Table [dbo].[Details] Script Date: 06/01/2017 10:37:43 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Details]( [Id] [int] IDENTITY(1,1) NOT NULL, [Descript] [nvarchar](150) NULL, [OrderId] [int] NULL, [Cost] [decimal](18, 2) NOT NULL, CONSTRAINT [PK_Details] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO /****** Object: Trigger [Details_Modify] Script Date: 06/01/2017 10:37:43 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TRIGGER [dbo].[Details_Modify] ON [dbo].[Details] AFTER INSERT,UPDATE AS BEGIN UPDATE Orders SET TotalCost = s.Total FROM ( SELECT i.OrderId OId, SUM(d.Cost) Total FROM Details d JOIN inserted i ON d.OrderId=i.OrderId GROUP BY i.OrderId ) s WHERE Id=s.OId END GO /****** Object: Trigger [Details_Delete] Script Date: 06/01/2017 10:37:43 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TRIGGER [dbo].[Details_Delete] ON [dbo].[Details] AFTER DELETE AS BEGIN UPDATE Orders SET TotalCost = s.Total FROM ( SELECT i.OrderId OId, SUM(d.Cost) Total FROM Details d JOIN deleted i ON d.OrderId=i.OrderId GROUP BY i.OrderId ) s WHERE Id=s.OId END GO /****** Object: Default [DF_Details_Cost] Script Date: 06/01/2017 10:37:43 ******/ ALTER TABLE [dbo].[Details] ADD CONSTRAINT [DF_Details_Cost] DEFAULT ((0)) FOR [Cost] GO /****** Object: Default [DF_Orders_TotalCost] Script Date: 06/01/2017 10:37:43 ******/ ALTER TABLE [dbo].[Orders] ADD CONSTRAINT [DF_Orders_TotalCost] DEFAULT ((0)) FOR [TotalCost] GO /****** Object: ForeignKey [FK_Details_Orders] Script Date: 06/01/2017 10:37:43 ******/ ALTER TABLE [dbo].[Details] WITH CHECK ADD CONSTRAINT [FK_Details_Orders] FOREIGN KEY([OrderId]) REFERENCES [dbo].[Orders] ([Id]) GO ALTER TABLE [dbo].[Details] CHECK CONSTRAINT [FK_Details_Orders] GO
Opret en konsolapplikation og link Shed.DbSync nuget-pakken til den.
XML-databasestrukturen er som følger:
class Program { private const string OrigConnString = "data source=.;initial catalog=FiocoKb;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"; static void Main(string[] args) { // getting XML with the database structure var db = new Shed.DbSync.DataBase(OrigConnString); var xml = db.GetXml(); File.WriteAllText("DbStructure.xml", xml); } }
Efter at have kørt programmet, ser vi følgende i filen DbStructure.xml:
<?xml version="1.0"?> <DataBase xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Version>0</Version> <Tables> <Table Name="Orders" ObjectId="2137058649" ParentObjectId="0"> <Columns> <Column Name="Id"> <ColumnId>1</ColumnId> <Type>int</Type> <MaxLength>4</MaxLength> <IsNullable>false</IsNullable> <IsIdentity>true</IsIdentity> <IsComputed>false</IsComputed> </Column> <Column Name="OrderNumber"> <ColumnId>2</ColumnId> <Type>nvarchar</Type> <MaxLength>100</MaxLength> <IsNullable>true</IsNullable> <IsIdentity>false</IsIdentity> <IsComputed>false</IsComputed> </Column> <Column Name="OrderTime"> <ColumnId>3</ColumnId> <Type>datetime</Type> <MaxLength>8</MaxLength> <IsNullable>true</IsNullable> <IsIdentity>false</IsIdentity> <IsComputed>false</IsComputed> </Column> <Column Name="TotalCost"> <ColumnId>4</ColumnId> <Type>decimal</Type> <MaxLength>9</MaxLength> <IsNullable>false</IsNullable> <IsIdentity>false</IsIdentity> <IsComputed>false</IsComputed> </Column> </Columns> <Indexes> <Index Name="PK_Orders"> <IndexId>1</IndexId> <Type>CLUSTERED</Type> <IsUnique>true</IsUnique> <IsPrimaryKey>true</IsPrimaryKey> <IsUniqueConstraint>false</IsUniqueConstraint> <Columns> <IndexColumn> <TableColumnId>1</TableColumnId> <KeyOrdinal>1</KeyOrdinal> <IsDescendingKey>false</IsDescendingKey> </IndexColumn> </Columns> </Index> <Index Name="IX_Orders_OrderNumber"> <IndexId>2</IndexId> <Type>NONCLUSTERED</Type> <IsUnique>false</IsUnique> <IsPrimaryKey>false</IsPrimaryKey> <IsUniqueConstraint>false</IsUniqueConstraint> <Columns> <IndexColumn> <TableColumnId>2</TableColumnId> <KeyOrdinal>1</KeyOrdinal> <IsDescendingKey>false</IsDescendingKey> </IndexColumn> </Columns> </Index> </Indexes> <PrimaryKey Name="PK_Orders" ObjectId="5575058" ParentObjectId="2137058649"> <UniqueIndexId>1</UniqueIndexId> </PrimaryKey> <ForeignKeys /> <Defaults> <Default Name="DF_Orders_TotalCost" ObjectId="69575286" ParentObjectId="2137058649"> <ParentColumnId>4</ParentColumnId> <Definition>((0))</Definition> </Default> </Defaults> </Table> <Table Name="Details" ObjectId="85575343" ParentObjectId="0"> <Columns> <Column Name="Id"> <ColumnId>1</ColumnId> <Type>int</Type> <MaxLength>4</MaxLength> <IsNullable>false</IsNullable> <IsIdentity>true</IsIdentity> <IsComputed>false</IsComputed> </Column> <Column Name="Descript"> <ColumnId>2</ColumnId> <Type>nvarchar</Type> <MaxLength>300</MaxLength> <IsNullable>true</IsNullable> <IsIdentity>false</IsIdentity> <IsComputed>false</IsComputed> </Column> <Column Name="OrderId"> <ColumnId>3</ColumnId> <Type>int</Type> <MaxLength>4</MaxLength> <IsNullable>true</IsNullable> <IsIdentity>false</IsIdentity> <IsComputed>false</IsComputed> </Column> <Column Name="Cost"> <ColumnId>4</ColumnId> <Type>decimal</Type> <MaxLength>9</MaxLength> <IsNullable>false</IsNullable> <IsIdentity>false</IsIdentity> <IsComputed>false</IsComputed> </Column> </Columns> <Indexes> <Index Name="PK_Details"> <IndexId>1</IndexId> <Type>CLUSTERED</Type> <IsUnique>true</IsUnique> <IsPrimaryKey>true</IsPrimaryKey> <IsUniqueConstraint>false</IsUniqueConstraint> <Columns> <IndexColumn> <TableColumnId>1</TableColumnId> <KeyOrdinal>1</KeyOrdinal> <IsDescendingKey>false</IsDescendingKey> </IndexColumn> </Columns> </Index> </Indexes> <PrimaryKey Name="PK_Details" ObjectId="117575457" ParentObjectId="85575343"> <UniqueIndexId>1</UniqueIndexId> </PrimaryKey> <ForeignKeys> <ForeignKey Name="FK_Details_Orders" ObjectId="149575571" ParentObjectId="85575343"> <ReferenceTableId>2137058649</ReferenceTableId> <References> <Reference> <ColumnId>1</ColumnId> <ParentColumnId>3</ParentColumnId> <ReferenceColumnId>1</ReferenceColumnId> </Reference> </References> <DeleteAction>NO_ACTION</DeleteAction> <UpdateAction>NO_ACTION</UpdateAction> </ForeignKey> </ForeignKeys> <Defaults> <Default Name="DF_Details_Cost" ObjectId="101575400" ParentObjectId="85575343"> <ParentColumnId>4</ParentColumnId> <Definition>((0))</Definition> </Default> </Defaults> </Table> </Tables> <Views /> <ProgrammedObjects> <ProgObject Name="Details_Modify" ObjectId="165575628" ParentObjectId="0"> <Definition>CREATE TRIGGER [dbo].[Details_Modify] ON dbo.Details AFTER INSERT,UPDATE AS BEGIN UPDATE Orders SET TotalCost = s.Total FROM ( SELECT i.OrderId OId, SUM(d.Cost) Total FROM Details d JOIN inserted i ON d.OrderId=i.OrderId GROUP BY i.OrderId ) s WHERE Id=s.OId END</Definition> <Type>SQL_TRIGGER</Type> </ProgObject> <ProgObject Name="Details_Delete" ObjectId="181575685" ParentObjectId="0"> <Definition>CREATE TRIGGER [dbo].[Details_Delete] ON dbo.Details AFTER DELETE AS BEGIN UPDATE Orders SET TotalCost = s.Total FROM ( SELECT i.OrderId OId, SUM(d.Cost) Total FROM Details d JOIN deleted i ON d.OrderId=i.OrderId GROUP BY i.OrderId ) s WHERE Id=s.OId END</Definition> <Type>SQL_TRIGGER</Type> </ProgObject> </ProgrammedObjects> </DataBase>
Implementering/opdatering af databasestruktur ved hjælp af XML
Opret endnu en tom DbSyncSampleCopy-database, tilføj følgende kode til konsolprogramkoden:
class Program { private const string OrigConnString = "data source=.;initial catalog=DbSyncSample;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"; private const string TargetConnString = "data source=.;initial catalog=DbSyncSampleCopy;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"; static void Main(string[] args) { // getting XML with the structure of the reference database var dborig = new Shed.DbSync.DataBase(OrigConnString); var xml = dborig.GetXml(); File.WriteAllText("DbStructure.xml", xml); // if you need to clear the structure of the target database, use // Shed.DbSync.DataBase.ClearDb(TargetConnString); // update the structure of the target database var dbcopy = Shed.DbSync.DataBase.CreateFromXml(xml); dbcopy.UpdateDb(TargetConnString); // in fact, you can use one line: // dborig.UpdateDb(TargetConnString); // create dbcopy only to demonstrate the creation of a database object from XML } }
Efter at have kørt programmet, kan du kontrollere, at DbSyncSampleCopy nu har en tabelstruktur, der er identisk med referencedatabasen. Du er velkommen til at eksperimentere med at ændre referencestrukturen og opdatere målstrukturen.
I testscenarier skal du muligvis oprette en testdatabase hver gang fra bunden. I dette tilfælde vil det være nyttigt at bruge funktionen Shed.DbSync.DataBase.ClearDb(string connString).
Automatisk databasestruktursporing
Struktursporingen er lavet til en separat funktion, som skal kaldes ved start/genstart af applikationen, eller et andet sted efter anmodning fra en udvikler.
static void SyncDb() { // autotracking of database structure Shed.DbSync.DataBase.Syncronize(OrigConnString, @"Struct\DbStructure.xml", // path to the structure file @"Struct\Logs", // path to synchronization log folder @"Struct\update_script.sql" // (optional) in case of defining this parameter // the script generated for the database update // will be stored within it ); }SCRIPT
Sporing udføres ved hjælp af Version parameter (tag) i XML. Scenariet for brug af proceduren er som følger:
Tildel en version til en database. I Microsoft SQL Server Management Studio skal du højreklikke på noden for den nødvendige database og vælge Egenskaber.
Klik derefter på Udvidede egenskaber og tilføj versionsegenskaben med værdi 1 til egenskabstabellen. Med hver efterfølgende ændring af strukturen bør denne egenskab øges med 1.
Når du starter programmet, oprettes filen, hvis der ikke er nogen XML-fil, eller hvis versionen er mindre end databasens.
Hvis versionen af XML-filen er større end databasens, genereres og udføres et script til at opdatere databasen.
Hvis der opstår fejl under udførelsen af scriptet, rulles alle ændringer tilbage.
Synkroniseringsresultaterne skrives til logfilen, der er oprettet i mappen angivet af logDitPath-parameteren.
Hvis parameteren SqlScriptPath er angivet, oprettes en fil med scriptet fra punkt 4.