Anmelden
Ich möchte für die nächsten 30 Tagen angemeldet bleiben
Deutsch
Several pages in the usergroup are available in English. Click on english to visit these pages.

Blogs

16.03.2008
DNN-Modulprogrammierung - ein einfaches Beispiel (Schritt 1 - Erstellen der Tabelle und der Gespeicherten Prozeduren) (Michael Tobisch)
Die "unterste" Schicht der Anwendung ist die Datenbank. Beginnen wir also damit, die notwendige Tabelle und die Gespeicherten Prozeduren für unser Modul zu erstellen.

Dazu melden wir uns zunächst einmal auf dem Portal in unserer Testinstallation als Host an und wählen im Menü System die Option SQL ausführen:
SQL ausführen

Das führt uns zu folgender Maske:
SQL

Grundsätzlich können wir hier SQL-Befehle absetzen, die dann ausgeführt werden. Eine Besonderheit von DotNetNuke ist die Möglichkeit, mehrere Instanzen (Installationen) auf die selbe Datenbank zugreifen zu lassen. Zu diesem Zweck gibt es das Konzept der sogenannten Objektqualifizierer (object qualifier). Diese werden den Objektnamen (Tabellen, Ansichten, Gespeicherten Prozeduren etc.) vorangestellt, um die einzelnen Installationen voneinander zu unterscheiden. Wie der Objektqualifizierer heißt wird während der Installation angegeben und steht dann in der Datei web.config, das sieht so aus:

<configuration>
   ...
   <dotnetnuke>
      ...
      <data defaultProvider="SqlDataProvider">
         <providers>
            <clear />
            <add name="SqlDataProvider"
                 type="DotNetNuke.Data.SqlDataProvider, DotNetNuke.SqlDataProvider"
                 connectionStringName="SiteSqlServer"
                 upgradeConnectionString=""
                 providerPath="~\Providers\DataProviders\SqlDataProvider\"
                 objectQualifier="dnn_"
                 databaseOwner="dbo" />
         </providers>
      </data>
      ...
   </dotnetnuke>
   ...
</configuration>

In diesem Fall heißt der Objektqualifizierer also "dnn_". Die Tabelle Modules würde in unserer Datenbank also dnn_Modules heißen. Wissen wir das immer? Nein. Vor allem wissen wir nicht, wie der Objektqualifizierer auf irgendeinem System heißt, wo unser Modul installiert wird. Deshalb müssen wir einen Platzhalter dafür angeben, der bei der Ausführung des SQL-Befehls durch den eigentlichen Objektqualifizierer ersetzt wird. Dieses Verhalten wird vom Installer angewendet, um ein Modul sauber in eine DotNetNuke-Instanz zu installieren. Als Platzhalter steht {objectQualifier}. Die Groß-/Kleinschreibung ist dabei zu beachten, sonst funktioniert es nicht. (Gibt es eigentlich ein deutsches Wort für case sensitive?) Das schöne ist nun, dass unser SQL-Fenster genau dieses Verhalten zeigt, und wir den Platzhalter auch hier verwenden können. Geben wir also einmal folgenden SQL-Befehl ein:

SELECT * FROM {objectQualifier}Modules
und klicken wir auf ausführen. Wir erhalten eine Liste aller installierten Module, ohne gewusst haben zu müssen, wie der Objektqualifizierer wirklich heißt.

Dem aufmerksamen Leser wird es nicht entgangen sein, dass in der Konfiguration des Datenproviders auch der Datenbankbesitzer (nomalerweise dbo) steht. Auch für diesen gibt es einen Platzhalter, der sinngemäß gleich funktioniert, dieser ist {databaseOwner} - auch hier gilt die Groß-/Kleinschreibkonvention. Wir könnten also auch folgenden Befehl ausführen lassen, um eine Liste aller angelegten Portale zu erhalten:

SELECT * FROM {databaseOwner}{objectQualifier}Portals

