Zur Zeit wird gefiltert nach: sql server
Filter zurücksetzen
Parallelisierung wenn eine relationale Datenbank im Spiel ist
Nachdem ich mir das erste Mal eine CLR-Funktion für eine Datenmigration gebaut habe, geht es nun daran Codeteile zu parallelisieren, damit die Migrationsdauer verkürzt werden kann. Ein paar Herausforderungen dabei:
- Ich habe die Auflage das Ganze in .NET 3.5 zu entwickeln, sodass ich die TPL nicht nutzen kann.
- Direkte Insert, Update und Delete-Anweisungen dürfen nicht ausgeführt werden. Es muss die Sharepoint Content Deployment API verwendet werden.
Im Blogpost fand ich einen interessanten Ansatz, wie in .NET 3.5 eine parallele Schleife realisiert werden kann.
Also den ersten Test erstellt. Eine Stub-Methode die 10 Sekunden wartet, geschrieben und 5-mal aufgerufen. Testkriterium: Ausführungsdauer weniger als 15 Sekunden bestehen, funktioniert.
Anschliessend den integrativen Test in Verbindung mit einer Datenbank. Testkriterium: Auditinformationen in Datenbank schreiben nicht erfüllt, tatsächlich:
Transaction (Process ID XX) was deadlocked on lock | communication buffer resources with another process and has been chosen as the deadlock victim. Rerun the transaction. at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, …
Ein Codestück, das die Verbindung zur Datenbank herstellt, hat folgenden Aufbau:
public static void ExecuteNonQuery(SqlCommand cmd)
{
try
{
using (var conn = GetSqlConnection())
{
cmd.Connection = conn;
cmd.ExecuteNonQuery();
}
}
catch (Exception ex)
{
log.Error(ex.Message, ex);
throw;
}
}
Als ersten Ansatz stellte ich dieses Codestück um und machte einen asynchronen Aufruf daraus.
Es dauerte unwesentlich länger, aber auch dieser Code erzeugte Deadlocks auf der Datenbank. In diesem Moment kam ich zum Schluss: So, genug gebastelt jetzt wird analysiert wer bzw. was diese Deadlocks auf der Datenbank auslösen. Wenn ich mit dem SQL Server arbeite, nutze ich üblicherweise den SQL Server Profiler für diese Zwecke.
Dieser kann über das SQL Server Management Studio (SSMS) unter dem Menüpunkt Tools gestartet werden. Nach der Verbindung zur Datenbank kann auf der ersten Dialogmaske das Template TSQL_Locks ausgewählt werden.
Unter dem zweiten Tab können weitere Einstellungen vorgenommen werden, darauf gehe ich nicht näher ein. Mit klick auf die Schaltfläche "Run" wird die Ablaufverfolgung gestartet.
Nach dem Start des Integrationstests wurde alles aufgezeichnet und es dauerte nicht lange, bis die ersten Meldungen im Tracelog erschienen.
Nun stellt sich die Frage, welcher Befehl ausgeführt wurde. Dies lässt sich über die Object Id ermitteln. Im Trace steht die Object ID 693577509. Wer sich dahinter versteckt, lässt sich mit folgendem SQL-Befehl ermitteln:
SELECT
name
, type
, type_desc
FROM sys.objects
WHERE object_id IN (693577509)
Wer das obige Codebeispiel nochmals betrachtet sieht, dass die Methode statisch ist. Hier ging ich von der Annahme aus, dass dieser Ansatz "Thread Safety" ist. Um das Problem zu beheben, habe ich die statische Methode wie folgt angepasst:
static readonly object locker = new object();
public static void ExecuteNonQuery(SqlCommand cmd)
{
try
{
lock (locker)
{
using (var conn = GetSqlConnection())
{
cmd.Connection = conn;
cmd.ExecuteNonQuery();
}
}
}
catch (Exception ex)
{
log.Error(ex.Message, ex);
throw;
}
}
Nach dieser Anpassung waren die Deadlocks verschwunden. Ganz parallel arbeitet dieser Code natürlich nicht mehr, es wird sich also nicht der komplette Code parallelisieren lassen.
Noch eine Anmerkung zum Schluss: Vergesst vor lauter Unit-Testing mit Mocks und Fakes die Integrationstests mit zufälligen und korrupten Daten nicht. ;-)
Weitere Informationen zum Thema:
SQL Server - Indexfragmentierung feststellen
Bei sehr vielen Einfügeoperationen kann ein Index sehr schnell seine Wirkung verlieren und muss entweder reorganisiert oder neu erstellt werden. Da ich die Abfrage des öfteren brauche, lege ich diese mal hier ab:
SELECT
DB_NAME(DPS.DATABASE_ID) AS [DatabaseName]
, OBJECT_NAME(DPS.OBJECT_ID) AS TableName
, SI.NAME AS IndexName
, DPS.INDEX_TYPE_DESC AS IndexType
, DPS.AVG_FRAGMENTATION_IN_PERCENT AS AvgPageFragmentation
, DPS.PAGE_COUNT AS PageCounts
FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, NULL) DPS --N'LIMITED') DPS
INNER JOIN sysindexes SI
ON DPS.OBJECT_ID = SI.ID
AND DPS.INDEX_ID = SI.INDID
ORDER BY AVG_FRAGMENTATION_IN_PERCENT DESC
In der Regel sollen alle Indexe mit einer Framentierung grösser 30% neu erstellt werden, darunter reicht häufig ein reorganisieren aus. Mit folgenden Befehlen ist dies möglich:
-- Index neu erstellen
EXEC sp_MSforeachtable @command1="print 'Erstelle Index für ?' ALTER INDEX ALL ON ? REBUILD WITH (FILLFACTOR = 90)"
GO
-- Index reorganisieren
EXEC sp_MSforeachtable @command1="print 'Reorgansiere Index für ?' ALTER INDEX ALL ON ? REORGANIZE"
GO
CLR Funktion für den SQL Server erstellen
Lange habe ich auf die Gelegenheit und einen Anwendungsfall gewartet, um eine CLR Funktion für den SQL Server schreiben zu können. Für ein Migrationsprojekt habe ich nun einen Einsatzzweck gefunden. ;-)
Hauptgrund dafür ist, dass eine Anwendung abgelöst werden soll, die ihre Business Logik in C# implementiert hat. Der Code ist nach den gängigen Clean Code Regeln erstellt, das ist sofort ersichtlich, die Datenhaltung ist dafür sehr kreativ.
Da der imperative Code die Logik aus den Daten zusammenstellt, habe ich mich für den Ansatz entschieden, eine CLR Funktion für den SQL Server zu schreiben. Würde ich diese Logik in T-SQL abbilden, wäre die Performanz und Verständlichkeit grausam. Da eine grosse Datenmenge migriert werden muss, ist ein Ansatz notwendig, der einerseits performant läuft, anderseits wenig Aufwand verursacht.
So habe ich nun die Möglichkeit eine SQL Server Eigenschaft zu nutzen, die seit der Version 2005 existiert.
Gehen wir nun der Reihe nach vor. Zuerst wird ein Projekt vom Typ Klassenbibliothek angelegt. Anschliessend lege ich einen Schlüssel an, damit die Assembly auch signiert werden kann. Die Implementierung ist recht einfach. Es werden die Referenzen:
- System.Data.Sql
- System.Data.SqlTypes
- Microsoft.SqlServer.Server
benötigt. Das Grundgerüst der Klasse hat folgenden Aufbau:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Sql;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.Server;
namespace Firma.Projekt.TVFStringConvert
{
public class StringConvertTable
{
[SqlFunction(FillRowMethodName = "FillRow")]
public static IEnumerable Inititalize(string tags)
{
return ParseString(tags);
}
public static void FillRow(Object obj, out SqlInt32 keyId, out SqlInt32 valueId)
{
if (obj == null)
SqlContext.Pipe.Send("The obj to create a tupel is null.");
Tag tag = obj as Tag;
keyId = tag.ParentId;
valueId = tag.Id;
}
public static ICollection<Tag> ParseString(string tagString)
{
if (tagString != null)
{
return ExternalLib.Convert(tagString);
}
return new List<Tag>(0);
}
}
}
Einstiegspunkt ist in meinem Beispiel die Methode Initialize. Diese wird mit dem Attribute SqlFunction versehen. Mit diesem wird festgelegt, dass die Methode FillRow ein Tupel für die Verwendung in T-SQL aufbereitet. Der erste Parameter enthält die Informationen dafür. Die eigentliche Logik beziehe ich dabei aus einer anderen Bibliothek und ist nicht Bestandteil des Codebeispiels.
Nachdem diese CLR Funktion erstellt ist, muss diese auf dem SQL-Server registriert werden. Dies erfolgt in mehreren Schritten. Zuerst wird die Assembly registriert und anschliessend eine Funktion erstellt, welche auf die Logik in der Assembly zeigt. Nachfolgendes Beispiel zeigt den Ablauf:
USE TestDatabase
GO
-- Für Permission Set External_Access und Unsafe
-- Bad Practice, besser die DLL mit einem richtigen Zertifikat signieren
-- ALTER DATABASE TestDatabase SET TRUSTWORTHY ON
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'StringConvertTable')
DROP FUNCTION StringConvertTable
GO
IF EXISTS (SELECT name FROM sys.assemblies WHERE name = 'TVFStringConvert')
DROP ASSEMBLY [TVFStringConvert]
GO
CREATE ASSEMBLY [TVFStringConvert]
FROM 'C:\projects\ Firma.Projekt.TVFStringConvert\Firma.Projekt.TVFStringConvert.dll'
WITH PERMISSION_SET = UNSAFE --SAFE
GO
CREATE FUNCTION StringConvertTable(@tags nvarchar(max))
RETURNS TABLE (
keyId int,
valueId int
)
AS EXTERNAL NAME TVFStringConvert.[Firma.Projekt.TVFStringConvert.StringConvertTable].[Inititalize]
GO
-- Test (Weiterverwendung mit CROSS APPLY)
SELECT * FROM StringConvertTable('kreative Datenlogik')
GO
Falls die Fehlermeldung
Execution of user code in the .NET Framework is disabled. Enable "clr enabled" configuration option.
erscheint, muss diese Option aktiviert werden.
Mit folgendem Befehl ist dies möglich:
-- Servereinstellungen
EXEC sp_configure 'clr enabled', 1
GO
RECONFIGURE
GO
Ich habe nun einen praktikablen und pragmatischen Weg, der auf die Situation abgestimmt mit akzeptabler Performanz die Daten (kreative Datenlogik) in auswertbarer Form für die Migration bereitstellt.
Mein nächster Schritt besteht darin das Ganze mit parallelisierter Ausführung weiter zu beschleunigen.
Weitere Informationen zum Thema:
Entity Framework 4.1 - Code-/Model First und die Unterschiede einer Gemeinsamkeit
Code- bzw. Model First mit dem Entity Framework sind 2 unterschiedliche Ansatzmodelle. Bei Model First wird in der Regel mit dem Mapping begonnen, die Vorgehensweise dazu ist auch als Middle out bekannt. Anhand des Mappings wird anschliessend der Code und das DDL-Skript für die Datenbank erstellt. Bei Code First wird zuerst der Code geschrieben und daraus das Mapping und das DDL-Skript für die Datenbank erstellt. Dieser Ansatz ist auch häufig als Top down geläufig.
Die Gemeinsamkeit beider Vorgehensweisen ist die Erstellung der Datenbank. Der Unterschied liegt jedoch darin, dass bei der modellzentrierten Anwendungsentwicklung mit Hilfe von T4 Einfluss auf das DDL-Skript genommen werden kann, während bei Code First die Datenbank erstellt wird.
Wo liegt also nun der Unterschied in dieser Gemeinsamkeit, ausser bei T4? Dazu muss man sich nur das von der T4 erstellte SQL-Skript betrachten:
-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
-- Creating foreign key on [OrderID] in table 'OrderItems'
ALTER TABLE [dbo].[OrderItems]
ADD CONSTRAINT [FK_OrderOrderItem]
FOREIGN KEY ([OrderID])
REFERENCES [dbo].[Orders]
([ID])
ON DELETE NO ACTION ON UPDATE NO ACTION;
-- Creating non-clustered index for FOREIGN KEY 'FK_OrderOrderItem'
CREATE INDEX [IX_FK_OrderOrderItem]
ON [dbo].[OrderItems]
([OrderID]);
GO
Im Bereich der Fremdschlüssel werden im Gegensatz zum Code First - Ansatz die FK-Indizes angelegt. Darin liegt der Unterschied. Bei wenigen Daten wird das nicht besonders schmerzhaft sein, wenn jedoch das Datenvolumen wächst und der Datenbankserver zudem noch virtualisiert ist, können da an der einen oder anderen Ecke Performance-Probleme entstehen, natürlich schleichend. Betrachten wir es anhand eines Beispiels mit wenigen Daten. In meinem Testszenario existieren ein paar Tausend Datensätze in der Orders-Tabelle aus der 10 Datensätze selektiert werden sollen.
Die SQL-Abfrage hat folgenden Aufbau:
SELECT
*
FROM dbo.Orders o
INNER JOIN dbo.OrderItems oi ON o.Id = oi.OrderId
WHERE o.id IN (1,5,10,34,80,92,500,897,3456,4567)
Wenn man die Kosten in Relation betrachtet, so benötigt die Datenbank ohne Fremdschlüssel mehr Ressourcen.
Nun gibt es mehrere Möglichkeiten die Indizes zu erstellen. Ich bevorzuge die Art der Metaprogrammierung. Sprich der SQL-Server genauer gesagt ein SQL-Skript soll mithilfe der Angaben die notwendigen Inidizes selbst erstellen. Dafür habe ich folgendes Skript geschrieben:
DECLARE @table nvarchar(100),
@fkName nvarchar(100),
@columnName nvarchar(100),
@index nvarchar(800)
DECLARE missing_FkIndex_cursor CURSOR FOR SELECT
t.name as TB_Name
, fk.name as FK_Name
, c.name as Column_Name
FROM sys.foreign_key_columns fkc
INNER JOIN sys.tables t ON t.object_id = fkc.parent_object_id
INNER JOIN sys.foreign_keys fk ON fk.parent_object_id =
fkc.parent_object_id
INNER JOIN sys.columns c ON c.object_id = fk.parent_object_id AND
c.column_id = fkc.parent_column_id
LEFT OUTER JOIN sys.index_columns indc ON indc.object_id = c.object_id AND
c.column_id = indc.column_id
LEFT OUTER JOIN sys.indexes ind ON ind.object_id = indc.object_id AND
ind.index_id = indc.index_id
WHERE fk.type = 'F' AND ind.name is null
OPEN missing_FkIndex_cursor;
FETCH NEXT FROM missing_FkIndex_cursor
INTO @table, @fkName, @columnName
WHILE @@FETCH_STATUS = 0
BEGIN
SET @index = 'CREATE INDEX IX_FK_' + @fkName + '_' + @columnName + ' ON ' + @table + ' (' + @columnName + ')'
EXECUTE sp_executesql @index
PRINT @index
FETCH NEXT FROM missing_FkIndex_cursor
INTO @table, @fkName, @columnName
END
CLOSE missing_FkIndex_cursor;
DEALLOCATE missing_FkIndex_cursor;
Dieses Skript erstellt den Fremschlüsselindex nur, wenn dieser noch nicht existiert. Nun liegt es in der Natur des Menschen, dass solche Anpassungen, gerade wenn man die gewohnte Umgebung verlassen muss, vergessen werden.
Für die Code First Variante wäre es nun möglich, dieses Skript in einen Initializer zu packen, der bei der Erstellung der Datenbank ausgeführt wird. Der Vorteil dieser Variante, der Initalizer kann in eine Library verstaut und zur Erstellung der Datenbank genutzt werden.
Der Initializer könnte folgenden Aufbau haben:
public class CreateSqlServerDatabaseWithFkIndizesIfNotExists<T>
: CreateDatabaseIfNotExists<T> where T : DbContext
{
protected override void Seed(T context)
{
context.Database.ExecuteSqlCommand(GetFkIndexSqlScript());
base.Seed(context);
}
private string GetFkIndexSqlScript()
{
return @"
DECLARE @table nvarchar(100),
@fkName nvarchar(100),
@columnName nvarchar(100),
@index nvarchar(800)
DECLARE missing_FkIndex_cursor CURSOR FOR SELECT
t.name as TB_Name,
fk.name as FK_Name
, c.name as Column_Name
--, ind.name as Index_Name
--, fk.type_desc as FK_Type
FROM sys.foreign_key_columns fkc
INNER JOIN sys.tables t ON t.object_id = fkc.parent_object_id
INNER JOIN sys.foreign_keys fk ON fk.parent_object_id =
fkc.parent_object_id
INNER JOIN sys.columns c ON c.object_id = fk.parent_object_id AND
c.column_id = fkc.parent_column_id
LEFT OUTER JOIN sys.index_columns indc ON indc.object_id = c.object_id AND
c.column_id = indc.column_id
LEFT OUTER JOIN sys.indexes ind ON ind.object_id = indc.object_id AND
ind.index_id = indc.index_id
WHERE fk.type = 'F' AND ind.name is null
OPEN missing_FkIndex_cursor;
FETCH NEXT FROM missing_FkIndex_cursor
INTO @table, @fkName, @columnName
WHILE @@FETCH_STATUS = 0
BEGIN
SET @index = 'CREATE INDEX IX_FK_' + @fkName + '_' + @columnName + ' ON ' + @table + ' (' + @columnName + ')'
EXECUTE sp_executesql @index
PRINT @index
FETCH NEXT FROM missing_FkIndex_cursor
INTO @table, @fkName, @columnName
END
CLOSE missing_FkIndex_cursor;
DEALLOCATE missing_FkIndex_cursor; ";
}
}
Wie die Datenbank mit dem Skript initialisiert werden kann, zeigt folgender Code:
var initializer = new CreateSqlServerDatabaseWithFkIndizesIfNotExists<TestContext>();
Database.SetInitializer<TestContext>
(initializer);
initializer.InitializeDatabase(new TestContext());
Dieser Ansatz ist etwas tricky, aber es ist aktuell der einzige lauffähige Code. Muss es mal mit dem Update 1 von EF 4.1 nochmals durchtesten.
Wenn nun die Datenbank erstellt wird, läuft das o.a. Skript und der Unterschied einer Gemeinsamkeit ist verschwunden. ;-)
Entity Framwork 4 – Model First und die Dokumentation mit sp_addextendedproperty auf dem SQL Server
Letzte Woche bekam ich eine interessante Frage gestellt: Ist es möglich die Dokumentation aus dem Entity Data Model auf dem SQL-Server mithilfe der Extended Property - Prozeduren zu übertragen.
Interessant an der Frage fand ich, dass es wirklich Entwickler gibt, die sich Gedanken darüber machen pragmatisch zu dokumentieren und nicht mit dem Argument: "Guter Code braucht keine Dokumentation" um sich schlagen.
Bei der DB First-Variante können die Huagati Tools mit den MS_Descriptions auf dem SQL Server umgehen und diese in das Entity Data Model einarbeiten. Beim Model First-Ansatz wird jedoch nur ein DDL-Skript erstellt, dass die Dokumentation nicht auf das Datenmodell überträgt. Es ist aber möglich dies mithilfe der Prozeduren durch eine angepasste T4-Vorlage zu generieren.
Dazu muss man sich eine eigene DDL-Vorlage anlegen. Die Vorgehensweise dazu habe ich bereits hier beschrieben. Als Vorlage dient die bereits vorhandene DDL T4-Vorlage, die entsprechend angepasst und erweitert werden muss.
Im Standard sind die Funktionen für das Store-Schema vorhanden, aus denen das SQL für die Datenbank erstellt wird. Es stehen also nicht die Entities aus dem konzeptionellen Modell zur Verfügung. Das Problem dabei ist, die Dokumentation befindet sich im konzeptionellen Modell.
Aus diesem Grund muss die Include-Datei EF.Utility.CS.ttinclude und der entsprechende Initialcode hinzugefügt werden. Damit wird ein Mapping von den EntitySets auf die Entities möglich und der Zugriff auf die Dokumentation ist gewährleistet.
// required for documentation
MetadataLoader loader = new MetadataLoader(this);
MetadataTools ef = new MetadataTools(this);
var ItemCollection = loader.CreateEdmItemCollection(edmxPath, new string[] {});
EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
// ----
Für die Dokumentation muss in der T4-Vorlage der notwendige Prozedur-Code mit den Kommentaren hinzugefügt werden, der durch das Mapping über die EntitySets erreichbar wird.
this.WriteLine("-- EDM Dokumentation");
// EDM-Dokumentation der Entity auf SQL-Server übertragen
// get entity from entity set
string entityType = (from c in container.BaseEntitySets
where c.Name == entitySet.Name
select c.ElementType.Name).FirstOrDefault();
EntityType entity = (from e in ItemCollection.GetItems<EntityType>()
where e.Name == entityType
select e).FirstOrDefault();
string tableDoku = String.Empty;
if (entity.Documentation != null &&
entity.Documentation.Summary != null)
{
tableDoku = entity.Documentation.Summary;
}
string propertyProcedure = @"
exec sp_addextendedproperty N'MS_Description', '{0}', N'user', N'dbo',
N'table', N'{1}'
GO";
if (String.IsNullOrEmpty(tableDoku) == false)
this.WriteLine(propertyProcedure, tableDoku, tableName);
foreach (EdmProperty item in entity.Properties)
{
if (item.Documentation != null && item.Documentation.Summary != null)
{
string columnDoku = item.Documentation.Summary;
string columnPropertyProcedure = @"exec sp_addextendedproperty N'MS_Description', '{0}', N'user',
N'dbo', N'table', N'{1}', N'column', N'{2}'
GO";
this.WriteLine(columnPropertyProcedure, columnDoku, tableName, item.Name);
}
}
this.WriteLine("-- End documentation");
Zusammengefasst hat die Vorlage folgenden Inhalt:
<#
//---------------------------------------------------------------------
// <copyright file="SsdlToSql10.tt" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//---------------------------------------------------------------------
// This T4 template generates T-SQL from an instance of
// System.Data.Metadata.Edm.StoreItemCollection, an object representation
// of the SSDL. This T-SQL is compatible with SQL 2008, 2005, CE, and Azure databases.
//---------------------------------------------------------------------
// Note: We will resolve all paths in assembly directives at runtime, taking
// macros into account (e.g. $(DevEnvDir), $(ProjectDir), etc.)
#>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="System.Data.Entity.Design" #>
<#@ assembly name="$(DevEnvDir)Microsoft.Data.Entity.Design.DatabaseGeneration.dll"#>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.Entity.Design" #>
<#@ import namespace="System.Data.Metadata.Edm" #>
<#@ import namespace="Microsoft.Data.Entity.Design.DatabaseGeneration" #>
<#@ import namespace="System.Runtime.Remoting.Messaging" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ template language="C#" debug="true" hostspecific="true" #>
<#@ include file="GenerateTSQL.Utility.ttinclude"#>
<#@ include file="EF.Utility.CS.ttinclude"#> -- Dokumentation
<#@ output extension = ".sql" #>
<#
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Setup for the template (initializing variables, etc.)
// +++++++++++++++++++++++++++++++++++++++++++++++++
string databaseName = this.GetInput<string>(EdmParameterBag.ParameterName.DatabaseName.ToString());
string edmxPath = this.GetInput<string>(EdmParameterBag.ParameterName.EdmxPath.ToString());
Version targetVersion = this.GetInput<Version>(EdmParameterBag.ParameterName.TargetVersion.ToString());
// required for documentation
MetadataLoader loader = new MetadataLoader(this);
MetadataTools ef = new MetadataTools(this);
var ItemCollection = loader.CreateEdmItemCollection(edmxPath, new string[] {});
EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
// ----
if (false == InitializeAndValidateExistingStore())
{
#>
-- Warning: There were errors validating the existing SSDL. Drop statements
-- will not be generated.
<#
}
#>
-- --------------------------------------------------
<#
if (this.IsSQLCE) {
#>
-- Entity Designer DDL Script for SQL Server Compact Edition
<#
} else {
#>
-- Entity Designer DDL Script for SQL Server 2005, 2008, and Azure
<#
}
#>
-- --------------------------------------------------
-- Date Created: <#=DateTime.Now#>
<#
if (!String.IsNullOrEmpty(edmxPath))
{
#>
-- Generated from EDMX file: <#=Id(edmxPath)#>
<#
}
#>
-- --------------------------------------------------
<# if (!this.IsSQLCE)
{
#>
SET QUOTED_IDENTIFIER OFF;
GO
<# if (!String.IsNullOrEmpty(databaseName))
{
#>
USE [<#=Id(databaseName)#>];
GO
<#
}
foreach (string unescapedSchemaName in (from es in Store.GetAllEntitySets() select es.GetSchemaName()).Distinct())
{
#>
IF SCHEMA_ID(N'<#=Lit(unescapedSchemaName)#>') IS NULL EXECUTE(N'CREATE SCHEMA [<#=Id(unescapedSchemaName)#>]');
<#
}
#>
GO
<# } #>
-- --------------------------------------------------
-- Dropping existing FOREIGN KEY constraints
<# if (this.IsSQLCE)
{
#>
-- NOTE: if the constraint does not exist, an ignorable error will be reported.
<# } #>
-- --------------------------------------------------
<#
foreach (AssociationSet associationSet in ExistingStore.GetAllAssociationSets())
{
ReferentialConstraint constraint = associationSet.ElementType.ReferentialConstraints.Single();
string constraintName = Id(WriteFKConstraintName(constraint));
AssociationSetEnd dependentSetEnd = associationSet.AssociationSetEnds.Where(ase => ase.CorrespondingAssociationEndMember == constraint.ToRole).Single();
string schemaName = Id(dependentSetEnd.EntitySet.GetSchemaName());
string dependentTableName = Id(dependentSetEnd.EntitySet.GetTableName());
if (!this.IsSQLCE)
{
#>
IF OBJECT_ID(N'[<#=Lit(schemaName)#>].[<#=Lit(constraintName)#>]', 'F') IS NOT NULL
<# } #>
ALTER TABLE <# if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=dependentTableName#>] DROP CONSTRAINT [<#=constraintName#>];
GO
<#
}
#>
-- --------------------------------------------------
-- Dropping existing tables
<# if (this.IsSQLCE)
{
#>
-- NOTE: if the table does not exist, an ignorable error will be reported.
<# } #>
-- --------------------------------------------------
<#
foreach (EntitySet entitySet in ExistingStore.GetAllEntitySets())
{
string schemaName = Id(entitySet.GetSchemaName());
string tableName = Id(entitySet.GetTableName());
if (!this.IsSQLCE)
{
#>
IF OBJECT_ID(N'[<#=Lit(schemaName)#>].[<#=Lit(tableName)#>]', 'U') IS NOT NULL
<# } #>
DROP TABLE <# if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=tableName#>];
GO
<#
}
#>
-- --------------------------------------------------
-- Creating all tables
-- --------------------------------------------------
<#
foreach (EntitySet entitySet in Store.GetAllEntitySets())
{
string schemaName = Id(entitySet.GetSchemaName());
string tableName = Id(entitySet.GetTableName());
#>
-- Creating table '<#=tableName#>'
CREATE TABLE <# if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=tableName#>] (
<#
for (int p = 0; p < entitySet.ElementType.Properties.Count; p++)
{
EdmProperty prop = entitySet.ElementType.Properties[p];
#>
[<#=Id(prop.Name)#>] <#=prop.ToStoreType()#> <#=WriteIdentity(prop, targetVersion)#> <#=WriteNullable(prop.Nullable)#><#=(p < entitySet.ElementType.Properties.Count - 1) ? "," : ""#>
<#
}
#>
);
GO
<#
this.WriteLine("-- EDM Dokumentation");
// EDM-Dokumentation der Entity auf SQL-Server übertragen
// get entity from entity set
string entityType = (from c in container.BaseEntitySets
where c.Name == entitySet.Name
select c.ElementType.Name).FirstOrDefault();
EntityType entity = (from e in ItemCollection.GetItems<EntityType>()
where e.Name == entityType
select e).FirstOrDefault();
string tableDoku = String.Empty;
if (entity.Documentation != null &&
entity.Documentation.Summary != null)
{
tableDoku = entity.Documentation.Summary;
}
string propertyProcedure = @"
exec sp_addextendedproperty N'MS_Description', '{0}', N'user', N'dbo',
N'table', N'{1}'
GO";
if (String.IsNullOrEmpty(tableDoku) == false)
this.WriteLine(propertyProcedure, tableDoku, tableName);
foreach (EdmProperty item in entity.Properties)
{
if (item.Documentation != null && item.Documentation.Summary != null)
{
string columnDoku = item.Documentation.Summary;
string columnPropertyProcedure = @"exec sp_addextendedproperty N'MS_Description', '{0}', N'user',
N'dbo', N'table', N'{1}', N'column', N'{2}'
GO";
this.WriteLine(columnPropertyProcedure, columnDoku, tableName, item.Name);
}
}
this.WriteLine("-- End documentation");
}
#>
-- --------------------------------------------------
-- Creating all PRIMARY KEY constraints
-- --------------------------------------------------
<#
foreach (EntitySet entitySet in Store.GetAllEntitySets())
{
string schemaName = Id(entitySet.GetSchemaName());
string tableName = Id(entitySet.GetTableName());
#>
-- Creating primary key on <#=WriteColumns(entitySet.ElementType.GetKeyProperties(), ',')#> in table '<#=tableName#>'
ALTER TABLE <# if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=tableName#>]
ADD CONSTRAINT [PK_<#=tableName#>]
PRIMARY KEY <# if (!IsSQLCE) {#><#=WriteClustered(Store, entitySet.ElementType)#> <#}#>(<#=WriteColumns(entitySet.ElementType.GetKeyProperties(), ',')#> <# if (!IsSQLCE) {#>ASC<#}#>);
GO
<#
}
#>
-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
<#
foreach (AssociationSet associationSet in Store.GetAllAssociationSets())
{
ReferentialConstraint constraint = associationSet.ElementType.ReferentialConstraints.Single();
AssociationSetEnd dependentSetEnd = associationSet.AssociationSetEnds.Where(ase => ase.CorrespondingAssociationEndMember == constraint.ToRole).Single();
AssociationSetEnd principalSetEnd = associationSet.AssociationSetEnds.Where(ase => ase.CorrespondingAssociationEndMember == constraint.FromRole).Single();
string schemaName = Id(dependentSetEnd.EntitySet.GetSchemaName());
string dependentTableName = Id(dependentSetEnd.EntitySet.GetTableName());
string principalTableName = Id(principalSetEnd.EntitySet.GetTableName());
#>
-- Creating foreign key on <#=WriteColumns(constraint.ToProperties, ',')#> in table '<#=dependentTableName#>'
ALTER TABLE <#if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=dependentTableName#>]
ADD CONSTRAINT [<#=WriteFKConstraintName(constraint)#>]
FOREIGN KEY (<#=WriteColumns(constraint.ToProperties, ',')#>)
REFERENCES <# if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=principalTableName#>]
(<#=WriteColumns(constraint.FromProperties, ',')#>)
ON DELETE <#=GetDeleteAction(constraint)#> ON UPDATE NO ACTION;
<#
// if the foreign keys are part of the primary key on the dependent end, then we should not add a constraint.
if (!dependentSetEnd.EntitySet.ElementType.GetKeyProperties().Take(constraint.ToProperties.Count()).OrderBy(r => r.Name).SequenceEqual(constraint.ToProperties.OrderBy(r => r.Name)))
{
#>
-- Creating non-clustered index for FOREIGN KEY '<#=WriteFKConstraintName(constraint)#>'
CREATE INDEX [IX_<#=WriteFKConstraintName(constraint)#>]
ON <#if (!IsSQLCE) {#>[<#=schemaName#>].<#}#>[<#=dependentTableName#>]
(<#=WriteColumns(constraint.ToProperties, ',')#>);
<#
}
#>
GO
<#
}
#>
-- --------------------------------------------------
-- Script has ended
-- --------------------------------------------------
Nach diesen Anpassungen wird aus meinem Modell, folgendes DDL-Skript erstellt. Dabei ist ersichtlich, dass die Extended Property - Prozeduren für die Dokumentation nach dem Tabellenskript generiert werden.
-- Dokumentation
-- --------------------------------------------------
-- Entity Designer DDL Script for SQL Server 2005, 2008, and Azure
-- --------------------------------------------------
-- Date Created: 05/11/2011 21:35:51
-- Generated from EDMX file: DokuTest.edmx
-- --------------------------------------------------
SET QUOTED_IDENTIFIER OFF;
GO
USE [dbDWHQueryTest];
GO
IF SCHEMA_ID(N'dbo') IS NULL EXECUTE(N'CREATE SCHEMA [dbo]');
GO
-- --------------------------------------------------
-- Dropping existing FOREIGN KEY constraints
-- --------------------------------------------------
-- --------------------------------------------------
-- Dropping existing tables
-- --------------------------------------------------
-- --------------------------------------------------
-- Creating all tables
-- --------------------------------------------------
-- Creating table 'ProductMenge'
CREATE TABLE [dbo].[ProductMenge] (
[ID] int IDENTITY(1,1) NOT NULL,
[Name] nvarchar(max) NOT NULL,
[CategoryID] int NOT NULL
);
GO
-- EDM Dokumentation
exec sp_addextendedproperty N'MS_Description', 'Enthält alle Produkte und dient als Beispiel für die SQL Server Extended Properties', N'user', N'dbo',
N'table', N'ProductMenge'
GO
exec sp_addextendedproperty N'MS_Description', 'Identifiziert das Produkt', N'user',
N'dbo', N'table', N'ProductMenge', N'column', N'ID'
GO
exec sp_addextendedproperty N'MS_Description', 'Name des Produkts', N'user',
N'dbo', N'table', N'ProductMenge', N'column', N'Name'
GO
exec sp_addextendedproperty N'MS_Description', 'Fremdschlüssel auf die Kategorie', N'user',
N'dbo', N'table', N'ProductMenge', N'column', N'CategoryID'
GO
-- End documentation
-- Creating table 'CategoryMenge'
CREATE TABLE [dbo].[CategoryMenge] (
[ID] int IDENTITY(1,1) NOT NULL,
[Name] nvarchar(max) NOT NULL
);
GO
-- EDM Dokumentation
exec sp_addextendedproperty N'MS_Description', 'Enthält alle Produkte und dient als Beispiel für die SQL Server Extended Properties', N'user', N'dbo',
N'table', N'CategoryMenge'
GO
exec sp_addextendedproperty N'MS_Description', 'Identifiziert die Kategorie', N'user',
N'dbo', N'table', N'CategoryMenge', N'column', N'ID'
GO
exec sp_addextendedproperty N'MS_Description', 'Bezeichnung der Kategorie', N'user',
N'dbo', N'table', N'CategoryMenge', N'column', N'Name'
GO
-- End documentation
-- --------------------------------------------------
-- Creating all PRIMARY KEY constraints
-- --------------------------------------------------
-- Creating primary key on [ID] in table 'ProductMenge'
ALTER TABLE [dbo].[ProductMenge]
ADD CONSTRAINT [PK_ProductMenge]
PRIMARY KEY CLUSTERED ([ID] ASC);
GO
-- Creating primary key on [ID] in table 'CategoryMenge'
ALTER TABLE [dbo].[CategoryMenge]
ADD CONSTRAINT [PK_CategoryMenge]
PRIMARY KEY CLUSTERED ([ID] ASC);
GO
-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
-- Creating foreign key on [CategoryID] in table 'ProductMenge'
ALTER TABLE [dbo].[ProductMenge]
ADD CONSTRAINT [FK_CategoryProduct]
FOREIGN KEY ([CategoryID])
REFERENCES [dbo].[CategoryMenge]
([ID])
ON DELETE NO ACTION ON UPDATE NO ACTION;
-- Creating non-clustered index for FOREIGN KEY 'FK_CategoryProduct'
CREATE INDEX [IX_FK_CategoryProduct]
ON [dbo].[ProductMenge]
([CategoryID]);
GO
-- --------------------------------------------------
-- Script has ended
-- --------------------------------------------------
Die Dokumentation des Datenmodells auf dem SQL Server mithilfe der Extended Property - Prozeduren ist realisierbar, wenn man sich an den Gedanken gewöhnen kann, dies Selbst zu tun. Die Erweiterungspunkte mithilfe von T4 und Workflows machen daraus auch ein Kinderspiel.
Für mich war dieses Beispiel auch eine kleine Fallstudie, die nicht bis zum Ende durchdacht ist.
Was fehlt in dieser Vorlage im Bezug auf die Dokumentation noch?










Social Bookmarking