Hey, Scripting Guy!Die Rückkehr des Datenbankskripts

Die Scripting Guys von Microsoft

Laden Sie den Code für diesen Artikel herunter: HeyScriptingGuy2008_04.exe (151KB)

Sie wissen ja, wenn die Scripting Guys eine Schwäche haben (mit Betonung auf „wenn“), dann ist es diese: Wir sorgen uns zu sehr um die möglichen Folgen unserer Aktionen bzw. Unterlassungen. Statt so zu handeln, wie es für uns am besten wäre, halten wir uns zurück und handeln so, wie wir denken, dass es für alle anderen am besten wäre. Wir tun dies ganz unabhängig davon, was diese Aktionen für uns bedeuten könnten.

Nehmen Sie zum Beispiel die Artikel zum Thema Datenbankskripterstellung. Um ehrlich zu sein, es fällt uns Scripting Guys schwer, Artikel über das Erstellen von Datenbankskripts zu verfassen. Das liegt nicht daran, dass das Erstellen von Datenbankskripts schwer wäre. Es ist eigentlich recht einfach. Das Problem ist vielmehr, dass die Scripting Guys nicht oft die Gelegenheit haben, mit Datenbanken zu arbeiten. Deshalb müssen wir jedes Mal, wenn wir über Datenbanken schreiben, alles genau überdenken. Wie Sie wahrscheinlich schon vor langer Zeit festgestellt haben, ist Denken definitiv keine Voraussetzung dafür, ein Scripting Guy zu werden.

Genau genommen, gäbe es wohl gar keine Scripting Guys, wenn sich einer von uns das am Anfang einmal genau überlegt hätte.

Wenn es also so schwer ist, Artikel über das Erstellen von Datenbankskripts zu schreiben, stellt sich die Frage, warum die Scripting Guys es trotzdem weiterhin tun. Die Antwort ist einfach: Wir sorgen uns darum, was mit allen anderen in der Welt wird, wenn wir damit aufhören, diese Artikel zu schreiben. Als Sir Arthur Conan Doyle entschied, Sherlock Holmes ein Ende zu machen (indem er ihn in der Kurzgeschichte „Das letzte Problem“ von den Klippen stürzen ließ), dachte er, dass er sich einfach einer Romanfigur entledigt, über die er nicht mehr schreiben wollte. Stattdessen ging ein großer Aufschrei der Empörung um die Welt. Einigen Berichten zufolge brachten Londoner Trauerbänder an ihren Hüten und Ärmeln an, als sie die Nachricht hörten. „Das letzte Problem“ wurde 1893 in The Strand Magazine veröffentlicht. Als Antwort darauf kündigten 20.000 Leser ihre Abonnements.

Autsch.

Die Scripting Guys sind besorgt, dass das Einstellen der Artikel zur Datenbankskripterstellung weltweit eine ähnliche Wirkung hätte. Wir wollen nicht so viel Kummer und Verzweiflung verursachen, und wir wollen sicher nicht, dass 20.000 von Ihnen ihr Abonnement für das TechNet Magazin kündigen. Deshalb möchten wir ankündigen, dass die Scripting Guys weiterhin Artikel über die Datenbankskripterstellung schreiben werden. Wann? Warum nicht sofort?

Hinzufügen von Datensätzen zu einer Datenbank

Wir dachten uns, dass wir diesen Monat einige raffinierte kleine Tricks für die Arbeit mit Datenbanken vorstellen. Das sind neue Tricks, die zu denen hinzukommen, die im Artikel vom September 2007 vorgestellt wurden (technetmagazine.com/issues/2007/09/HeyScriptingGuy). Nicht einmal die Scripting Guys würden sich erlauben, denselben Artikel zweimal zu veröffentlichen.

Es sei denn, wir wüssten, dass wir damit durchkommen.

Zunächst wird eine einfache Möglichkeit aufgezeigt, wie Sie einer Datenbank einen Datensatz hinzufügen können. Angenommen, es ist eine Datenbank namens „C:\Scripts\Inventory.mdb“ vorhanden, die eine Tabelle namens „Computers“ enthält. Die Tabelle „Computers“ enthält die folgenden Felder:

  • ComputerName
  • SerialNumber
  • UserName
  • Department

