Views müssen für Volltextsuche schemagebunden sein

Sicher ein alter Hut für DBA´s! Aber irgendwie stoße ich von Zeit zu Zeit auf dieses kleine Problem. Und um anderen die Suche nach der Lösung zu vereinfachen, schreibe ich es mal auf.

Problemstellung:

Ich möchte über die View einer MS SQL Server Datenbank einen Volltextindex legen.

Tue ich dies, bekomme ich mein Vorhaben mit dem Fehler quittiert, dass eine eindeutige Spalte auf der View vorhanden sein muss, sofern die noch nicht indiziert wurde.

Fehlermeldung: „A unique column must be defined on this table/view.“

In diesem Fall besteht die Lösung darin, der View einen Index zu verpassen, was aber daran scheitert, dass die View nicht schemagebunden ist.

Möchte ich eine vorhandene View an ein Schema binden, kann das mit dem folgenden Statement gemacht werden:

ALTER VIEW [Profile].[MyView]

WITH SCHEMABINDING AS SELECT  

Danach ist es notwendig, einen „Clustered Unique Index“ anzulegen. Dafür bietet sich natürlich eine ID-Spalte an.

Hat das funktioniert, kann ich nun der View den gewünschten Volltext Index spendieren und dann die Volltextsuche ausführen.

Screencast Serie zu Windows 8 App Development

Meine Kollegen von Visual World haben eine interessante und hilfreiche Videoserie zum Thema Windows-8-App-Development gestartet. Mit Hilfe dieser kostenfreien Serie rund um dieses Thema findet man recht schnell den Einstieg in die Spezifika der App-Entwicklung. Einfach mal reinschauen und gerne auch Feedback geben, ob es hilfreich war und gefallen hat.

Die erste Folge hat meine Kollege Geraud erarbeitet: http://www.youtube.com/watch?v=m7iF2-ucDpE&feature=plcp

Viel Spaß!

Volltextsuche mit Problem bei Office 2010-Dateiformaten im SQL Server 2008

Soeben stieß ich auf ein Problem in einer bestehenden Lösung, bei der Office-Dokumente als Binaries in einer varbinary(max) -Spalte im SQL Server gespeichert werden. In der Applikation soll es möglich sein, in der Datenbank gespeicherte Dokumente zu durchsuchen. Dafür wurde für die Binärdaten-Spalte in der Datenbank ein Fulltext-Index/Katalog wie folgt angelegt.

CREATE  FULLTEXT CATALOG [DefaultCatalog] WITH ACCENT_SENSITIVITY=ON

AS  DEFAULT

AUTHORIZATION [dbo]

Danach wird auf der Spalte „Content“, welche die Dokumente als Binary enthält, der Volltext-Index angelegt. Da es sich- wie bereits gesagt- um eine Binärdatenspalte handelt, muss eine weitere Spalte angegeben werden, die die Dateierweiterung enthält. Dort steht im Grunde die Dateiendung, also doc, docx, xls usw. drin.

CREATE FULLTEXT INDEX ON dbo.Documents

(Content TYPECOLUMN Extension)

KEY INDEX Files_PK

ON DefaultCatalog

WITH CHANGE_TRACKING AUTO;

Nun ist es bereits möglich, die Inhalte gespeicherter Dokumente zu durchsuchen. Das folgende Statement gibt uns alle Namen der Dokumente aus, bei denen im Text das Word „Branchenschwerpunkt“ vorkommt.

SELECT Name FROM dbo.Documents  WHERE Freetext (Content, ‚Branchenschwerpunkt‘)

Das funktioniert soweit gut. Nun zum Problem. Zufälligerweise bemerkte ich, dass Worddokumente mit der Endung docx nicht gefunden wurden. Nach etwas Recherche kam ich der Ursache auf die Spur.

Man kann mit dem folgenden Befehl abfragen, welche Documenttypen registiert sind und damit in der Volltextsuche berücksichtigt werden.

