code it

Martins Tech Blog

6. Treffen der SharePoint Usergroup Dresden am 28.01.2010

Am 28.01.2010 findet das erste Usergroup-Treffen in diesem Jahr statt. Dieses Mal treffen wir uns bei der datafino GmbH am Waldschlösschen. Beginn ist 19:00 Uhr.

Thematisch gibt es auch dieses Mal eine interessante Mischung aus Entwickler- und Anwenderthemen:

Zunächst widmet sich Ronny Schattauer gemeinsam mit uns dem Thema Datenmodellierung in SharePoint mit Hilfe von Microsoft Visio. Es geht dabei einerseits um die Modellierung der Datenstrukturen in UML, andererseits aber auch um die automatische Generierung von Programmcode zur Verwendung in SharePoint Solutions aus den erstellten Modellen.

Im Anschluss daran setzen wir eine Diskussionsrunde zum Thema Web 2.0 Funktionen in SharePoint 2010. Bei der letzten Usergroup haben wir uns ja dem großen Thema SharePoint 2010 gewidmet. Dabei haben wir auch gesehen, dass sich sehr viel im Bereich Web 2.0-Funktionalität getan hat – um nur einige Features zu nennen: verbesserter Wiki-Editor, Kommentar- und Tagging-Funktionalitäten. Seitdem hat sicher der ein oder andere schon Eindrücke sammeln können, die wir gern diskutieren würden: Sind die bisherigen Ansätze überhaupt in der Praxis brauchbar?

Weitere Informationen und die Möglichkeit zur Anmeldung gibt es im Xing-Event.

Ansprechpartner für Rückfragen: Sascha Henning, Martin Hey

Wie aktuell sind meine Statistiken?

Aktuelle Abfragestatistiken können zu schnellerer Ausführung von Queries im SQL-Server führen, denn sie sind die Grundlage der Ausführungspläne. Aber wann werden Statistiken aktualisiert und wie aktuell sind die Statistiken überhaupt?

Welche Informationen es zu einer Statsitik gibt, erhält man mit dem Befehl DBCC SHOW_STATISTICS. Im Ergebnis sieht man unter anderem auch, wie viele Zeilen laut Statistik erfasst sind und wann die Statistiken das letzte Mal aktualisiert wurden.

Anhand des folgenden Beispiels soll verdeutlicht werden, wann die Statistiken eines Index aktualisiert werden. Grundlage ist eine Tabelle mit Mitarbeiterdaten - der Einfachheit halber nur Nachname und Vorname.

Statistiken werden erst bei Lesezugriff aktualisiert
So lange in die Tabelle nur Datensätze eingefügt werden, wird die Statistik nicht aktualisiert. In einem ersten Beispiel werden 10000 Datensätze an eine Tabelle angefügt und im Anschluss daran die Statistikeigenschaften abgefragt.

Erst nachdem der Server beim Lesezugriff festgestellt hat, dass die Statistiken nicht aktuell sind, werden diese aktualisiert.

 

Statistiken werden erst ab einer bestimmten Anzahl relevanter Änderungen aktualisiert
Nicht jede Änderung an den Daten führt zu einer Neuberechnung der Statistiken. Grundlage dafür sind sogenannte column modification counters. Eine Statistik wird als nicht aktuell angesehen wenn:

  • erst kein Datensatz in der Tabelle war und nun mindestens ein Datensatz in der Tabelle ist
  • wenn die Anzahl der Datensätze zur letzten Ermittlung der Statistiken <= 500 war und seitdem mehr als 500 relevante Änderungen durchgeführt wurden
  • wenn die Anzahl der Datensätze zur letzten Ermittlung der Statistiken > 500 war und seitdem an 500 + 20% der Anzahl an Datensätzen Änderungen durchgeführt wurden 

s. auch entsprechende Einträge im Technet zu SQL-Server 2005 und SQL-Server 2008