Unter SQL hätten wir nach dem Datenbankbesitzer noch einen Punkt, diesen können wir uns hier ersparen, weil {databaesOwner} nicht durch den Wert in der web.config, sondern durch den Wert in der web.config plus einem Punkt ersetzt wird.

Um nun unsere Tabelle zu generieren, überlegen wir uns einmal, welche Felder benötigt werden:

Table: Contacts

Feldname Feldtyp Länge Schlüssel NULL erlauben? Anmerkung
ModuleID int   FK Modules.ModuleID Nein Um genau diejenigen Kontakte anzuzeigen, die von einer Instanz unseres Moduls erfasst werden, benötigen wir diese Angabe. Ausserdem wollen wir alle Kontakte löschen, wenn ein Modul gelöscht wird.
ContactID int   PK Nein  
Firstname nvarchar 50   Nein  
Lastname nvarchar 50   Nein  
EmailAddress nvarchar 50   Nein  

Können wir uns vorstellen, dass es in unserer Datenbank eine Tabelle gibt, die Contacts (plus vorangestelltem Objektqualifizierer) heißt? Eigentlich ja. Es gehört zum "guten Ton", den eigenen Firmennamen seinen Datenbankobjektnamen voranzustellen, und wir nennen unsere Firma un diesem Beispiel DnnUgDe. Unsere Tabelle wird also {objectQualifier}DnnUgDe_Contacts heißen. Nun können wir darangehen, die Tabelle zu erstellen. Zuerst einmal prüfen wir, ob so eine Tabelle bereits vorhanden ist, diese löschen wir und erstellen die Tabelle neu. (Wenn jetzt jemand anderer ein Modul geschrieben hat, das eine Tabelle namens DnnUgDe_Contacts benötigt, hat das bereits installierte Modul das Nachsehen, aber das ist dann wohl Pech...)

Tippen wir also einmal folgende Statements im SQL-Fenster ein (oder kopieren sie hier und fügen sie in das Fenster ein bzw. du kannst auch die ZIP-Datei mit den beiden Skripts herunterladen, lokal entpacken und dann mit dem Datei-Upload-Element SQL-Datei das Skript CreateTable.sql laden):

-- Überprüfen, ob der Fremdschlüssel zu Modules existiert und ggf. löschen
IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}FK_{objectQualifier}DnnUgDe_Contacts_Modules') AND type = 'F')
ALTER TABLE {databaseOwner}{objectQualifier}DnnUgDe_Contacts DROP CONSTRAINT FK_{objectQualifier}DnnUgDe_Contacts_Modules
GO

-- Überprüfen, ob die Tabelle DnnUgDe_Contacts existiert und ggf. löschen
IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}{objectQualifier}DnnUgDe_Contacts') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
DROP TABLE {databaseOwner}{objectQualifier}DnnUgDe_Contacts
GO

-- Tabelle anlegen
CREATE TABLE {databaseOwner}{objectQualifier}DnnUgDe_Contacts(
	ModuleID [int] NOT NULL,
	ContactID [int] IDENTITY(1,1) NOT NULL,
	Firstname [nvarchar](50) NOT NULL,
	Lastname [nvarchar](50) NOT NULL,
	EmailAddress [nvarchar](50) NOT NULL,
   CONSTRAINT [PK_{objectQualifier}DnnUgDe_Contacts] PRIMARY KEY CLUSTERED (ContactID ASC)
)
GO

-- Fremdschlüssel zu Modules anlegen (mit Löschweitergabe)
ALTER TABLE {databaseOwner}{objectQualifier}DnnUgDe_Contacts WITH CHECK
ADD CONSTRAINT FK_{objectQualifier}DnnUgDe_Contacts_Modules FOREIGN KEY(ModuleID) REFERENCES {objectQualifier}Modules (ModuleID)
ON DELETE CASCADE
GO