SELECT  * FROM sys.fulltext_document_types;

Findet man in dieser Liste die Office 2007/10 Dateitypen (docx, xlsx usw.) nicht, ist folgendes zu tun:

Microsoft bietet sogenannte Filter-Packs an. Der Download für die Microsoft Office 2010-Filterpacks ist hier zu finden:

http://www.microsoft.com/en-us/download/details.aspx?id=17062

Dieses Paket muss auf der Maschine, auf welcher der SQL Server läuft, installiert werden. Danach erfolgt die Registrierung mit folgendem Befehl:

sp_fulltext_service ‚load_os_resources‘, 1

Der SQL Server -Dienst muss anschließend zwingend neu gestartet werden.

Fragt man danach die registrierten Typen erneut ab (SELECT  * FROM sys.fulltext_document_types;), sollten die gewünschten Dateitypen im Resultset erscheinen.

Damit ist das Durchsuchen von Office 2007/2010 -Dokumenten über die Volltextsuche möglich.

VS2010 mit dem TFSPreview verbinden

Unter http://tfspreview.com stellt Microsoft eine Cloudvariante des Team Foundation Server, Team Foundation Service genannt, bereit. Ich finde, dies ist eine tolle Sache. Gerade, wenn man von überall aus arbeiten möchte oder muss. In der Vorschau stehen alle Features kostenfrei zur Verfügung. Was die Nutzung später mal kosten soll, ist derzeit noch nicht bekannt. Aber es soll laut Aussage auf der Homepage zukünftig weiterhin eine kostenfreie, wahrscheinlich abgespeckte(?), Variante geben. Siehe auch: http://tfspreview.com/en-us/pricing/information/

In die neue Visual Studio Version 11 wurden Team Foundation Services bereits integriert. Nutzt man allerdings die (noch) aktuelle Version 10, muss man den Service Pack 1 installiert haben (gibts schon lange, also hat wahrscheinlich jeder gemacht) als auch den HotFix KB2581206. Ist der Hotfix nicht installiert, kann sich Visual Studio nicht gegen Team Foundation Services authentifizieren.

Dieser ermöglicht den Login mittels Windows Live ID. Der Rest ist wie gehabt. Im Menü Team auf „Connect to team foundation server“ klicken, im folgenden Fenster dann „Servers“, danach „Add“ anwählen und die Verbindungsdaten angeben.

Klickt man auf OK, erscheint nun ein Fenster für den Login am Team Foundation Service mittels Windows Live ID.

Viel Spaß beim Probieren.

Validation Application Block und WCF

Begleitend zu meiner DDD Kolumne in der Fachzeitschrift Visual Studio One, bei welcher es in den letzten beiden Ausgaben um das Thema Validierungen ging, möchte ich in diesem Artikel den Einsatz des Validation Application Block der Enterprise Library 5.0 näher betrachten. Die Enterprise Library ist dafür bekannt, dass sie Best-Practise-Konzepte in Form wiederverwendbarer Komponenten bereistellt. Die aktuelle, seit Mai 2011 verfügbare Version 5.0, ist im Download Center auf der Microsoft Webseite erhältlich.

Zum Download

Seit November 2011 ist zudem noch ein Integrationpack für Windows Azure verfügbar.

Neben dem Validation Application Block (VAB) stellt die Library auch andere nützliche Bibliotheken, wie den Unity DI-Container, eine Datenzugriffskomponente sowie Cache-, Logging-, Security- und Exception-Komponenten. Die letztengenannten sind einsetzbar, um typische schichtenübergreifende Aufgaben zu erledigen. Durch den Einsatz kann vor allem Entwicklungszeit gespart werden. In diesem Artikel möcnte ich mich den Vorteilen des Einsatzes in einer Serviceschicht unter Verwendung von WCF widmen.

