Deutsche SQL Server Konferenz 2015 – Nachlese 26.02.2015

Andreas Franz
Andreas Franz, Principal eXpert BI

Anfang Februar veranstaltete die PASS SQL Server Community zusammen mit Microsoft die zweite Deutsche SQL Server Konferenz in Darmstadt und SDX war als Austeller mit dabei.

Die Agenda der Veranstaltung hat das gehalten was sie versprochen hat: sehr informative Vorträge – zum Teil auch tief technisch – aus den Bereichen DBA, DEV, BI und Informationen Management.

Als BI eXpert kann ich diese Veranstaltung jedem weiterempfehlen, der sich rund um den SQL Server und Business Intelligence auf dem Laufenden halten möchte.

Auf dem SDX Stand präsentierten wir uns mit dem Thema „Professional BI mit Touch! One Application - Multiple Devices“ und zeigten damit professionelle Business Apps auf unterschiedlichen Plattformen mit unterschiedlichen Formfaktoren:

clip_image002[7]

Anhand der „coolen“ Apps (nativ, nativ mit Xamarin oder hybrid mit Cordova) konnten wir mögliche Lösungsansätze für die Herausforderungen eines solchen Themas aufzeigen:

  • Responsive Web (unterschiedliche Formfaktoren mit unterschiedlicher Ausrichtung)
  • Komplexe Backendintegration (Microsoft CRM, SAP, eigene Systeme, u.v.m.)
  • Interaktive Dashboards (Highlevel Informationen mit Drill-Through/-Down)
  • Mobiles Reporting (Integration bestehender BI Reports)

Fazit: Die mit den Teilnehmern geführten Diskussionen waren sehr vielseitig, angefangen von technischen „Fachsimpeleien“ über fachliche Diskussion bis hin zu Gesprächen rund um unser Gewinnspiel (auf Grund unserer Projekttätigkeit für eine große deutsche Fluggesellschaft gab es einen Flug im Flight Simulator zu gewinnen).

Gerne nächstes Jahr wieder. ;-)

15881452424_e6cb48ea1d_o

Share |

Neue eXperts gesucht: Mobile- /Web- Developer m/w (.NET, Xamarin, HTML) 25.02.2015

Svenja Henß
Svenja Henß, Senior Assistant

Du wünschst Dir Kollegen auf Augenhöhe?
Dich begeistern aktuelle Technologien?
Du liebst attraktive Projekte bei Enterprise Kunden?
Du schläfst abends gerne zu Hause im eigenen Bett?

Dann herzlich Willkommen im Team der SDX eXperts!

Wir suchen weitere Mobile- / Web-Developer für unsere Standorte in Frankfurt und München. Unsere Projekte sind übernachtungsfrei und der Projekteinsatz auf den Raum Frankfurt bzw. Raum München beschränkt.

Mehr Infos zum aktuellen Jobangebot gibt es hier:

Xamarin

Share |

Testen von Datenbanken - 1 Einführung 24.02.2015

Alexander Kabisch
Alexander Kabisch, Principal eXpert

Meiner Meinung nach macht es definitiv Sinn, bestimmten Code einer Datenbank zu testen. Ich konzentriere mich dabei eher auf komplexe Verarbeitungen. Einfache CRUD Methoden mit Tests abzudecken schlägt sich meist in steigendem Aufwand nieder. In nachfolgenden Kapiteln möchte ich nun einige Möglichkeiten vorstellen, wie man einen Datenbanktest erstellen könnte.

Ich werde Begriffe wie Systemtest oder Integrationstest vermeiden und von Testfällen und Testszenarien sprechen. Ein Testfall ist ein atomarer Test mit Eingangsdaten, Verarbeitung und einem erwarteten und zu prüfenden Ergebnis. Als Testszenario bezeichne ich mehre Testfälle, die logisch zusammen gehören. Also beispielsweise die gleichen Eingangsdaten verwenden.

Meine Testfälle sind

  • technisch voneinander unabhängig,
  • nutzen eine Verbindung zu einer Datenbank,
  • lesen und schreiben Dateien