In der Beispieltabelle sind nun 10.000 Datensätze. Nach der eben genannten Formel wäre die nächste Neuberechnung fällig, wenn (10.000 * 20% + 500 = ) 2.500 relevante Änderungen vorgenommen wurden.

Also eben 2.499 Datensätze hinzugefügt, die Daten abgefragt und geschaut, was die Statistik macht....

 

Erwartungsgemäß basiert die Statistik noch immer auf 10.000 Datensätzen. Fügt man nun noch einen Datensatz hinzu, so wird sichtbar, dass genau jetzt eine Aktualisierung vorgenommen wird.

 

Das Beispiel zeigt, dass man bei ungünstiger Datenkonstellation noch sehr lange mit nicht aktuellen Statistiken und Ausführungsplänen arbeiten kann und gibt vielleicht dem ein oder anderen Administrator den notwendigen Anschub, einen Datenbank-Wartungsjob zur Aktualisierung der Statistiken einzurichten...

Temporäre Stored Procedures in SQL-Server

Um sehr rechenintensive Operationen im SQL-Server durchzuführen, bietet es sich an, zu geeigneten Zeitpunkten Zwischenergebnisse zu generieren, diese zu speichern und dann auf Basis dieser Ergebnisse weiterzuarbeiten. Nun besitzt aber nicht jeder Benutzer Berechtigungen, in Datenbanken Tabellen nach Lust und Laune zu erstellen oder in bestehenden Tabellen Datensätze anzufügen, zu löschen oder zu manipulieren.

Zur Lösung dieses Problems bieten sich temporäre Tabellen an. Mit Hilfe des Zeichens # vor dem Tabellennamen wird dem Server signalisiert, dass keine physische Tabelle in der Datenbank erstellt werden soll, sondern vielmehr eine temporäre. Natürlich hat diese auch eine physische Repräsentation (in der tempdb) aber das soll hier keine Rolle spielen. Vorteil dieser Tabellen ist, dass sie nur im aktuellen Kontext gültig sind und über keinerlei Berechtigungseinschränkungen verfügen. Über die Anzahl der # wird definiert, ob sie nur in der aktuellen Session oder global gültig sein soll.

SELECT [Id], [Firstname], [Lastname], [Street], [Zipcode], [City]
INTO #MyAddressTable
FROM MyComplexLongRunningAddressView

/* do further calculations */

Doch dieses Feature gibt es nicht nur Tabellen. Nach dem gleichen Muster können auch nur für den aktuellen Kontext gültige Stored Procedures erstellt werden.

CREATE PROCEDURE #DoSomeComplexCalculations(@id as int)
AS
BEGIN
 /*complex calculations*/
END
GO

EXEC #DoSomeComplexCalculations 1
EXEC #DoSomeComplexCalculations 2

Die Prozedur wird automatisch wieder gelöscht, wenn die Session beendet wird. Sicher: In den meisten Fällen wird es am sinnvollsten sein, richtige Stored Procedures zu erstellen, die dann auch allen Benutzern zur Verfügung stehen und nicht ständig wieder neu erstellt werden müssen - aber ich bin mir sicher, dass es auch den ein oder anderen Anwendungsfall gibt, in dem es sinnvoll ist, auf diese temporären Objekte zurückzugreifen.

Unerwartete ChangeConflictException in LinqToSql

Das Löschen eines Datensatzes in LinqToSql in der Regel recht einfach: Zunächst ermittelt man das zu löschende Entity und ruft im Folgenden zunächst die Methode DeleteOnSubmit und im Anschluss SubmitChanges auf.

public void RemoveAddressById(int id)
{
    using (AddressRepositoryDataContext context = new AddressRepositoryDataContext(ConnectionString))
    {
        Address addressToDelete = (from address in context.Addresses
                                   where address.Id == id
                                   select address).FirstOrDefault();
        if (addressToDelete != null)
        {
            context.Addresses.DeleteOnSubmit(addressToDelete);
            context.SubmitChanges();
        }
    }
}