Wie fügen Sie dieser Datenbank einen neuen Computer hinzu? Betrachten Sie den Code in Abbildung 1.

Figure 1 Hinzufügen eines Datensatzes zur Tabelle „Computers“

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Inventory.mdb"

objRecordSet.Open "INSERT INTO Computers (ComputerName, SerialNumber, UserName, Department)" &  _
    "VALUES ('atl-ws-07', '11234', 'Ken Myer', 'Finance')", _
        objConnection, adOpenStatic, adLockOptimistic

Die ersten Zeilen dieses Skripts werden hier nicht im Detail besprochen. Wenn Sie Hintergrundinformationen benötigen, können Sie sich einige der Ressourcen ansehen, die im Skriptcenter unter microsoft.com/technet/scriptcenter aufgeführt werden. Es soll nur angemerkt werden, dass die Konstanten „adOpenStatic“ und „adLockOptimistic“ verwendet werden, um den Cursortyp und die Datensatzsperre für das Recordset zu steuern. (Das klingt zwar beeindruckend, ist aber sehr leicht zu bewerkstelligen.) Nach dem Erstellen von Instanzen der ADODB.Connection- und ADODB.Recordset-Objekte wird die Datenbank mit dem folgenden Befehl geöffnet:

objConnection.Open _
"Provider = Microsoft.Jet.OLEDB.4.0; " & _
"Data Source = C:\Scripts\Inventory.mdb"

Dieser Befehl öffnet übrigens eine Microsoft® Access® 2003-Datenbank. Eine Access 2007-Datenbank öffnen Sie mit diesem Befehl:

objConnection.Open _
"Provider = Microsoft.ACE.OLEDB.12.0; " & _
"Data Source = C:\Scripts\Inventory.accdb"

Auch wenn dies etwas vom Hauptthema ablenkt: Sie können einen Befehl wie etwa den folgenden verwenden, um eine SQL Server®-Datenbank zu öffnen:

objConnection.Open _
"Provider=SQLOLEDB;" & _
"Data Source=atl-sql-01;" & _
"Trusted_Connection=Yes;" & _ "InitialCatalog=Inventory;" & _
"User ID=fabrikam\kenmyer;Password=34DE6t4G!;"

Bisher handelt es sich bei allem, was aufgeführt wurde, um häufig verwendeten Standardcode. Wie Sie bald feststellen werden, beginnen alle Datenbankskripts mit denselben Codezeilen. Der Teil, der hervorgehoben werden soll, ist eigentlich diese Zeile:

objRecordSet.Open _
  "INSERT INTO Computers " & _
  "(ComputerName, SerialNumber, " & _
  "UserName, Department) " & _
    "VALUES ('atl-ws-07', 'A14B1234', " & _
      "'Ken Myer', 'Finance')", _
        objConnection, adOpenStatic, _
        adLockOptimistic

Hier wird die Abfrage INSERT INTO verwendet, um einen neuen Datensatz in die Tabelle „Computers“ einzufügen. Beachten Sie, dass nach dem Tabellennamen die Namen aller Felder folgen, für die Werte vorliegen, dass die Feldnamen mit Kommas getrennt werden und dass alle Namen in Klammern angegeben werden.

Sie fragen sich vielleicht: Was meinen die Scripting Guys mit „alle Felder, für die Werte vorliegen“? Gute Frage. Angenommen, dieser neue Computer wurde noch keiner Abteilung (Department) zugewiesen. Vorausgesetzt, die Datenbank ermöglicht die Eingabe eines Null-Werts im Feld „Department“, so muss der Abteilung also kein Wert zugewiesen werden, sie kann in der Liste weggelassen werden. Anders gesagt, könnte eine Abfrage etwa so geschrieben werden:

objRecordSet.Open _
  "INSERT INTO Computers " & _
  "(ComputerName, SerialNumber, " & _
  "UserName) " & _
    "VALUES ('atl-ws-07', 'A14B1234', " & _
      "'Ken Myer')", _
        objConnection, adOpenStatic, _
        adLockOptimistic

Was das Zuweisen von Werten angeht, sehen Sie sich an, was den Feldnamen folgt: das VALUES-Schlüsselwort, an das sich die Werte anschließen, die wir den einzelnen Feldern zuweisen möchten (ebenfalls in Klammern eingeschlossen). Bedenken Sie, dass die Feldnamen beim Auflisten in beliebiger Reihenfolge angegeben werden können. Zum Beispiel kommt ComputerName zuerst, auch wenn es sich nicht unbedingt um das erste Feld in der Datenbank handelt. Das ist bei Feldnamen angebracht, aber nicht so sehr bei Werten. Werte müssen in genau derselben Reihenfolge wie die Felder aufgelistet werden. Wenn das erste aufgelistete Feld „ComputerName“ ist, muss der erste Wert der Computername sein. Andernfalls treten später Probleme auf. (Zum Beispiel würde dann womöglich der Benutzername oder die Seriennummer dem Feld „ComputerName“ zugewiesen werden.)

Wie Sie sehen können, ist das Zuweisen von Werten nicht kompliziert. Stellen Sie nur sicher, dass Sie die Werte gemäß dem Datentyp formatieren: Daten und Zeichenfolgewerte müssen in einfache Anführungszeichen gesetzt werden. Numerische und boolesche Werte dürfen nicht in einfache Anführungszeichen gesetzt werden.