und sind sehr wahrscheinlich nicht nach 200ms fertig. Ausführbar sind sie über die Visual Studio Testtools oder auch NUnit und zum Schluss ist jeder einzelne rot oder grün.

Warum existiert komplexer Code in der Datenbank?

Ich verwende gern einfache CRUD Methoden, aber hin und wieder müssen Daten nur von einer Tabelle in eine andere Tabelle kopiert, gruppiert, erweitert (Bsp.: mit technischen IDs), etc. werden. Hier würde ich immer eine etwas komplexere Verarbeitung in der Datenbank als in einem darüber liegendem Layer bevorzugen.

Aber auch aus der Historie der Anwendung heraus können komplexe Datenbankverarbeitungen existieren. Hier lohnen sich Datenbanktest ganz besonders. Man kann so nicht nur seine Weiterentwicklung absichern, sondern auch eher ein Migration und Überarbeitung angehen. Wer weiß schon ohne Tests ob der neue Code zum gleichen Ergebnis führt?

Migration

Referenzanwendung

Als Basis für meine Erläuterungen habe ich mich für eine relativ einfache Verarbeitung entschieden. Mein .NET DataAcessLayer ruft eine Stored Procedure im MS SQL Server auf. Dabei sollen Daten aus der Tabelle [dbo].[Source] in die Tabellen [dbo].[Target_Code] und [dbo].[Target_Sum] überführt werden. Sie werden nach [Code] gruppiert und mit ein technischer Schlüssel über [dbo].[Target_Code] versehen, der ggf. auch neu erzeugt werden muss. Die Summen pro Code Schlüssel und Datum werden in [dbo].[Target_Sum] gespeichert.

Die später zu testende Datenbank:

DB Diagramm

   1: CREATE PROCEDURE [dbo].[Delete_Target_Sum_ByCalcDate] 
   2:     @CalcDate AS DATETIME
   3: AS
   4: BEGIN
   5:     SET NOCOUNT ON;
   6:  
   7:     DELETE FROM dbo.Target_Sum WHERE CalcDate = @CalcDate;
   8: END
   1: CREATE PROCEDURE [dbo].[Insert_Target_ByCalcDate] 
   2:     @CalcDate AS DATETIME
   3: AS
   4: BEGIN
   5:     SET NOCOUNT ON;
   6:  
   7:     WITH Source_Code AS
   8:     (
   9:         SELECT 
  10:             DISTINCT Code
  11:         FROM dbo.Source
  12:         WHERE CalcDate = @CalcDate
  13:     )
  14:     INSERT INTO dbo.Target_Code
  15:     (
  16:         Code
  17:     )
  18:     SELECT 
  19:         Code
  20:     FROM Source_Code
  21:     WHERE Code not in (SELECT Code FROM dbo.Target_Code);
  22:  
  23:     WITH Source_Sum AS
  24:     (
  25:         SELECT 
  26:             Code,
  27:             SUM(ISNULL(Value,0.0)) as Value_Sum
  28:         FROM dbo.Source
  29:         WHERE CalcDate = @CalcDate
  30:         GROUP BY Code
  31:     )
  32:     INSERT INTO dbo.Target_Sum 
  33:     ( 
  34:         CalcDate,
  35:         ID,
  36:         Value
  37:     )
  38:     SELECT 
  39:         @CalcDate, 
  40:         C.ID, 
  41:         S.Value_Sum
  42:     FROM Source_Sum AS S
  43:     INNER JOIN dbo.Target_Code C
  44:     ON S.Code = C.Code;
  45:  
  46:  
  47: END