Dieses Vorgehen war bei mir bisher immer von Erfolg gekrönt. Heute allerdings funktionierte es plötzlich nicht mehr - bei der Ausführung erschien eine ChangeConflictException.

Ein Konflikt konnte es nicht sein, denn es handelte sich um die einzige Applikation auf dieser Datenbank und es wurden auch keine weiteren Datenänderungen durchgeführt.

Seltsam sah auch das automatisch abgesetzte SQL-Statement aus - so kann das ja nicht funktionieren....

Was war passiert? Zusammengefasst: Das in der dbml-Datei gespeicherte Datenbankschema stimmte nicht mehr mit dem real existierenden Datenbankschema überein. Beim Entwurf waren alle Spalten als Not Nullable definiert. In der Zwischenzeit war eine der Spalten Nullable geworden. Beim entsprechenden Datensatz stand nun in dieser Spalte auch ein Null-Wert. Offenbar kommt LinqToSql damit nicht klar. Nach einer Aktualisierung des Schemas der dbml-Datei funktionierte alles wieder wie gewohnt.

Bliebe zu wünschen übrig, dass hier eine aussagekräftigere Fehlermeldung kommt - zumal der Wert der Spalte für eine Delete-Operation überhaupt nicht notwendig ist, da der zu löschende Datensatz ja eindeutig anhand des Primärschlüssels erkennbar gewesen wäre.

Treffen der .NET Usergroup Dresden

Die .NET Usergroup Dresden trifft sich das nächste Mal am 03.02.2010 im Gebäude der T-Systems MMS. Beginn ist wie immer 18:00 Uhr und an diesem Abend gibt es wieder zwei sehr interessante Vorträge:
  • Einführung in die Entwicklung mit Windows Azure (Erik Baum)
    Erik wird uns einen praktischen Einstieg in die Entwicklung mit Microsofts Cloud Computing Dienst Windows Azure geben und uns zeigen wie man mit wenigen Schritten zur eigenen Anwendung in der Cloud kommt. 
  • Dynamic Linq (Martin Hey)
    Anhand eines Beispieles zeige ich, wie man zur Laufzeit Expression-Trees für Linq-Abfragen erstellt, die man dann zur dynamischen Filterung und Sortierung von Objekten verwenden kann.
Weitere Informationen zum Termin und einen Link zur Anmeldeliste findet man auf der Seite der .NET Usergroup.

Wo sind schon wieder meine Code Coverage-Ergebnisse?

Mal wieder gab es ein Problem mit den Code-Coverage-Ergebnissen - dieses Mal allerdings nicht bin Desktop-Build wie in meinem letzten Post als die Code-Coverage-Ergebnisse fehlten. Im aktuellen Fall waren sie im aktuellen Projekt beim Teambuild nicht vorhanden. Das war etwas ernüchternd, denn genau diese Kennzahlen sollten Teil des Projektstatusberichtes sein.

In meinem Fall steuert die tfsbuild.proj den Serverbuild. Bereits enthalten waren die Tags RunTest und RunCodeAnalysis. Mit diesen Einstellungen war ich davon ausgegangen, dass auch die Tests durchgeführt und Code-Coverage berechnet werden kann. Aber weit gefehlt: Zwar wurden Tests durchgeführt, aber als Ergebnis der Code-Coverage kam die Meldung "No coverage result".

Nach einigem Suchen war das Problem gefunden: Damit die Ergebnisse auch erzeugt werden, benötigt der Build die Angabe einer *.testrunconfig-Datei. In dieser Datei stehen die Informationen von welchen Dateien Code-Coverage-Ergebnisse erzeugt werden sollen. Eine solche Datei wird in der Regel auch schon automatisch angelegt, wenn man ein Testprojekt erstellt und genau eine Datei mit diesem Schema muss auch in der tfsbuild.prj-Datei unter dem Tag RunConfigFile angegeben werden. Ist dieser Tag in der prj-Datei nicht vorhanden, so muss er komplett neu hinzugefügt werden.