Wenn Sie einen Wert haben, der tatsächlich sein eigenes einfaches Anführungszeichen enthält (wie der Name O'Brien), müssen Sie das einfache Anführungszeichen durch Verdoppeln „hinüberretten“:

'O''Brien'

Seltsam, aber wahr.

Löschen von Datensätzen aus einer Datenbank

Das war schon recht interessant: Sie können einer Datenbank einen Datensatz allein durch Ausführen einer einzigen SQL-Abfrage hinzufügen. Gibt es etwas Besseres? Uns ist nichts bekannt.

Außer, dass es schon gut wäre, mithilfe einer einzigen Abfrage mehrere Datensätze aus einer Datenbank löschen zu können.

Angenommen, Ihr Unternehmen beschließt, die Personalabteilung abzubauen. (Nein, das ist kein Witz. Wir haben meist schon genügend Schwierigkeiten mit der Microsoft-Personalabteilung, so wie sie ist.) Wie können Sie alle Computer der Personalabteilung aus Ihrer Inventardatenbank löschen? In Abbildung 2 wird eine Möglichkeit dafür gezeigt.

Figure 2 Löschen mehrerer Datensätze

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Inventory.mdb"

objRecordSet.Open "DELETE * FROM Computers Where Department='Human Resources'", _
    objConnection, adOpenStatic, adLockOptimistic

Wie Sie sehen können, beginnt dieses Skript genau wie das Skript zum Hinzufügen eines neuen Datensatzes: mit dem Definieren einiger Konstanten, dem Erstellen einiger Objekte und dem anschließenden Herstellen der Verbindung mit der Inventory.mdb-Datenbank. Danach wird einfach eine DELETE-Abfrage verwendet, um alle Datensätze (*) aus der Tabelle „Computers“ zu löschen oder zumindest alle Datensätze zu löschen, bei denen das Feld „Department“ den Wert „Human Resources“ (Personalabteilung) besitzt:

objRecordSet.Open _
  "DELETE * FROM Computers " & _
   "Where Department='Human Resources'", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Es ist alles so einfach, wie das Stürzen von einer Klippe.

Nicht böse gemeint, Sherlock Holmes.

Aktualisieren von Datensätzen in einer Datenbank

Wem würde es keinen Spaß machen, alle Daten der Personalabteilung zu löschen? (Hinweis an die Microsoft-Personalabteilung: Wir meinen natürlich Spaß für andere und deren Personalabteilung. Nicht für uns. Uns würde das wirklich keine Freude bereiten.) Dennoch ist das Löschen aller Computer der Personalabteilung aus der Inventardatenbank möglicherweise nicht gerade das Beste, was Sie machen können. Warum nicht? Auch wenn die Mitarbeiter Ihrer Personalabteilung sich mit der gesamten Hardware davonstehlen, gehören die Computer nach wie vor Ihrem Unternehmen. Das heißt, dass die Computer dementsprechend in der Datenbank aufgelistet werden sollten. Also soll dies jetzt durchgeführt werden. Statt die Datensätze aus der Datenbank zu löschen, soll jeder Datensatz aktualisiert werden. Es wird nur der Abteilungsname für jeden Computer der Personalabteilung in „None“ (Kein) geändert. Kurz gesagt, es wird das Skript in Abbildung 3 ausgeführt.

Figure 3 Aktualisieren von Datensätzen

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Inventory.mdb"

objRecordSet.Open "UPDATE Computers Set Department = 'None' " & _
    "Where Department = 'Human Resources'", _
    objConnection, adOpenStatic, adLockOptimistic

Der einzige Unterschied bei diesem Skript ist die SQL-Abfrage. In diesem Fall verwenden Sie eine UPDATE-Abfrage, um die Datensätze in der Tabelle „Computers“ zu aktualisieren. Zusätzlich zum UPDATE-Schlüsselwort müssen zwei Parameter hinzugefügt werden, die der Abfrage mitteilen, wie und welche Datensätze aktualisiert werden sollen:

  • Set Department = 'None'. Dies gibt einfach den neuen Wert für das Feld „Department“ an. Sie können genau genommen mehr als einen Feldwert auf einmal festlegen, indem Sie eine Syntax wie die folgende verwenden: Set Department = 'None', UserName = 'None'. Wie Sie sehen können, werden dadurch sowohl das Feld „Department“ als auch das Feld „UserName“ auf „None“ gesetzt.
  • Where Department = 'Human Resources'. Dies ist eine standardmäßige Where-Klausel, die angibt, welche Datensätze aktualisiert werden sollen. In diesem Fall sind es alle Datensätze, bei denen das Feld „Department“ den Wert „Human Resources“ hat. Dies wird aus einem einfachen Grund erwähnt: Es sollen nicht alle Datensätze in der Datenbank, sondern nur die Datensätze (Computer) geändert werden, die früher zur Personalabteilung gehörten.

Hier ist etwas Interessantes, was Sie mit UPDATE-Abfragen durchführen können: Angenommen, Ihr Unternehmen beschließt, angesichts der Lebenshaltungskosten allen Mitarbeitern eine Gehaltserhöhung von 10 Prozent zu gewähren. (Die Scripting Guys und Sir Arthur Conan Doyle sind sich zumindest in einem Punkt sehr ähnlich: Sie schreiben über fiktive Welten.) Hier ist ein Skript, das eine Tabelle namens „Employees“ (Mitarbeiter) öffnet und das Feld „Salary“ (Gehalt) für jeden Mitarbeiter ändert. Welcher Wert wird dabei für das Feld „Salary“ festgelegt? Die Abfrage setzt dieses Feld auf das aktuelle Gehalt des Mitarbeiters multipliziert mit 1,1. Anders gesagt, sie gewährt jedem Mitarbeiter eine Erhöhung von 10 Prozent. (Wenn es nur so leicht wäre!) Hier ist die Abfrage:

objRecordSet.Open _
  "Update Employees " & _
  "Set Salary = (Salary * 1.1)", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Was daran so interessant ist? Wie Sie sehen, können Sie in Ihren Update-Abfragen Berechnungen anstellen. Sie haben beschlossen, den Vertrag Ihrer Zeitarbeiter um weitere 60 Tage zu verlängern? In diesem Fall dürfte eine Abfrage ähnlich der folgenden nützlich sein:

objRecordSet.Open _
  "Update TempEmployees " & _
  "Set ContractExpirationDate = " & _
  "(ContractExpirationDate + 60)", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Interessante Möglichkeiten zum Abrufen von Daten

Was nützliche Abfragen angeht, sollen abschließend noch einige interessante Möglichkeiten untersucht werden, um Daten aus einer Datenbank abzurufen. Hier ist z. B. ein einfaches, aber sehr nützliches Skript. Angenommen, die Inventardatenbank enthält ein Feld namens „Price“, ein Feld, das logischerweise den Preis des Computers angibt. Sie möchten wissen, welche Computer die fünf teuersten in Ihrer Organisation sind? Das erfahren Sie anhand des Beispielskripts in Abbildung 4.

Figure 4 Sortieren des Recordset

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
    "Provider = Microsoft.Jet.OLEDB.4.0; " & _
        "Data Source = C:\Scripts\Test.mdb" 

objRecordSet.Open "SELECT Top 5 * FROM Computers Order By Price", _
     objConnection, adOpenStatic, adLockOptimistic

objRecordSet.MoveFirst

Do Until objRecordset.EOF
    Wscript.Echo objRecordset.Fields.Item("ComputerName")
    objRecordset.MoveNext
Loop

objRecordSet.Close
objConnection.Close

Wie Sie sehen können, wurden in dieser SQL-Abfrage zwei Dinge durchgeführt: Zum einen wurde das Recordset nach Preis sortiert (Order By Price), und es wurden nur die ersten fünf (d. h. die fünf teuersten) Computer abgefragt. Das ist die Aufgabe von „SELECT Top 5“. Wenn Sie eine Liste der 10 teuersten Computer wünschen, würden Sie diese Abfrage verwenden:

objRecordSet.Open _
  "SELECT Top 10 * FROM Computers " & _
  "Order By Price", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Hinweis: Es wurde außerdem etwas Code hinzugefügt, der das Recordset durchläuft und die Namen der einzelnen Computer anzeigt. Dieser Code wurde in früheren Skripts nicht benötigt, weil die Skripts keine Daten zurückgegeben und angezeigt haben.

Alternativ können Sie die ersten 10 Prozent mithilfe dieser Abfrage abrufen:

objRecordSet.Open _
  "SELECT Top 10 PERCENT * " & _
  "FROM Computers Order By Price", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Was ist, wenn Sie die kostengünstigsten Computer abrufen möchten? Kein Problem. Dazu verwenden Sie einfach den gleichen Ansatz, außer dass diesmal das Recordset in absteigender Reihenfolge sortiert wird (d. h. vom niedrigsten zum höchsten Preis). Anders gesagt, diese Abfrage wird mit DESC verwendet, was angibt, dass das Recordset in absteigender Reihenfolge (descending order) sortiert werden muss:

objRecordSet.Open _
  "SELECT Top 5 * FROM Computers " & _
  "Order By Price DESC", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Alles klar? Wir haben ja gesagt, dass das alles nützliche Skripts sind.

Hier sind noch zwei, die für Sie vielleicht interessant sind. Angenommen, Ihre Datenbank enthält ein Feld namens „Budgeted“, mit dem der Geldbetrag nachverfolgt wird, der ursprünglich für den Computer eingeplant war. Sie möchten die Ist-Kosten mit dem Sollbetrag vergleichen? Diese Abfrage gibt eine Liste der Computer zurück, die mehr kosten als der geplante Betrag:

objRecordSet.Open _
  "SELECT * FROM Computers " & _
  "Where (Budgeted < Price)", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Währenddessen berechnet diese Abfrage den Durchschnittspreis aller Computer (SELECT AVG(Price) FROM Computers), gibt dann eine Liste aller Computer zurück, die unter dem Durchschnittspreis liegen:

objRecordSet.Open _
  "SELECT * FROM Computers " & _
  "WHERE Price < " & _
  "(SELECT AVG(Price) FROM Computers)", _
    objConnection, adOpenStatic, _
    adLockOptimistic

Sie haben Recht, es wurde eine SELECT-Abfrage in einer Where-Klausel verwendet. Wie funktioniert das? Das ist eine lange Geschichte. Doch wie Sie sehen können, eröffnet dies Möglichkeiten zum Abrufen aller Arten von interessanten Informationen mit Ihren SQL-Abfragen.

Die Moral der Geschichte

Wie am Anfang dieses Artikels für diesen Monat schon angemerkt, waren wir nicht sicher, ob wir überhaupt einen weiteren Artikel zur Datenbankskripterstellung schreiben wollten. Wir sind jedoch froh, dass wir es getan haben, zum Teil deshalb, weil wir denken, dass Sie einige dieser Abfragen für nützlich halten werden, aber auch aus folgendem Grund: Wenn wir diesen Artikel jetzt nicht verfasst hätten, müssten wir dies früher oder später nachholen. Denn letzten Endes wurde sogar Sir Arthur Conan Doyle dazu gezwungen, Sherlock Holmes wieder ins Leben zurückzurufen. Sir Arthur hat die ersten vorsichtigen Schritte in diese Richtung genommen, indem er eine neue Sherlock Holmes-Kurzgeschichte schrieb, die angeblich vor dem Tod des Detektivs spielte. Danach hat er allerdings einfach dem Druck seiner Leser nachgegeben und eine etwas zweifelhafte Geschichte erfunden, der zufolge Sherlock Holmes seinen eigenen Tod vorgetäuscht hatte. Letzten Endes waren alle glücklich, und damit war alles in bester Ordnung.

Interessanterweise abonnierten 30.000 neue Leser The Strand Magazine, als die neue Sherlock Holmes-Kurzgeschichte veröffentlicht wurde. Sir Arthurs dankbare Herausgeber in Großbritannien und den USA reagierten darauf, indem sie ihn zum höchstbezahlten Schriftsteller der Welt machten.

Hmmm. Sir Arthur Conan Doyle rief einen sehr beliebten Helden ins Leben zurück und wurde der höchstbezahlte Schriftsteller der Welt. Die Scripting Guys haben jetzt ein sehr beliebtes Thema wiederbelebt, und zwar ohne irgendwelche Tode vortäuschen zu müssen. Ob dies von den Leuten beim TechNet Magazin bemerkt wird?

Der Scripting Perplexer von Dr. Scripto

Die monatliche Herausforderung, die nicht nur Ihr Talent zum Rätsellösen testet, sondern auch Ihre Skriptingfähigkeiten.

April 2008: Wählen Sie einen Buchstaben

Im Rätsel dieses Monats müssen Sie die Buchstaben A bis O in die blauen Felder eintragen, um den Namen einer VBScript-Funktion zu erhalten. Jeder Buchstabe (A bis O) darf nur einmal verwendet werden, und die Buchstaben erscheinen nicht in alphabetischer Reihenfolge. Der eingefügte Buchstabe könnte der Anfang oder das Ende eines Funktionsnamens sein oder sich irgendwo in der Mitte eines Funktionsnamens befinden. In der folgenden Zeile würden Sie zum Beispiel den Buchstaben D eintragen, um den Funktionsnamen IsDate zu erhalten:

  (Klicken Sie zum Vergrößern auf das Bild)

Jetzt versuchen Sie es selbst. Fügen Sie die Buchstaben A bis O ein, um in diesem Raster VBScript-Funktionsnamen zu enthüllen:

**** (Klicken Sie zum Vergrößern auf das Bild)

ANSWER:

Der Scripting Perplexer von Dr. Scripto

Antwort: Wählen Sie einen Buchstaben, April 2008

  (Klicken Sie zum Vergrößern auf das Bild)

Die Scripting Guys von Microsoft arbeiten für Microsoft (oder sind zumindest dort angestellt). Wenn sie nicht gerade ihrem Hobby, dem Baseball (oder verschiedenen anderen Aktivitäten) nachgehen, betreiben sie das TechNet-Skriptcenter. Besuchen Sie es unter www.scriptingguys.com.

© 2008 Microsoft Corporation und CMP Media, LLC. Alle Rechte vorbehalten. Die nicht genehmigte teilweise oder vollständige Vervielfältigung ist nicht zulässig.