Nach der Installation der Microsoft Enterprise Library 5.0 (EntLib5) auf der lokalen Maschine, sollten die für das Projekt benötigten Bibliotheken in einen Projektordner kopiert werden, damit die Solution überall lauffähig ist, ohne das auf jeder Entwicklermaschine eine Installation der EntLib5 notwendig wird.

Dieses Vorgehen ist generell zu bevorzugen. Drittanbieterbibliotheken, die in einem Projekt verwendet werden, sollten nie vom Installationsort bezogen werden, sondern immer in einem separaten Projektordner (z.B. in einem Ordner namens „Shared Binaries“. ) abgelegt werden und in die Quellcodeverwaltung eingecheckt werden, so dass die Solution auf allen Entwicklermaschinen lauffähig ist. Konzepte wie Continuous Integration erzwingen ein solches Vorgehen. Nutzt man Binärdateien, die nur lokal verfügbar sind, schlägt der Team-Build fehl.

Der nun zu betrachtende Fall geht davon aus, dass ein Dienst in Form einer öffentlichen API bereitgestellt wird, der Daten liefert als auch verarbeitet. Dafür stellt dieser die notwendigen Operationen bereit. Die Visualisierung der Daten könnte nunmehr durch verschiedene UI Komponenten unterschiedlichster Technologien erfolgen. Für mich bedeuted dies ebenfalls, dass der Service in der Pflicht ist, eingehende Requests zu validieren und bei Nichterfüllung fachlicher Anforderungen, die Anfrage zurückzuweisen. Es wäre fatal, sich darauf zu verlassen, dass der Aufrufer des Dienstes alle Validierungen korrekt durchgeführt hat.

Eine bewährte Methode besteht darin, die DataContracts so auszustatten, dass zumindest formelle Prüfungen schon beim Empfang der Nachrichten stattfinden. Hier kommt nun der Validation Application Block zum Einsatz.

Im konkreten Beispiel möchten wir einen kleinen Webshop entwickeln. Jegliche Geschäftslogik soll auf einem Application-Server laufen, der nach außen von einem WCF-Service repräsentiert wird. Für die Darstellung im Web dient ein ASP.NET MVC Projekt, welches die WCF-Dienste konsumiert. Das Projekt befindet sich der Einfachheit halber in der selben Solution. Dem WCF-Dienste Projekt „Shopping.Services“ müssen die Verweise auf die beiden Bibliotheken „Microsoft.Practices.EnterpriseLibrary.Validation“ und „Microsoft.Practices.EnterpriseLibrary.Validation.Integration.WCF“ hinzugefügt werden [ Siehe Abb. 1].Abb. 1

Der CustomerService stellt für das Erstellen neuer Kunden eine Operation CreateCustomer bereit, welche eine Nachricht vom Typ CreateCustomerRequest entgegennimmt. Diese Serviceschnittstelle sieht wie folgt aus [Listing 1]:

Listing 1

namespace Shopping.Service
{
    using System.Collections.Generic;
    using System.ServiceModel;
    using Microsoft.Practices.EnterpriseLibrary.Validation.Integration.WCF;
    using Shopping.Service.DataContracts;
    using Shopping.Service.Messages;

    [ServiceContract]
    [ValidationBehavior]
    public interface ICustomerService
    {
        /// <summary>
        /// Liefert eine Auflistung aller Kunden.
        /// </summary>
        /// <returns></returns>
        [OperationContract]
        IEnumerable<CustomerInformation> GetAllCustomers();

        /// <summary>
        /// Erstellt einen neuen Kunden.
        /// </summary>
        /// <param name="request">Die Anfrage.</param>
        [OperationContract]
        [FaultContract(typeof(ValidationFault))]
        void CreateCustomer(CreateCustomerRequest request);

        /// <summary>
        /// Löscht einen Kunden aus dem System, sofern möglich.
        /// </summary>
        /// <param name="request">Die Anfrage.</param>
        [OperationContract]
        void DeleteCustomer(DeleteCustomerRequest request);
    }
}

