This blog post has also been published in English.
Wer sagt eigentlich, dass man mittels Metadaten nur Datenquellen beschreiben kann? In diesem Beispiel gehen wir von folgendem Szenario aus: Es gibt in 3 Regionen (USA, Asien, Europa) jeweils ein ähnliches Systemsetup mit einer gleichartigen Quelldatenbank und dem Ziel jeweils identische Staging-Umgebungen zu betreiben. Die Quelldatenbanken können sich in diesem Szenario zwischen den Standorten z.B. durch unterschiedliche Collations leicht unterscheiden. Eine Möglichkeit wäre das Arbeiten mit entsprechenden Verbindungs-Konfigurationen. Wir erstellen aber mittels Metadaten einfach entsprechende Pakete je Region.
In unserem Fall liegen alle Umgebungen in der gleichen Datenbankinstanz – falls Ihnen verschiedene Instanzen zur Verfügung stehen, so können Sie dies gerne bei den Metadaten berücksichtigen.
Zur Vorbereitung legen Sie bitte die folgenden 4 Datenbanken an:
MyBiml_MultiTarget – Diese Datenbank enthält unsere Metadaten
MyBiml_MultiTarget_USA
MyBiml_MultiTarget_ASIA
MyBiml_MultiTarget_EUROPE
Dann führen Sie zur Vorbereitung bitte die folgenden Scripte aus:
USE [MyBiml_MultiTarget] GO CREATE TABLE [dbo].[MyBimlMeta_Connections]( [TargetName] [nvarchar](50) NULL, [ConnectionName] [nvarchar](50) NULL, [ConnectionServer] [nvarchar](50) NULL, [ConnectionDatabase] [nvarchar](50) NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[MyBimlMeta_Tables]( [ConnectionName] [nvarchar](50) NULL, [Tablename] [nvarchar](50) NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[MyBimlMeta_Targets]( [TargetName] [nvarchar](50) NULL, [TargetServer] [nvarchar](50) NULL, [TargetDatabase] [nvarchar](50) NULL, [IsActive] [bit] NULL ) ON [PRIMARY] GO CREATE VIEW [dbo].[vMyBimlMeta_Connections] AS SELECT targets.[TargetName] , [ConnectionName]+'_'+targets.Targetname AS [ConnectionName] , [ConnectionName] AS BaseConnection , [ConnectionServer] , [ConnectionDatabase] , 'Source' AS Type FROM [MyBimlMeta_Connections] AS Conn INNER JOIN [dbo].[MyBimlMeta_Targets] AS Targets ON targets.[TargetName] = conn.[TargetName] WHERE ISNULL(isactive, 0) = 1 UNION ALL SELECT TargetName , 'Target_'+targetname AS Connectionname , targetname AS BaseConnection , TargetServer , TargetDatabase , 'Target' AS Type FROM dbo.MyBimlMeta_Targets WHERE ISNULL(isactive, 0) = 1 GO CREATE VIEW [dbo].[vMyBimlMeta_Tables] AS SELECT Conn.ConnectionName , TableName FROM dbo.vMyBimlMeta_Connections AS Conn INNER JOIN dbo.MyBimlMeta_Tables AS tbl ON Conn.BaseConnection = tbl.ConnectionName GO INSERT [dbo].[MyBimlMeta_Connections] ([TargetName], [ConnectionName], [ConnectionServer], [ConnectionDatabase]) VALUES (N'ASIA', N'AW', N'localhost', N'AdventureWorks2014') GO INSERT [dbo].[MyBimlMeta_Connections] ([TargetName], [ConnectionName], [ConnectionServer], [ConnectionDatabase]) VALUES (N'USA', N'AW', N'localhost', N'AdventureWorks2014') GO INSERT [dbo].[MyBimlMeta_Connections] ([TargetName], [ConnectionName], [ConnectionServer], [ConnectionDatabase]) VALUES (N'EUROPE', N'AW', N'localhost', N'AdventureWorks2014') GO INSERT [dbo].[MyBimlMeta_Tables] ([ConnectionName], [Tablename]) VALUES (N'AW', N'Person') GO INSERT [dbo].[MyBimlMeta_Tables] ([ConnectionName], [Tablename]) VALUES (N'AW', N'PersonPhone') GO INSERT [dbo].[MyBimlMeta_Targets] ([TargetName], [TargetServer], [TargetDatabase], [IsActive]) VALUES (N'USA', N'localhost', N'MyBiml_MultiTarget_USA', 1) GO INSERT [dbo].[MyBimlMeta_Targets] ([TargetName], [TargetServer], [TargetDatabase], [IsActive]) VALUES (N'ASIA', N'localhost', N'MyBiml_MultiTarget_ASIA', 1) GO INSERT [dbo].[MyBimlMeta_Targets] ([TargetName], [TargetServer], [TargetDatabase], [IsActive]) VALUES (N'EUROPE', N'localhost', N'MyBiml_MultiTarget_EUROPE', 1) GO
Zusätzlich legen Sie bitte in einer neuen Biml Solution ein neues Codefile mit dem nachfolgenden Inhalt an. Da diese Datei inhaltlich im Vergleich zu den vorhergehenden Blog-Beiträgen keine Neuerungen birgt, gehen wir auf den Inhalt hier nicht näher ein.
Imports Varigence.Biml.CoreLowerer.SchemaManagement Imports Varigence.Biml.Extensions Imports Varigence.Languages.Biml Imports Varigence.Languages.Biml.Connection Imports System.Data Imports System.Collections.Generic Public Class BF Public Shared ControlDB As String = "MyBiml_MultiTarget" Public Shared ControlServer As String = "Localhost" Public Shared Function GetConnectionString (Server As String, DB As String) As String Return "Provider=SQLNCLI11;Server=" & server & ";Initial Catalog=" & DB & ";Integrated Security=SSPI;" End Function Public Shared Function GetNonEmptyList (Conn As AstDbConnectionNode, SQL As String) As List(Of String) Dim tmplist As New List(Of String) If SQL.Contains(" ") = 0 Then sql = "select * from " + sql Dim DT As DataTable = ExternalDataAccess.GetDataTable(Conn.ConnectionString, SQL) For Each dr As datarow In dt.rows tmplist.add(dr.item(0).ToString()) Next If tmplist.count = 0 Then tmplist.add ("NONEMPTYFILLER") Return tmplist End Function Public Shared Function DefaultImportOptions Return ImportOptions.ExcludeIdentity Or ImportOptions.ExcludePrimaryKey Or ImportOptions.ExcludeUniqueKey Or ImportOptions.ExcludeColumnDefault _ Or ImportOptions.ExcludeIndex Or ImportOptions.ExcludeCheckConstraint Or ImportOptions.ExcludeForeignKey End Function End Class
Basierend auf unseren Metadaten legen wir uns nun mittels einem Loop die Verbindungen in Biml an.
Hierbei geben wir den Verbindungen, neben den ohnehin erforderlichen Attributen Name und ConnectionString, mittels Annotations noch einige Zusatzinformationen mit. Über die Information „Type“ können wir später beispielsweise einfach zwischen Quell- und Zieldatenbanken unterscheiden.
<#@ template language="VB" tier="1" #> <#@ code file="../code/BimlFunctions.vb" #> <#@ import namespace="System.Data" #> <#@ import namespace="BF" #> <Biml xmlns="http://schemas.varigence.com/biml.xsd"> <Connections> <OleDbConnection Name="Control" ConnectionString="<#= GetConnectionString(ControlServer, ControlDB) #>"/> <# For Each dr As datarow In ExternalDataAccess.GetDataTable( GetConnectionString(ControlServer, ControlDB), "Select * from vMyBimlMeta_Connections").rows #> <OleDbConnection Name="<#=dr("ConnectionName") #>" ConnectionString="<#= GetConnectionString(dr("ConnectionServer"),dr("ConnectionDatabase")) #>"> <Annotations> <Annotation AnnotationType="Tag" Tag="Type"><#=dr("Type") #></Annotation> <Annotation AnnotationType="Tag" Tag="BaseConnection"><#=dr("BaseConnection") #></Annotation> <Annotation AnnotationType="Tag" Tag="DatabaseName"><#=dr("ConnectionDatabase") #></Annotation> <Annotation AnnotationType="Tag" Tag="TargetName"><#=dr("TargetName") #></Annotation> </Annotations> </OleDbConnection> <# Next #> </Connections> </Biml>
Im nächsten Schritt legen wir für jede Zieldatenbank (Type = Target) einen Database- und darauf basierend wiederum einen Schema-Eintrag in Biml an:
<#@ template language="VB" tier="2" #> <Biml xmlns="http://schemas.varigence.com/biml.xsd"> <Databases> <# for each target as AstConnectionBaseNode in rootnode.connections.where(function(c) c.GetTag("Type") = "Target") #> <Database Name="<#= target.GetTag("DatabaseName") #>" ConnectionName="<#= target.name #>"> <Annotations> <Annotation AnnotationType="Tag" Tag="TargetName"><#= target.getTag("TargetName") #></Annotation> </Annotations> </Database> <# next #> </Databases> </Biml>
<#@ template language="VB" tier="3" #> <Biml xmlns="http://schemas.varigence.com/biml.xsd"> <Schemas> <# for each db as AstDatabasenode in rootnode.databases#> <Schema Name="dbo" DatabaseName="<#=db.name #>"> <Annotations> <Annotation AnnotationType="Tag" Tag="TargetName"><#= db.getTag("TargetName") #></Annotation> </Annotations> </Schema> <# next #> </Schemas> </Biml>
Jetzt, da wir Verbindungen, Datenbanken und Schemata definiert haben, benötigen wir noch die entsprechenden Metadaten für unsere Tabellen.
Hierfür nutzen wir den bekannten Weg eines Loops über die Quelldatenbanken, ermitteln (aus der View vMyBimlMeta_Tables) die für diese Datenbank bzw. diesen System-Typ definierten Tabellen und erzeugen auch hierfür jeweils einen Eintrag mit Verweis auf das entsprechende Ziel-Schema. Hierdurch stellt es auch kein Problem dar, dass Tabellen teilweise auf den ersten Blick doppelt erscheinen. Auch hier nutzen wir einige Annotations zur Weitergabe von zusätzlichen Informationen.
<#@ template language="VB" tier="4" #> <#@ code file="../code/BimlFunctions.vb" #> <#@ import namespace="BF" #> <Biml xmlns="http://schemas.varigence.com/biml.xsd"> <Tables> <# for each SrcDB as AstConnectionBaseNode in rootnode.connections.where(function(c) c.GetTag("Type") = "Source") for each Tbl as asttablenode in SrcDB.GetDatabaseSchema(nothing,getnonemptylist(rootnode.connections("Control"),"Select Tablename from [vMyBimlMeta_Tables] where ConnectionName = '" & srcdb.name & "'"),defaultimportoptions).tablenodes #> <Table Name="<#= srcDb.GetTag("BaseConnection") #>_<#= tbl.schema.name #>_<#= tbl.name #>" SchemaName="<#= rootnode.schemas.where(function(c) c.GetTag("TargetName") = srcDb.GetTag("TargetName") ).First.scopedname #>"> <Columns> <#= tbl.columns.getbiml #> </Columns> <Annotations> <Annotation AnnotationType="Tag" Tag="TableName"><#= tbl.scopedname #></Annotation> <Annotation AnnotationType="Tag" Tag="TargetName"><#=SrcDB.GetTag("TargetName") #></Annotation> <Annotation AnnotationType="Tag" Tag="SrcName"><#=SrcDB.Name #></Annotation> </Annotations> </Table> <# next next #> </Tables> </Biml>
Da nun alle Metadaten vorliegen können wir für jedes Zielsystem ein entsprechendes Paket erstellen, welches wiederum für alle Tabellen dieses Systems entsprechende CREATE Statements zur Erzeugung der Zieltabellen generiert:
<#@ template language="VB" tier="5" #> <Biml xmlns="http://schemas.varigence.com/biml.xsd"> <Packages> <# for each target as AstConnectionBaseNode in rootnode.connections.where(function(c) c.GetTag("Type") = "Target") #> <Package Name="Create_Staging_<#=target.gettag("TargetName") #>" PackageSubpath="<#=target.gettag("TargetName") #>"> <Tasks> <# for each tbl as AstTableNode in rootnode.tables.where(function(c) c.GetTag("TargetName").Trim = target.gettag("TargetName").trim) #> <ExecuteSQL Name="Create <#=tbl.Name#>" ConnectionName="<#= target.name #>"> <DirectInput> <#=tbl.GetDropAndCreateDdl()#> </DirectInput> </ExecuteSQL> <# next #> </Tasks> </Package> <# next #> </Packages> </Biml>
Der letzte Schritt ist dann lediglich noch eine Datei, welche wiederum für jedes Zielsystem ein Paket zur Befüllung der Tabellen erzeugt:
<#@ template language="VB" tier="6" #> <Biml xmlns="http://schemas.varigence.com/biml.xsd"> <Packages> <# for each target as AstConnectionBaseNode in rootnode.connections.where(function(c) c.GetTag("Type") = "Target") #> <Package Name="Populate_Staging_<#=target.gettag("TargetName") #>" PackageSubpath="<#=target.gettag("TargetName") #>"> <Tasks> <# for each tbl as AstTableNode in rootnode.tables.where(function(c) c.GetTag("TargetName").Trim = target.gettag("TargetName").trim) #> <Container Name="Transfer <#=tbl.Name#>" ConstraintMode="Linear"> <Tasks> <ExecuteSQL Name="Truncate" ConnectionName="<#= target #>"> <DirectInput>truncate table <#=tbl.ScopedName#></DirectInput> </ExecuteSQL> <Dataflow Name="Copy <#=tbl.Name#>"> <Transformations> <OleDbSource Name="Get Rows" ConnectionName="<#= tbl.GetTag("SrcName").trim #>"> <DirectInput>SELECT <#= tbl.GetColumnList #> from <#= tbl.GetTag("TableName") #></DirectInput> </OleDbSource> <OleDbDestination Name="Set Rows" ConnectionName="<#= target #>"> <TableOutput TableName="<#=tbl.ScopedName#>" /> </OleDbDestination> </Transformations> </Dataflow> </Tasks> </Container> <# next #> </Tasks> </Package> <# next #> </Packages> </Biml>
Somit haben wir eine Lösung, in der wir nicht nur mittels Metadaten steuern können, welche Daten wir abholen sondern auch, wohin diese geladen werden sollen. Dieser Ansatz funktioniert so natürlich nicht nur für regional unterschiedliche Systeme sondern könnte auch als Grundlage dienen, um Änderungen in ein TEST und ein PROD System auszurollen.
Haben Sie hierzu Fragen oder Anmerkungen? Wir freuen uns auf Ihren Input unter !
Weitere Informationen zu Biml, einschließlich Terminen und Blog Beiträgen finden Sie auch auf unserer Biml Seite.
Viel Spaß beim Biml’n!