-- Fremdschlüssel überprüfen
ALTER TABLE {databaseOwner}{objectQualifier}DnnUgDe_Contacts CHECK CONSTRAINT FK_{objectQualifier}DnnUgDe_Contacts_Modules
GO

Bevor wir jetzt auf ausführen klicken, sollten wir uns noch einige Besonderheiten ansehen:

  • Nach jedem GO ist eine Leerzeile eingefügt. Der Installer benötigt das.
  • Dieses Skript (und das folgende) werden wir später als Installationsskript verwenden. Dazu muss es in eine Datei gespeichert werde, welche
    1. so benannt wird, dass sie den Namen der Modulversion (also z.B. 01.00.00) trägt und SqlDataPrivider als Dateinamenserweiterung, also z.B. 01.00.00.SqlDataProvider
    2. unbedingt UTF-8 kodiert sein muss. Um dies zu erreichen können wir die Datei mit dem normalen Windows-Editor erstellen und im Dialog Speichern unter... die Codierung angeben. Wohin die Datei letztendlich gespeichert wird, dazu kommen wir später. Vorläufig benötigen wir sie nicht.
  • Um ein Installationsskript auszuführen, muss das Kontrollkästchen Skript ausführen aktiviert sein.

Führen wir nun das Skript aus. Wenn das geschehen ist, sollte die Meldung Die Abfrage wurde erfolgreich ausgeführt! unter dem SQL-Skript erscheinen.

Was haben wir getan?
Wir haben eine Tabelle erzeugt. Diese hat den Primärschlüssel ContactID und einen Fremdschlüssel zur Tabelle Modules, der eine Fremschlüssel-Einschränkung dafür sorgt, dass alle Datensätze der Tabelle gelöscht werden, wenn ein Modul gelöscht wird.

Kann mySQL das mittlerweile eigentlich? Ich wäre dieser Datenbank vor einigen Jahren ja beinahe verfallen, bis ich festgestellt habe, dass solche Foreign-Key-Constraints zwar brav vom SQL-Interpreter abgearbeitet werden (ohne jede Fehlermeldung oder irgendeinen Warnhinweis), die Datenbank selbst aber keinerlei Anstalten machte, sich darum auch zu kümmern - es wurde also einfach ignoriert... Das hat mich dann eigentlich doch zu sehr an dBASE erinnert, und ich habe mySQL seither auch ignoriert.

Gespeicherte Prozeduren

Nun benötigen wir noch für jede Methode, die unsere Anwendung zur Verfügung stellen soll, eine Methode, um die Daten aus der Tabelle abzurufen, in die Tabelle einzufügen, zu aktualisieren oder zu löschen. Dazu bedienen wir uns Gespeicherter Prozeduren, natürlich wäre es auch möglich, die SQL-Statements in der Datenzugriffs-Schicht ad hoc zu implementieren, was Vor- und Nachteile hat. Ich will mich darauf hier nicht näher einlassen und verweise auf diverse Diskussionen zum Thema im Web, z.B. Brian Noyes: Using the Visual Studio 2005 Data Set Designer to build a Data Access Layer (auch wenn wir hier keine Typed Datasets verwenden, sondern auf das DNN-Framework bzw. Microsoft Application Blocks zurückgreifen werden), Rob Howard: Don’t use Stored Procedures yet? oder Frans Bouma: Stored Procedures are bad, m’kay?

Wir werden Gespeicherte Prozeduren verwenden. Führen wir folgendes Skript aus (odere laden wir das Skript CreateStoredProcedures.sql aus der ZIP-Datei und führen es aus):

-- Prüfen, ob (zu erzeugende) Gespeicherte Prozedur vorhanden ist und ggf. löschen
IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}{objectQualifier}DnnUgDe_KontaktDelete') AND OBJECTPROPERTY(id,N'IsProcedure') = 1)
DROP PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktDelete
GO

IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}{objectQualifier}DnnUgDe_KontakteGet') AND OBJECTPROPERTY(id,N'IsProcedure') = 1)
DROP PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontakteGet
GO

IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}{objectQualifier}DnnUgDe_KontaktGet') AND OBJECTPROPERTY(id,N'IsProcedure') = 1)
DROP PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktGet
GO

IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}{objectQualifier}DnnUgDe_KontaktInsert') AND OBJECTPROPERTY(id,N'IsProcedure') = 1)
DROP PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktInsert
GO

IF  EXISTS (SELECT * FROM {databaseOwner}sysobjects WHERE id = OBJECT_ID(N'{databaseOwner}{objectQualifier}DnnUgDe_KontaktUpdate') AND OBJECTPROPERTY(id,N'IsProcedure') = 1)
DROP PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktUpdate
GO

-- Gespeicherte Prozeduren erzeugen

-- Alle Kontakte eines Moduls ausgeben
CREATE PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontakteGet
   @ModuleID int
AS
BEGIN
   SELECT
      ModuleID,
      ContactID,
      Firstname,
      Lastname,
      EmailAddress
   FROM
      {objectQualifier}DnnUgDe_Kontakte
   WHERE
      (ModuleID = @ModuleID)
   ORDER BY Lastname, Firstname
END
GO

-- Bestimmten Kontakt (nach ContactID) auswählen
CREATE PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktGet
   @ContactID int
AS
BEGIN
   SELECT
      ModuleID,
      ContactID,
      Firstname,
      Lastname,
      EmailAddress
   FROM
      {objectQualifier}DnnUgDe_Kontakte
   WHERE
      (ContactID = @ContactID)
   ORDER BY Lastname, Firstname
END
GO

-- Kontakt anlegen
CREATE PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktInsert
   @ModuleID int,
   @Firstname nvarchar(50),
   @Lastname nvarchar(50),
   @EmailAddress nvarchar(50)
AS
BEGIN
   INSERT INTO {objectQualifier}DnnUgDe_Kontakte (ModuleID, Firstname, Lastname, EmailAddress)
   VALUES (@ModuleID, @Firstname, @Lastname, @EmailAddress);
   SELECT SCOPE_IDENTITY()
END
GO

-- Kontakt aktualisieren
CREATE PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktUpdate
   @ContactID int,
   @Firstname nvarchar(50),
   @Lastname nvarchar(50),
   @EmailAddress nvarchar(50)
AS
BEGIN
   UPDATE {objectQualifier}DnnUgDe_Kontakte
   SET
      Firstname = @Firstname,
      Lastname = @Lastname,
      EmailAddress = @EmailAddress
   WHERE
      (ContactID = @ContactID)
END
GO

-- Kontakt löschen
CREATE PROCEDURE {databaseOwner}{objectQualifier}DnnUgDe_KontaktDelete
   @ContactID int
AS
BEGIN
   DELETE FROM {objectQualifier}DnnUgDe_Kontakte
   WHERE
      (ContactID = @ContactID)
END
GO