Der Schnittstelle muss das Attribute ValidationBehavior hinzugefügt werden. Damit wird quasi ein WCF-Behavior angeschalten, welches sich in den Nachrichtenkanal einklinkt und die eingehende Nachricht überprüft. Die Operation selbst benötigt das FaultContract -Attribut vom Typ ValidationFault, damit im Fehlerfall eine sinnvolle Fehlermeldung an den Aufrufer gegeben werden kann.

Die Nachricht CreateCustomerRequest sieht wie in Listing 2 dargestellt aus. Die Nachricht besitzt einen Member vom Typ CustomerCreate. CustomerCreate [Listing 3] ist der spezifische DataContract, welcher exakt auf den UseCase „Kunde anlegen“ zugeschnitten ist. Die Klasse besitzt nur die Felder, die bei der Aktion „Kunde anlegen“ verwendet werden dürfen. Dies ist wichtig, da unnötige Member in der Regel Verwirrung stiften. Ich habe schon oft gesehen, dass ein einziger DataContract für mehrere Operationen verwendet wird. Z.B. gleichzeitig für das Erstellen, Bearbeiten und um Details anzuzeigen. Davon ist allerdings abzuraten. In einem solchen Fall ist unklar, welche Eigenschaften im speziellen Fall überhaupt benötigt werden. Daher ist es aus meiner Sicht unerlässlich, für jede Operation einen zugeschnittenen DataContract bereitzustellen.

In Listing 2 ist zu sehen, dass der Member NewCustomer mit dem Attribut ObjectValidator versehen wurde. Dieses ist dafür verantwortlich, dass die Validierung des DataContract erfolgt. Lässt man dieses Attribut weg, dann wird die Validierung des NewCustomer nicht durchgeführt.

Listing 2

namespace Shopping.Service.Messages
{
    using System.ServiceModel;
    using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
    using Shopping.Service.DataContracts;

    [MessageContract]
    public sealed class CreateCustomerRequest
    {
        [MessageBodyMember]
        [ObjectValidator()]
        public CustomerCreate NewCustomer { get; set; }
    }
}

Listing 3

namespace Shopping.Service.DataContracts
{
    using System.Runtime.Serialization;

    using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;

    [DataContract]
    public sealed class CustomerCreate : ICustomerCreate
    {
        [DataMember]
        [NotNullValidator]
        [StringLengthValidator(2, 256)]
        public string FirstName { get; set; }

        [DataMember]
        [NotNullValidator]
        [StringLengthValidator(2, 256)]
        public string LastName { get; set; }

        [DataMember]
        public string StreetAddress { get; set; }

        [DataMember]
        public string PostalCode { get; set; }

        [DataMember]
        [NotNullValidator]
        [StringLengthValidator(3, 256)]
        public string City { get; set; }

        [DataMember]
        public string Country { get; set; }
    }
}

Die Member der Klasse CustomerCreate, welche zwingend einen Wert erfordern, wurden mit dem Attribute NotNullValidator versehen. Dieser stellt sicher, dass der Member einen Wert enthält und nicht NULL ist. Dieses Kennzeichnung legt fest, dass es sich um ein Pflichtfeld handelt.

Zusätzlich habe ich noch den StringLengthValidator hinzugefügt. Dieser legt den Gültigkeitsbereich der Zeichenfolge fest. Hier kann man sozusagen schon am Service Feldlängen prüfen, so dass nicht erst ein Versuch unternommen wird, in die Datenbank -sofern eine relationale Datenbank als physischer Speicherort benutzt wird- zu schreiben, obwohl die dort zulässige Länge überschritten wurde.

Mit diesen wenigen Schritten ist es möglich, die Prüfung eingehender Daten am WCF-Service vorzunehmen.

Neben den beiden hier verwendenten Validierungsattributen gibt es natürlich noch eine Menge weiterer. Beispielsweise einen RangeValidator, der die Gültigkeit numerischer Werte prüft als auch einen RegexValidator, der z.B. bei Überprüfung von Email-Adressen gute Dienste leisten kann.