<!--  TESTING
 Set this flag to enable/disable running tests as a post-compilation build step.
-->
<RunTest>true</RunTest>

<!--  CODE ANALYSIS
 Set this property to enable/disable running code analysis. Valid values for this property are 
 Default, Always and Never.
     Default - Perform code analysis as per the individual project settings
     Always  - Always perform code analysis irrespective of project settings
     Never   - Never perform code analysis irrespective of project settings
 -->
<RunCodeAnalysis>Default</RunCodeAnalysis>

<RunConfigFile>$(SolutionRoot)\MyProject\testrun.testrunconfig</RunConfigFile>

Mit dem so konfigurierten Build werden nun auch Code-Coverage-Ergebnisse beim Teambuild erzeugt.

In Microsoft Access ein eigenes Ribbon erstellen

Seit Office 2007 glänzt Microsoft Access mit Ribbons statt klassischer Menüs. Die Ribbons sind erweiterbar und mit Hilfe von etwas XML kann man recht einfach eigene Ribbons erstellen.

Die Konfiguration wird in einer neuen Systemtabelle gespeichert. Dazu muss zunächst einmal sichergestellt sein, dass in den Navigationsoptionen der Haken bei Systemobjekte anzeigen gesetzt ist. 

 

Nun legt man eine neue Tabelle mit dem Namen USysRibbons an, mit folgenden Feldern:

NameDatentyp
RibbonName Text
RibbonXml Memo

Da der Ribbonname eindeutig sein muss, kann das entsprechende Feld auch gleich als Primärschlüssel verwendet werden - es spricht aber auch nichts dagegen, noch ein weiteres Feld (z.B. Id) anzufügen und dieses als Primärschlüssel zu definieren.

Nun kann man in die Tabelle seine Ribbon-Definition eintragen. Wie das XML genau aussehen muss, entnimmt man dabei am besten der MSDN. Eine sehr gute Seite zu dem Thema ist auch accessribbon.de. Möchte man das XML nicht manuell erstellen, so können hier die Visual Studio Tools for Office (Export Ribbon to Xml) oder der Ribbon-Creator helfen.

Nachdem man den Datensatz angelegt und das Ribbon in den Access-Optionen als Standard definiert hat, wird nun ab dem nächsten Start derDatenbank das eben erstellte Ribbon angezeigt.

Ein kleiner Tipp noch am Rande: Problemlos können die bereits implementierten Bilder verwendet werden. Dazu muss nur im Tag imageMso ein gültiger Wert angegeben werden. Leider gibt es dafür keine offizielle Dokumentation von Microsoft. Welche Werte gültig sind, kann man ganz leicht selbst ermitteln, indem man in den Einstellungen der Schnellzugriffsleiste mit der Maus über die verfügbaren Symbole fährt. Im Tooltipp steht die Id des Bildes in Klammern dahinter.

Phonetische Ähnlichkeiten mit Hilfe der Kölner Phonetik erkennen

Bei der Suche nach ähnlichen Wörtern finden grundsätzlich zwei Verfahren Anwendung: Einerseits ist hier die Levenshtein-Distanz zu nennen. Diese bemisst den Unterschied zweier Zeichenketten anhand der Anzahl der notwendigen Operationen, um eine Zeichenkette in eine andere zu überführen. Zum anderen gibt es phonetische Verfahren, die die Ähnlichkeit zweier Wörter anhand des Klangbildes vergleichen.

Eines dieser phonetischen Verfahren ist der sogenannte Soundex-Algorithmus. Dabei wird ein vierstelliger Code gebildet, der den Klang in der englischen Sprache darstellt. So haben beispielsweise "Smith" und "Smythe" den gleichen Soundex-Wert von "S530". Nachteile dieses Algorithmus sind zum einen die Beschränkung auf den englischen Sprachraum. Somit können unter bestimmten Voraussetzungen im Deutschen unzufriedenstellende Ergebnisse entstehen. Ein weiterer Nachteil dieser Methode ist die Beschränkung auf einen 4-stelligen Code. Durch diese Beschränkung werden nur die Anfänge von Wörtern verglichen, was auch wieder zu interessanten Ergebnissen führen kann.

