Iedereen die ooit applicaties heeft ontwikkeld die een database gebruiken, heeft waarschijnlijk te maken gehad met het probleem van het bijwerken van de databasestructuur wanneer de applicatie wordt geïmplementeerd en bijgewerkt.
De meest gebruikelijke aanpak is om een set SQL-scripts te maken om de databasestructuur van versie tot versie aan te passen. Natuurlijk zijn er betaalde tools, maar die lossen niet altijd het probleem van volledige automatisering van de update op.
De migratietechnologie, voor het eerst geïntroduceerd in Hibernate ORM en geïmplementeerd in Linq, is erg goed en handig, maar het impliceert een "code eerst"-strategie voor het ontwikkelen van een databasestructuur, wat erg bewerkelijk is voor bestaande projecten, en het gebruik van triggers, opgeslagen procedures en functies in een database maakt de overgang naar de "code first"-strategie bijna onmogelijk.
Dit artikel suggereert een alternatieve benadering om dit probleem op te lossen:het opslaan van een referentiedatabasestructuur in een XML-bestand en het automatisch genereren van een SQL-script op basis van de vergelijking van de referentie en de bestaande structuur. Dus laten we beginnen...
XML-bestand genereren met databasestructuur
We zullen de DbSyncSample-database gebruiken. Het script voor het maken van de database wordt hieronder weergegeven.
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
Maak een console-applicatie en koppel er het Shed.DbSync nuget-pakket aan.
De XML-databasestructuur is als volgt:
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); } }
Nadat we het programma hebben uitgevoerd, zien we het volgende in het bestand 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>
Implementatie/update van databasestructuur met XML
Maak nog een lege DbSyncSampleCopy-database, voeg de volgende code toe aan de consoleprogrammacode:
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 } }
Nadat u het programma hebt uitgevoerd, kunt u controleren of de DbSyncSampleCopy nu een tabelstructuur heeft die identiek is aan de referentiedatabase. Voel je vrij om te experimenteren met het wijzigen van de referentiestructuur en het bijwerken van de doelstructuur.
In testscenario's moet u mogelijk elke keer opnieuw een testdatabase maken. In dit geval is het handig om de functie Shed.DbSync.DataBase.ClearDb(string connString) te gebruiken.
Automatisch bijhouden van databasestructuur
De structuurtracking is een aparte functie gemaakt, die moet worden aangeroepen bij het starten/herstarten van de applicatie, of op een andere plaats op verzoek van een ontwikkelaar.
P>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
Tracking wordt uitgevoerd met behulp van de Version-parameter (tag) in XML. Het scenario voor het gebruik van de procedure is als volgt:
Wijs een versie toe aan een database. Klik in Microsoft SQL Server Management Studio met de rechtermuisknop op het knooppunt van de vereiste database en selecteer Eigenschappen.
Klik vervolgens op Uitgebreide eigenschappen en voeg de eigenschap Version met waarde 1 toe aan de eigenschappentabel. Bij elke volgende wijziging van de structuur moet deze eigenschap met 1 worden verhoogd.
Als u de toepassing start, wordt het bestand aangemaakt als er geen XML-bestand is of als de versie kleiner is dan die van de database.
Als de versie van het XML-bestand groter is dan die van de database, wordt een script gegenereerd en uitgevoerd om de database bij te werken.
Als er fouten optreden tijdens de uitvoering van het script, worden alle wijzigingen teruggedraaid.
De synchronisatieresultaten worden geschreven naar het logbestand dat is gemaakt in de map die is opgegeven door de parameter logDitPath.
Als de parameter SqlScriptPath is opgegeven, wordt een bestand met het script uit item 4 gemaakt.