Auch das Erstellen eigener Validatoren ist denkbar einfach.

Nehmen wir an, der Member Country ist optional. Wird jedoch ein Wert angegeben, so sind nur bestimmte Länder gültig oder man möchte das Land gegen eine bestehende Liste prüfen, um sicher zu stellen, dass nur realistische Werte angegeben werden. Für diesen Fall implementieren wir ein Custom-Attribut [Listing 4]. Dieses Attribut ruft die Implementierung CountryValueValidator auf, welche die Überprüfung des eingehenden Wertes vornimmt. Kommt kein Wert mit, so wird auch keine Prüfung vorgenommen, da es sich ja um eine optional Angabe handelt. Falls doch, wird überprüft, ob der Wert in der Auflistung vorhanden ist. In realistischen Szenarien sollte dies jedoch keine hart codierte Liste sein. Um das Vorgehen zu veranschaulichen, sollte es aber in Ordnung gehen. [Listing 5]

Listing 4

namespace Shopping.Service.Validators
{
    using System;
    using Microsoft.Practices.EnterpriseLibrary.Validation;
    using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;

    public sealed class CountryValueValidatorAttribute : ValueValidatorAttribute
    {
        protected override Validator DoCreateValidator(Type targetType)
        {
            return new CountryValueValidator(string.Empty, string.Empty, false);
        }
    }
}

Listing 5

namespace Shopping.Service.Validators
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Practices.EnterpriseLibrary.Validation;
    using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;

    public sealed class CountryValueValidator : ValueValidator
    {
        private readonly List<string> countries = new List<string>() { "Deutschland", "Schweiz", "Österreich" };

        public CountryValueValidator(string messageTemplate, string tag, bool negated)
            : base(messageTemplate, tag, negated)
        {
        }

        public override void DoValidate(object objectToValidate, object currentTarget, string key, ValidationResults validationResults)
        {
            if (objectToValidate == null)
            {
                return;
            }

            if (objectToValidate is string && !string.IsNullOrEmpty(objectToValidate.ToString()))
            {
                if (!this.countries.Any(c => c == objectToValidate.ToString()))
                {
                    var errorMessage = string.Format("Das Land '{0}' ist nicht zulässig.", objectToValidate);
                    validationResults.AddResult(new ValidationResult(errorMessage, objectToValidate, "Country", string.Empty, this));
                }
            }
        }

        ...    }
}

Gibt man nun über einen Client einen ungültigen Wert im Feld Land ein, so kann die Anfrage nicht verarbeitet werden. Die Operation liefert eine FaultException vom Typ ValidationFault. Laut unserem Beispiel [Listing 5] sind nur Deutschland, Schweiz und Österrich gültig. Der Anwender hat allerding Frankreich eingegeben. Der Service ist in der Lage eine sinnvolle Meldung zu liefern, welche mit wenig Aufwand zum Beispiel in einem Controller (MVC-Framework) verarbeitbar ist und als Fehlermeldung ausgegeben werden kann [Abb. 2]. Die aufrufende Controller-Methode muss im Grunde nur auf die FaultException reagieren und die Details auslesen, diese dann als ModelErrors hinzufügen. Schon weiß das MVC-Framework damit etws anzufangen und kann die Meldung in der View darstellen [Listing 6]. Das Auslesen der Werte sollte korrekterweise nicht in jeder einzelnen Methode stattfinden, sondern an eine zentrale Stelle ausgelagert und wiederverwendet werden.

Abb. 2

Lsting 6

[HttpPost]
        public ActionResult Create(CustomerCreateModel customerToCreate)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    using (var client = new CustomerService.CustomerServiceClient())
                    {
                        client.CreateCustomer(customerToCreate.Customer);
                    }
                    return this.Redirect("/");
                }
                catch (FaultException<ValidationFault> validationFault)
                {
                    foreach (var detail in validationFault.Detail.Details)
                    {
                        ModelState.AddModelError(detail.Key, detail.Message);
                    }
                    return this.View(customerToCreate);
                }
            }

            return this.View(customerToCreate);
        }