Diese Probleme versucht ein anderer Ansatz zu umgehen - die Kölner Phonetik. Auch hier wird anhand des Klangbildes ein Code gebildet, der aber im Gegensatz zum Soundex-Algorithmus auf die deutsche Sprache zugeschnitten ist und auch keine Längenbeschränkung hat. Den Buchstaben werden Ziffern zwischen 0 und 8 zugewiesen, wobei benachbarte Buchstaben als Kontext benutzt werden:

BuchstabeKontextCode
A, E, I, J, O, U, Y   0
H   -
B   1
P nicht vor H
D, T nicht vor C, S, Z 2
F, V, W   3
P vor H
G, K, Q   4
C im Anlaut vor A, H, K, L, O, Q, R, U, X
vor A, H, K, O, Q, U, X außer nach S, Z
X nicht nach C, K, Q 48
L   5
M, N   6
R   7
S, Z   8
C nach S, Z
im Anlaut außer vor A, H, K, L, O, Q, R, U, X
nicht vor A, H, K, O, Q, U, X
D, T vor C, S, Z
X nach C, K, Q

Bei der Implementierung empfiehlt es sich, die Umlaute ä, ö und ü mit dem Code 0 zu versehen und das ß wie den Buchstaben s zu behandeln.

Die Umsetzung selbst ist dann recht trivial und eine Möglichkeit möchte ich im Folgenden kurz anbringen:

public static string ConvertToColognePhoneticCode(string value)
{
    // check parameter
    if (string.IsNullOrEmpty(value))
    {
        return string.Empty;
    }

    // convert to uppercase and copy to array
    char[] valueChars = value.ToUpperInvariant().ToCharArray();

    // create an array for all the characters without specialities
    char[] value0Chars = new[] { 'A', 'E', 'I', 'J', 'O', 'U', 'Y', 'Ä', 'Ö', 'Ü' };
    char[] value1Chars = new[] { 'B' };
    char[] value3Chars = new[] { 'F', 'V', 'W' };
    char[] value4Chars = new[] { 'G', 'K', 'Q' };
    char[] value5Chars = new[] { 'L' };
    char[] value6Chars = new[] { 'M', 'N' };
    char[] value7Chars = new[] { 'R' };
    char[] value8Chars = new[] { 'S', 'Z', 'ß' };

    // create a stringbuilder to combine the code
    StringBuilder cpCode = new StringBuilder();

    // iterate through the word's characters
    for (int i = 0; i < valueChars.Length; i++)
    {
        // get the current character and it's context
        char previousChar = i > 0 ? valueChars[i - 1] : ' ';
        char currentChar = valueChars[i];
        char nextChar = i < valueChars.Length - 1 ? valueChars[i + 1] : ' ';

        bool isFirstChar = (i == 0 || !Char.IsLetter(previousChar));

        // ignore non letters
        if (!Char.IsLetter(currentChar))
        {
            if (Char.IsWhiteSpace(currentChar))
            {
                cpCode.Append(' ');
            }
            
            continue;
        }

        // if current character is in group with value 0 add value 0
        if (value0Chars.Contains(currentChar))
        {
            cpCode.Append('0');
            continue;
        }
        // if current character is in group with value 1 add value 1
        if (value1Chars.Contains(currentChar))
        {
            cpCode.Append('1');
            continue;
        }
        // if current character is in group with value 3 add value 3
        if (value3Chars.Contains(currentChar))
        {
            cpCode.Append('3');
            continue;
        }
        // if current character is in group with value 4 add value 4
        if (value4Chars.Contains((currentChar)))
        {
            cpCode.Append('4');
            continue;
        }
        // if current character is in group with value 5 add value 5
        if (value5Chars.Contains(currentChar))
        {
            cpCode.Append('5');
            continue;
        }
        // if current character is in group with value 6 add value 6
        if (value6Chars.Contains(currentChar))
        {
            cpCode.Append('6');
            continue;
        }
        // if current character is in group with value 7 add value 7
        if (value7Chars.Contains(currentChar))
        {
            cpCode.Append('7');
            continue;
        }
        // if current character is in group with value 8 add value 8
        if (value8Chars.Contains(currentChar))
        {
            cpCode.Append('8');
            continue;
        }

        // if we are here it's a special combination of characters
        switch (currentChar)
        {
            case 'C':
                if (isFirstChar)
                {
                    if ((new[] { 'A', 'H', 'K', 'L', 'O', 'Q', 'R', 'U', 'X' }).Contains(nextChar))
                    {
                        cpCode.Append('4');
                    }
                    else
                    {
                        cpCode.Append('8');
                    }
                }
                else
                {
                    if ((new[] { 'S', 'Z', 'ß' }).Contains(previousChar))
                    {
                        cpCode.Append('8');
                    }
                    else if ((new[] { 'A', 'H', 'K', 'O', 'Q', 'U', 'X' }).Contains(nextChar))
                    {
                        cpCode.Append('4');
                    }
                    else
                    {
                        cpCode.Append('8');
                    }
                }
                break;
            case 'D':
            case 'T':
                if ((new[] { 'C', 'S', 'Z', 'ß' }).Contains(nextChar))
                {
                    cpCode.Append('8');
                }
                else
                {
                    cpCode.Append('2');
                }
                break;
            case 'P':
                if (nextChar.Equals('H'))
                {
                    cpCode.Append('3');
                }
                else
                {
                    cpCode.Append('1');
                }
                break;
            case 'X':
                if ((new[] { 'C', 'K', 'Q' }).Contains(previousChar))
                {
                    cpCode.Append('8');
                }
                else
                {
                    cpCode.Append('4');
                    cpCode.Append('8');
                }
                break;
        }
    }

    // cleanup the code (remove double characters and remove 0 values)
    StringBuilder cleanedCpCode = new StringBuilder(cpCode.Length);
    for (int i = 0; i < cpCode.Length; i++)
    {
        char lastAddedChar = cleanedCpCode.Length > 0 ? cleanedCpCode[cleanedCpCode.Length - 1] : ' ';
        if (lastAddedChar != cpCode[i])
        {
            if (cpCode[i] == '0' && lastAddedChar == ' ' || cpCode[i] != '0')
            {
                    cleanedCpCode.Append(cpCode[i]);
            }
        }
    }
    // return trhe result
    return cleanedCpCode.ToString();
}

Im ersten Schritt wird das Wort in Großbuchstaben umgewandelt und buchstabenweise in einen Array kopiert. Für Buchstaben, bei denen es keine Besonderheiten zu beachten gibt, werden Arrays definiert, die deren Wertigkeit angeben. Somit kann zu einem späteren Zeitpunkt in einem buchstabenweisen Vergleich dann mit Hilfe der Extension-Method Contains mit nur einem Statement geprüft werden, ob der Buchstabe die jeweilige Wertigkeit hat. Ist keiner dieser Vergleiche positiv, so handelt es sich um einen Buchstaben, der einer besonderen Beachtung verdient und zu dessen Bewertung der Kontext mit hinzugezogen werden muss. Sind alle Wertigkeiten ermittelt, werden Dopplungen und 0-Werte entfernt und das Ergebnis zurückgegeben.

Nachdem meine Funktion fast fertig war, bin ich auf einen Blogeintrag von Klaus Bock zum gleichen Thema gestolpert und auch er hat einen ähnlichen Ansatz gewählt, um den Algorithmus umzusetzen.