Die Prozeduren sind rasch erklärt:

  • DnnUgDe_ContactsGet liefert alle Kontakte eines gegebenen Moduls, geordnet nach Nachname und Vorname.
  • DnnUgDe_ContactGet liefert genau einen Kontakt (mit dem übergebenen Schlüssel KontaktID)
  • DnnUgDe_ContactInsert fügt einen neuen Kontakt ein und liefert den Schlüssel (KontaktID) des eingefügten Kontakts als Rückgabewert (SELECT SCOPE_IDENTITY()) – wichtig ist nur der Strichpunkt am Ende des INSERT-Statements. Das früher (bis zu SQL Server 7.0 und später leider selten ersetzte SELECT @@IDENTITY würde unter Umständen ein falsches Ergebnis liefern. Siehe dazu die Technische Dokumentation und den Blog-Eintrag des SQL-Teams.
  • DnnUgDe_KontaktUpdate aktualisiert einen Kontakt. Die ModuleID spielt dabei keine Rolle, sie wird nicht geändert und zur Identifizierung des Datensatzes dient die KontaktID.
  • Demo_KontakteDelete löscht einen Kontakt.

Damit hätten wir alles, was unsere Datenbank benötigt (zumindest vorläufig).

(PS: Ich sitze übrigens gerade im Zug von Graz nach Innsbruck und werde jetzt ein "Frankfurter" essen. Soviel zu meinem hier verwendeten sprachlichen Idiom :-) - wenn es also vielleicht irgendwas hier gibt, was unverständlich sein sollte, dann einfach in den Kommentar klopfen. Wie sagte schon Karl Kraus: Was Österreicher und Deutsche am stärksten trennt ist die gemeinsame Sprache... Ach ja, für alle, die nicht de-AT beherrschen: ein "Frankfurter" ist wohl ein "Wiener Würstchen".)

(PSS: Das übliche Programmierermenü Pizza und Tschick (=Zigaretten) steht nicht zur Verfügung. Pizza deswegen nicht, weil der Speisewagen das nicht führt, und seit einigen Monaten ist in österreichischen Zügen generelles Rauchverbot. Ich leide also vor mich hin, und muss mit einem Frankfurter vorlieb nehmen, und auf die Zigarette muss ich warten, bis der Zug in angekommen ist…)

(PSSS: Also, wer das alles schon weiß und dem beim Durchlesen meines Blogs fad (=langweilig) wird, der hat jetzt wenigstens die Gelegenheit, ein bisschen weiter entwickeltes Deutsch zu lernen ;-))

Source-Code kann hier heruntergeladen werden

Kommentare: 6

Dirk Marx meint

Ein tolles Blogthema, das mich dazu verführen könnte auch mal das eine oder andere DNN-Modul zu programmieren, anstatt sich alles zusammen zu suchen :)

Weiter so !
# 18.03.2008 22:03

Philipp Becker meint

Sehr schön geschrieben, danke! Weil ich manchmal ein Erbsenzähler bin, sei mir die Frage gestattet wieso der ; am Ende des Insert Stements wichtig sein soll - alles andere habe ich verstanden ;-)
# 19.03.2008 08:54

Michael Tobisch meint

Also bevor jemand anderer draufkommt: Die hier angeführte Prozedur zum Erstellen der Gespeicherten Prozeduren stimmt nicht - in den SQL-Skripts, die es zum Download gibt, sind sie richtig, also bitte nicht aus dem Artikel kopieren oder abtippen... Wenn es mir möglich ist, werde ich sie hier auch korrigieren, aber momentan kann ich veröffentlichte Blogs nicht mehr bearbeiten.
# 19.03.2008 18:53

Michael Tobisch meint

@Philipp: Vielleicht hast du recht... Ich habe das erste Mal, dass ich Kontakt mit ASP.Net 2.0 hatte so ein Skript erstellt ohne das Komma - in einem Typed Dataset als ad-hoc Statement - und das hat erst funktioniert, als ich das Komma eingefügt habe. Vielleicht ist es nicht notwendig, wenn man Gespeicherte Prozeduren verwendet, oder was anderes als Typed Datasets. Ich schreib' das Semikolon seither grundsätzlich (weil ich mir damals gedacht habe, das muss sein) - den genauen Sinn dahinter erkenne ich allerdings nicht. Versuch es ohne, und sag mir, ob es funktioniert...
# 19.03.2008 18:56

Michael Tobisch meint

@Dirk: Danke für das Lob!
# 19.03.2008 18:57

Michael Tobisch meint

Achtung, hier sind mir leider drei Fehler unterlaufen. Die korrigierten Versionen finden sich unter http://www.dnn-usergroup.de/Community/Blogs/tabid/80/articleType/ArticleView/articleId/97/Default.aspx
# 22.04.2008 19:08