und ihre Zugriffsschicht

   1: public class DBManagerTarget
   2: {
   3: public void DeleteTargetSum(DateTime calcDate)
   4: {
   5:     ExecSql((SqlCommand cmd) => FillCommand(cmd, "dbo.Delete_Target_Sum_ByCalcDate", calcDate));
   6: }
   7:  
   8: public void FillTarget(DateTime calcDate)
   9: {
  10:     ExecSql((SqlCommand cmd) => FillCommand(cmd, "dbo.Insert_Target_ByCalcDate", calcDate));
  11: }
  12:  
  13: private void FillCommand(SqlCommand cmd, string spName, DateTime calcDate)
  14: {
  15:     cmd.CommandType = System.Data.CommandType.StoredProcedure;
  16:     cmd.CommandText = spName;
  17:  
  18:     SqlParameter parCalcDate = cmd.CreateParameter();
  19:     parCalcDate.ParameterName = "@CalcDate";
  20:     parCalcDate.DbType = System.Data.DbType.DateTime;
  21:     parCalcDate.SqlValue = calcDate.Date.ToString("yyyy-MM-dd HH:mm:ss");
  22:     cmd.Parameters.Add(parCalcDate);
  23: }
  24:  
  25: public void ExecSql(Action<SqlCommand> fillCommand)
  26: {
  27:     try
  28:     {
  29:         string connectionString = ConfigurationManager.ConnectionStrings["DB"].ConnectionString;
  30:         using (SqlConnection con = new SqlConnection(connectionString))
  31:         {
  32:             con.Open();
  33:             using (SqlCommand cmd = con.CreateCommand())
  34:             {
  35:                 fillCommand(cmd);
  36:                 cmd.ExecuteNonQuery();
  37:             }
  38:         }
  39:     }
  40:     catch (Exception ex)
  41:     {
  42:         //TODO
  43:         throw;
  44:     }
  45: }
  46: }

Testhilfen