Die Ausgabe 2/2012 der Visual Studio One erscheint am 23.2.2012. Bis dahin wird auch eine Beispiel-Lösung zum Thema Validierungen zur Verfügung stehen. Den Link zum Download gebe ich in den nächsten Tagen hier bekannt.

„Clean Code“ – Erster Teil einer niemals endenden Geschichte

Irgendwie ist es doch immer dasselbe. Zu Beginn eines Softwareprojekt hat man noch gute Vorsätze,möchte alles besser machen als beim vorangegangenen Projekt. Aus den Fehlern hat man ja schließlich gelernt! Wer kennt diese Vorsätze nicht? Aber eigentlich ist es doch wie zu Silvester. Man hat gute Vorsätze fürs neue Jahr, will mehr Sport treiben, gesünder leben und so weiter. Aber wieviele dieser Vorsätze setzt man dann tatsächlich in die Tat um? Ist der Wille nicht stark genug oder sind die Vorsätze an sich nur reine Gewohnheit? Man muss sich was fürs neue Jahr vornehmen, dies hat man schon von klein auf beigebracht bekommen. Richtig? Nun, mit den Vorsätzen fürs neue Jahr kann ich diese Frage ohne zu zögern mit JA beantworten. Es ist eine Art Sitte oder Tradition, die da praktiziert wird. Ich nehme mir was vor, was ich eigentlich gar nicht so ernst meine. In Sachen Projektrealisierung ist das ganz anders. Dort sind meine Vorsätze ernst gemeint und entstehen aus tiefster Überzeugung.

Aber warum gelingt es dann nicht, alle Vorsätze bis zum Schluss umzusetzen? Sind es äußere Faktoren, die uns davon abhalten, die eigenen hohen -vielleicht auch sehr hohen- Ansprüche zu erfüllen? Zeit- und Budgetdruck sind mögliche Ursachen, aber nicht die einzigen. Unter Termindruck müssen Design-Entscheidungen getroffen werden, damit das Team beginnen kann, Anforderungen umzusetzen. Die Folgen dieser Entscheidung werden dabei oft von den Verantwortlichen gar nicht überblickt und richtig eingeschätzt. Oftmals sind es vermeintlich kleine Dinge, die zwischen Tür und Angel entschieden werden müssen, die später große, manchmal auch dramatische, Auswirkungen auf das gesamt Projekt haben.

Ich denke, es fehlt Verständnis dafür, dass Softwareprojekte eine gewisse Vorlaufzeit benötigen, bevor man mit der eigentlichen Umsetzung beginnt; eine Art Findungsphase, in der jeweils optimale Technologien, Patterns usw. evaluiert werden können. Allerdings sitzt einem meist schon der Kunde im Nacken und will was Greifbares für sein Geld sehen. Hinzu kommt häufig, dass das Team noch nicht aufeinander eingestimmt ist und die Entwickler stark unterschiedliche Voraussetzungen mitbringen. Der Anspruch an die Code Qualität differiert zudem zwischen den Beteiligten. Es gibt Entwickler, die sich von Style-Guides (z.B. dem Style- oder dem Fxcop oder auch nur firmeninternen Richtlinien) regelrecht gegängelt und bevormundet fühlen, woraus wiederum oft langwierige Diskussionen entstehen. Dabei ist es gerade wichtig, alle Beteiligten für diese so wichtige Thema richtig zu sensiblisieren. Es ist vor allem ein Stück Verantwortungsbewusstsein anderen gegenüber. Nämlich denen, die später den Code lesen, verstehen und bearbeiten müssen, wenn man vielleicht selbst schon nicht mehr im Projekt ist.

Erreicht man dies und schafft es, das gesamte Team dafür zu gewinnen, ist zumindest schon ein erster wichtiger Schritt in die richtige Richtung getan.