Was wäre ein Test ohne eine entsprechende Prüfung? Um mir hier das Leben zu vereinfachen habe ich diese Prüfung in ein eigenes Assembly ausgelagert. Dazu gehören:

  • Dateien mit SQL Befehlen im Test gegen die Datenbank ausführen
  • Speichern von Ergebnissen eines SQL Befehls in einer Datei
  • Einfacher Dateienvergleich.
   1: public class DBManager
   2: {
   3:     public string ConnectionStringKey { get; private set; }
   4:     public bool AssertTransactionContext { get; private set; }
   5:  
   6:     public DBManager(string connectionStringKey, bool assertTransactionContext)
   7:     {
   8:         ConnectionStringKey = connectionStringKey;
   9:         AssertTransactionContext = assertTransactionContext;
  10:     }
  11:  
  12:     public void ExecFile(string filePath)
  13:     {
  14:         string sql = File.ReadAllText(filePath);
  15:         ExecCommand(sql);
  16:     }
  17:  
  18:     public void ExecCommand(string cmdText)
  19:     {
  20:         try
  21:         {
  22:             string connectionString = ConfigurationManager.ConnectionStrings[ConnectionStringKey].ConnectionString;
  23:             using (SqlConnection con = new SqlConnection(connectionString))
  24:             {
  25:                 if (AssertTransactionContext && Transaction.Current == null)
  26:                     throw new ArgumentException("nicht ohne Transaction!");
  27:  
  28:                 con.Open();
  29:                 //con.EnlistTransaction(transaction);
  30:                 /* alternative für GO
  31:                         Server db = new Server(new ServerConnection(conn));
  32:                         string script = File.ReadAllText(scriptPath);
  33:                         db.ConnectionContext.ExecuteNonQuery(script); 
  34:                 */
  35:  
  36:                 foreach (string cmd in cmdText.Split(new string[] { "GO" }, StringSplitOptions.RemoveEmptyEntries))
  37:                 {
  38:                     using (SqlCommand sqlCmd = new SqlCommand())
  39:                     {
  40:                         sqlCmd.Connection = con;
  41:  
  42:  
  43:                         sqlCmd.CommandType = System.Data.CommandType.Text;
  44:                         sqlCmd.CommandText = cmd;
  45:  
  46:                         sqlCmd.ExecuteNonQuery();
  47:                     }
  48:                 }
  49:  
  50:  
  51:                 con.Close();
  52:             }
  53:         }
  54:         catch (Exception ex)
  55:         {
  56:             //TODO
  57:             throw;
  58:         }
  59:     }
  60:         
  61:     public void ReadToFile(string filePath, string cmdText)
  62:     {
  63:         try
  64:         {
  65:             string connectionString = ConfigurationManager.ConnectionStrings[ConnectionStringKey].ConnectionString;
  66:             using (SqlConnection con = new SqlConnection(connectionString))
  67:             {
  68:                 if (AssertTransactionContext && Transaction.Current == null)
  69:                     throw new ArgumentException("nicht ohne Transaction!");
  70:  
  71:                 con.Open();
  72:                 using (SqlCommand sqlCmd = new SqlCommand())
  73:                 {
  74:                     sqlCmd.Connection = con;
  75:  
  76:  
  77:                     sqlCmd.CommandType = System.Data.CommandType.Text;
  78:                     sqlCmd.CommandText = cmdText;
  79:  
  80:                     using (SqlDataReader reader = sqlCmd.ExecuteReader())
  81:                     {
  82:                         File.Delete(filePath); //könnte auch Folder mit Timestamp erzeugen!
  83:                         using (FileStream stream = new FileStream(filePath, FileMode.CreateNew))
  84:                         {
  85:                             using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8))
  86:                             {
  87:                                 writer.WriteLine(cmdText);
  88:                                 writer.WriteLine("");
  89:                                 writer.WriteLine(GetColumnInfo(reader));
  90:                                 writer.WriteLine("");
  91:  
  92:                                 while (reader.Read())
  93:                                 {
  94:                                     writer.WriteLine(GetData(reader));
  95:                                 }
  96:                             }
  97:                         }
  98:                     }
  99:                 }
 100:  
 101:                 con.Close();
 102:             }
 103:         }
 104:         catch (Exception ex)
 105:         {
 106:             //TODO
 107:             throw;
 108:         }
 109:     }
 110:  
 111:     private string GetColumnInfo(SqlDataReader reader)
 112:     {
 113:         StringBuilder sb = new StringBuilder();
 114:         for (int i = 0; i < reader.FieldCount; i++)
 115:         {                
 116:             if(i > 0)
 117:                 sb.Append(",");
 118:  
 119:             sb.AppendFormat("{0}({1})", reader.GetName(i), reader.GetDataTypeName(i));
 120:         }
 121:  
 122:         return sb.ToString();
 123:     }
 124:  
 125:     private string GetData(SqlDataReader reader)
 126:     {
 127:         StringBuilder sb = new StringBuilder();
 128:         for (int i = 0; i < reader.FieldCount; i++)
 129:         {
 130:             if (i > 0)
 131:                 sb.Append(",");
 132:  
 133:             sb.AppendFormat("{0}", reader.GetValue(i));
 134:         }
 135:  
 136:         return sb.ToString();
 137:     }
 138: }
   1: public static class CompareHelper
   2: {
   3:     public static void AssertAreEual(string expectedFilePath, string actualFilePath)
   4:     {
   5:         string expectedContent = File.ReadAllText(expectedFilePath,Encoding.UTF8);
   6:         string actualContent = File.ReadAllText(actualFilePath, Encoding.UTF8);
   7:         Assert.AreEqual(expectedContent, actualContent);
   8:     }
   9: }

Testfall

Die Tabelle [dbo].[Source] enthält folgende Daten:

1 - 7 Input

Nach dem Test sollen die Tabelle [dbo].[Target_Code] wie folgt befüllt sein:

1 - 9 output

und die Tabelle [dbo].[Target_Sum] so:

1 - 8 output

Im Test werden das SQL-Statement, die Spaltenstruktur und das Ergebnis in eine Textdatei geschrieben und verglichen. Der erwartete Inhalt der Datei sieht wie folgt aus:

SELECT * FROM dbo.Target_Sum WHERE CalcDate='2014-01-01';
 
CalcDate(datetime),ID(int),Value(float)
 
01.01.2014 00:00:00,1,10
01.01.2014 00:00:00,2,20
01.01.2014 00:00:00,3,30
01.01.2014 00:00:00,4,40

Diese Umgebung wird meine Grundlage sein, um die verschiedenen Vorgehen zu vergleichen. Während Daten, Anwendung und sogar Prüfung der Test gleich bleiben, wird sich lediglich die Methodik  und das Konzept der Test ändern.

zur Übersicht

Share |