{"id":55000489,"date":"2026-02-01T00:00:00","date_gmt":"2026-03-10T11:49:25","guid":{"rendered":"http:\/\/access-im-unternehmen.aix-dev.de\/aiu\/?p=489"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-30T00:00:00","slug":"Access_und_SQL_ServerFileTables","status":"publish","type":"post","link":"https:\/\/vbentwickler.de\/Access_und_SQL_ServerFileTables\/","title":{"rendered":"Access und SQL Server-FileTables"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/vg08.met.vgwort.de\/na\/fe95c33dad4f471780f092814fb0ecce\" width=\"1\" height=\"1\" alt=\"\"><b>Im Artikel &#8221; Access und SQL Server-FileTables&#8221; (www.vbentwickler.de\/489) haben wir gezeigt, wie man einer SQL Server-Datenbank eine sogenannte FileTable-Tabelle hinzuf&uuml;gt, in der man Dateien speichern kann, die gleichzeitig in einem vom SQL Server verwalteten Verzeichnis liegen. Im vorliegenden Artikel kommt nun Microsoft Access als Frontend ins Spiel, mit dem wir nicht nur die Dateien in der FileTable-Tabelle im SQL Server verwalten wollen, sondern wir m&ouml;chten diese am Beispiel von Bilddateien auch in Access-Formularen anzeigen. Letzteres ist leicht realisierbar, denn wir k&ouml;nnen dem Bild-Steuerelement einfach den Pfad zu der jeweiligen Datei in dem von SQL Server verwalteten Bereich des Dateisystems zuweisen. Etwas aufwendiger ist es, erst einmal &uuml;ber Access an diese Daten in der FileTable-Tabelle zu gelangen. Wie dies gelingt und wie wir die darin gespeicherten Dateien letztlich verwalten k&ouml;nnen, zeigen wir auf den folgenden Seiten.<\/b><\/p>\n<h2>Zugriff auf FileTables von Access &uuml;ber eine per ODBC verkn&uuml;pfte Tabelle<\/h2>\n<p>Erst einmal die schlechte Nachricht vorneweg: Ein Zugriff auf die <b>FileTable<\/b>-Tabelle als eingebundene Tabelle in Access ist nicht m&ouml;glich.<\/p>\n<p>Die Tabelle l&auml;sst sich zwar einbinden und wir k&ouml;nnen uns auch den Entwurf ansehen (siehe Bild 1).<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_001.png\" alt=\"Entwurfsansicht einer per ODBC eingebundenen FileTable-Tabelle\" width=\"649,627\" height=\"426,1945\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 1: Entwurfsansicht einer per ODBC eingebundenen FileTable-Tabelle<\/span><\/b><\/p>\n<p>Aber wir erhalten keinen Zugriff auf die Daten, sondern die Fehlermeldung  <b>ODBC-Aufruf fehlgeschlagen<\/b>, wenn wir versuchen, die Tabelle in der Datenblattansicht anzuzeigen (siehe Bild 2).<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_002.png\" alt=\"Versuch, die Tabellenverkn&uuml;pfung mit der FileTable-Tabelle in der Datenblattansicht zu &ouml;ffnen\" width=\"424,6267\" height=\"296,16\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 2: Versuch, die Tabellenverkn&uuml;pfung mit der FileTable-Tabelle in der Datenblattansicht zu &ouml;ffnen<\/span><\/b><\/p>\n<p>Woran liegt dies und wie kann man das &auml;ndern? Es sollte einen Weg geben, denn Microsoft propagiert ja nicht umsonst seit Jahren, dass man f&uuml;r den Zugriff auf SQL Server-Datenbanken ODBC und DAO nutzen soll.<\/p>\n<p>Mit ADODB k&ouml;nnen wir problemlos auf die Daten dieser Tabelle zugreifen.<\/p>\n<p>Aber nun wenden wir uns wieder der Frage zu, woran der Zugriff per ODBC-verkn&uuml;pfter Tabelle und per DAO scheitert.<\/p>\n<p>Das Problem wird durch die beiden Felder <b>path_locator <\/b>und <b>parent_path_locator <\/b>der <b>FileTable<\/b>-Tabelle ausgel&ouml;st. Diese haben den Datentyp <b>hierarchyid<\/b>, der interessanterweise eine Methode namens <b>ToString() <\/b>bereitstellt. Und siehe da &#8211; mit einer gespeicherten Prozedur gelingt auch der Zugriff per DAO. Diese legen wir wie folgt f&uuml;r die Datenbank mit der <b>FileTable<\/b>-Tabelle an:<\/p>\n<pre>CREATE PROC [dbo].[spFileTable_name]\r\nAS\r\nSELECT stream_id, file_stream, name,   path_locator.ToString() AS PathLocator, \r\n  parent_path_locator.ToString() AS ParentPathLocator,   file_type, is_directory FROM dbo.tblFiletable;<\/pre>\n<p>Danach legen wir eine neue Abfrage an, die wir in eine PassThrough-Abfrage umwandeln. In dieser legen wir den folgenden SQL-Befehl fest:<\/p>\n<pre>EXEC spFileTable_name<\/pre>\n<p>Au&szlig;erdem hinterlegen wir f&uuml;r die Eigenschaft <b>ODBC-Verbindung <\/b>die Verbindungszeichenfolge zum SQL Server, die beispielsweise wie in Bild 3 aussieht.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_015.png\" alt=\"PassThrough-Abfrage f&uuml;r den Zugriff auf eine gespeicherte Prozedur\" width=\"699,627\" height=\"408,9806\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 3: PassThrough-Abfrage f&uuml;r den Zugriff auf eine gespeicherte Prozedur<\/span><\/b><\/p>\n<pre>ODBC;DRIVER={ODBC Driver 18 for SQL Server};SERVER=AMVDESKTOP2023\\SQLEXPRESS;DATABASE=FileTableDB;Trusted_Connection=Yes;TrustServerCertificate=Yes;OPTION=3;LOG_QUERY=1;<\/pre>\n<p>Und siehe da: Die Pass-Through-Abfrage <b>pt_spFileTable <\/b>liefert alle gew&uuml;nschten Daten (siehe Bild 4), sogar die aus den Feldern <b>path_locator <\/b>und <b>parent_path_locator<\/b>.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_016.png\" alt=\"Ergebnis der PassThrough-Abfrage\" width=\"649,627\" height=\"377,2328\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 4: Ergebnis der PassThrough-Abfrage<\/span><\/b><\/p>\n<h2>Beispiel Produktbilder<\/h2>\n<p>In einer reinen Access-Anwendung w&uuml;rde man, um zu einem Produkt eines oder mehrere Bilder anzuzeigen, den Pfad zur Bilddatei an der entsprechenden Stelle unterbringen.<\/p>\n<p>Wenn es nur ein Produktbild geben soll, k&ouml;nnte man dieses direkt in die Tabelle <b>tblProdukte <\/b>integrieren, andernfalls w&uuml;rde man eine Tabelle namens <b>tblProduktbilder <\/b>hinzuf&uuml;gen, in der beispielsweise ein Feld namens <b>Bildbeschreibung <\/b>und ein weiteres namens <b>Bildpfad <\/b>gespeichert werden.<\/p>\n<p>Diese Tabelle, nennen wir sie <b>tblProduktbilder<\/b>, w&uuml;rde dann &uuml;ber ein Fremdschl&uuml;sselfeld namens <b>ProduktID <\/b>mit der Tabelle <b>tblProdukte <\/b>verkn&uuml;pft werden.<\/p>\n<p>Gehen wir also zun&auml;chst davon aus, dass wir eine sehr einfache Tabelle namens <b>tblProdukte <\/b>im SQL Server angelegt haben, deren Entwurf in Bild 5 zu sehen ist.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_017.png\" alt=\"Entwurf der Tabelle tblProdukte im SQL Server\" width=\"474,6267\" height=\"185,3836\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 5: Entwurf der Tabelle tblProdukte im SQL Server<\/span><\/b><\/p>\n<p>Au&szlig;erdem aktivieren wir f&uuml;r das Feld <b>ProduktID <\/b>den Autowert &uuml;ber die Eigenschaft <b>Identit&auml;tsspezifikation<\/b>, die wir unten in den Spalteneigenschaften dieses Feldes finden.<\/p>\n<h2>Zwei Beispiele: Ein Bild und mehrere Bilder je Produkt<\/h2>\n<p>In den folgenden Abschnitten schauen wir uns gleich beide Konstellationen an:<\/p>\n<ul>\n<li>jedes Produkt soll nur genau ein Produktbild erhalten und<\/li>\n<li>jedes Produkt soll ein, kein oder mehrere Produktbilder erhalten.<\/li>\n<\/ul>\n<h2>Beispiel: Ein Bild je Produkt<\/h2>\n<p>Als Erstes legen wir eine neue <b>FileTable<\/b>-Tabelle an, die <b>tblProduktbilder <\/b>hei&szlig;en soll.<\/p>\n<p>Dazu verwenden wir die folgende T-SQL-Anweisung, die zun&auml;chst die grundlegende <b>FileTable<\/b>-Tabelle erstellt und f&uuml;r diese das Verzeichnis <b>Produktbilder <\/b>zu dem Verzeichnis f&uuml;r die Dateien der <b>FileTable<\/b>-Tabellen des SQL Servers hinzuf&uuml;gt, das wir wie im Artikel  <b>Access und SQL Server-FileTables <\/b>(<b>www.vbentwickler.de\/489<\/b>) beschrieben angelegt haben:<\/p>\n<pre>CREATE TABLE dbo.tblProduktbilder\r\nAS FILETABLE\r\nWITH\r\n(\r\n    FILETABLE_DIRECTORY = N''Produktbilder'',\r\n    FILETABLE_COLLATE_FILENAME = database_default\r\n);<\/pre>\n<p>Leider k&ouml;nnen wir einer <b>FileTable<\/b>-Tabelle keine eigenen Spalten hinzuf&uuml;gen, sonst h&auml;tten wir wie &uuml;blich ein Prim&auml;rschl&uuml;sselfeld wie <b>ProduktID <\/b>und gegebenenfalls noch ein Feld wie Bildbeschreibung hinzuf&uuml;gen k&ouml;nnen.<\/p>\n<p>Stattdessen k&ouml;nnen wir aber auch das Prim&auml;rschl&uuml;sselfeld der <b>FileTable<\/b>-Tabelle verwenden, um die Tabelle <b>tblProdukte <\/b>&uuml;ber ein geeignetes Fremdschl&uuml;sselfeld damit zu verkn&uuml;pfen.<\/p>\n<p>Dazu f&uuml;gen wir der Tabelle <b>tblProdukte <\/b>mit der folgenden T-SQL-Anweisung im SQL Server Management Studio das Feld <b>ProduktbildID<\/b> hinzu:<\/p>\n<pre>ALTER TABLE dbo.tblProdukte\r\nADD ProduktbildID UNIQUEIDENTIFIER NULL;<\/pre>\n<p>Danach f&uuml;hren wir die folgende Anweisung aus, um eine Beziehung zwischen den beiden Tabellen herzustellen:<\/p>\n<pre>ALTER TABLE dbo.tblProdukte\r\nADD CONSTRAINT FK_tblProdukte_tblProduktbilder\r\n    FOREIGN KEY (ProduktbildID)\r\n    REFERENCES dbo.tblProduktbilder (stream_id);<\/pre>\n<p>Das Feld <b>ProduktbildID <\/b>der Tabelle <b>tblProdukte <\/b>verweist nun auf das Prim&auml;rschl&uuml;sselfeld <b>stream_id <\/b>der Tabelle <b>tblProduktbilder<\/b>.<\/p>\n<p>Nun erstellen wir eine gespeicherte Prozedur, die uns die Daten der Tabelle <b>tblProduktbilder <\/b>in den nachfolgend beschriebenen Feldern liefert:<\/p>\n<ul>\n<li><b>ProduktbildID<\/b>: enth&auml;lt den Wert des Feldes <b>stream_id<\/b><\/li>\n<li><b>Bildpfad<\/b>: enth&auml;lt den kompletten Pfad, der mit der SQL Server-Funktion <b>CONCAT(FileTableRootPath(), file_stream.GetFileNamespacePath()) <\/b>ermittelt wird.<\/li>\n<\/ul>\n<p>Die gespeicherte Prozedur erstellen wir mit dieser Anweisung (zum sp&auml;teren &Auml;ndern muss <b>CREATE <\/b>durch <b>ALTER <\/b>ersetzt werden):<\/p>\n<pre>CREATE PROC dbo.spProduktbilder\r\nAS\r\nSELECT stream_id AS ProduktbildID, \r\n    name<span style=\"color:blue;\"> As <\/span>Bildname,\r\n    CONCAT(FileTableRootPath(), file_stream.GetFileNamespacePath())<span style=\"color:blue;\"> As <\/span>Bildpfad\r\nFROM tblProduktbilder\r\nWHERE file_type = ''png'' AND is_directory = 0<\/pre>\n<p>Sie filtert au&szlig;erdem nach dem Wert <b>png <\/b>f&uuml;r das Feld <b>file_type <\/b>und liefert nur Dateien zur&uuml;ck (<b>is_directory = 0<\/b>).<\/p>\n<p>Nachdem wir dem Verzeichnis einige Beispielbilder hinzugef&uuml;gt haben, k&ouml;nnen wir die gespeicherte Prozedur mit <b>EXEC spProduktbilder <\/b>ausf&uuml;hren und erhalten das Ergebnis aus Bild 6.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_018.png\" alt=\"Gespeicherte Prozedur, die alle .png-Dateien liefert\" width=\"699,627\" height=\"265,78\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 6: Gespeicherte Prozedur, die alle .png-Dateien liefert<\/span><\/b><\/p>\n<h2>Gespeicherte Prozedur f&uuml;r Bilder per PassThrough-Abfrage in Access verf&uuml;gbar machen<\/h2>\n<p>Damit wechseln wir zur Access-Anwendung. Dieser f&uuml;gen wir eine Tabellenverkn&uuml;pfung zur SQL Server-Tabelle <b>tblProdukte <\/b>hinzu.<\/p>\n<p>Au&szlig;erdem legen wir eine PassThrough-Abfrage namens <b>pt_spProduktbilder <\/b>an, mit der wir die Daten der gespeicherten Prozedur <b>spProduktbilder <\/b>in Access verf&uuml;gbar machen wollen.<\/p>\n<p>&Ouml;ffnen wir diese PassThrough-Abfrage, sehen wir in der Datenblattansicht die gleichen Daten, welche die gespeicherte Prozedur im SQL Server Management Studio geliefert hat (siehe Bild 7).<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_019.png\" alt=\"PassThrough-Abfrage, die alle Daten der gespeicherten Prozedur spProduktbilder liefert\" width=\"700\" height=\"172,81\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 7: PassThrough-Abfrage, die alle Daten der gespeicherten Prozedur spProduktbilder liefert<\/span><\/b><\/p>\n<h2>Produkte und Produktbilder verwalten<\/h2>\n<p>Nun stellt sich die Frage, wie wir einem Produkt ein Bild hinzuf&uuml;gen k&ouml;nnen. Dies geschieht durch folgende Aktionen:<\/p>\n<ul>\n<li>Auswahl der gew&uuml;nschten Bilddatei<\/li>\n<li>Hinzuf&uuml;gen der Bilddatei zur Tabelle <b>tblProduktbilder<\/b><\/li>\n<li>Ermitteln des Prim&auml;rschl&uuml;sselwertes f&uuml;r den neuen Datensatz der Tabelle <b>tblProduktbilder<\/b><\/li>\n<li>Eintragen dieses Wertes in das Fremdschl&uuml;sselfeld <b>ProduktbildID <\/b>der Tabelle <b>tblProdukte<\/b><\/li>\n<\/ul>\n<h2>Auswahl der gew&uuml;nschten Bilddatei<\/h2>\n<p>Die Bilddatei, die wir der <b>FileTable<\/b>-Tabelle hinzuf&uuml;gen wollen, wollen wir per Dateiauswahl-Dialog ermitteln.<\/p>\n<p>Dazu nutzen wir die folgende Funktion, welche die <b>FileDialog<\/b>-Klasse der <b>Office<\/b>-Bibliothek nutzt. Diese Bibliothek m&uuml;ssen wir zun&auml;chst &uuml;ber den <b>Verweise<\/b>-Dialog des VBA-Editors zum Projekt hinzuf&uuml;gen. Die Bibliothek hei&szlig;t <b>Microsoft Office 16.0 Object Library<\/b>.<\/p>\n<p>Die Funktion zum Anzeigen und Auslesen des Dateidialogs legen wir wie folgt an:<\/p>\n<pre><span style=\"color:blue;\">Public Function <\/span>ChooseFolder()<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>objFileDialog<span style=\"color:blue;\"> As <\/span>Office.FileDialog\r\n    <span style=\"color:blue;\">Dim <\/span>strTemp<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Set<\/span> objFileDialog = _\r\n        Application.FileDialog( _\r\n        msoFileDialogFilePicker)\r\n    <span style=\"color:blue;\">With<\/span> objFileDialog\r\n        .Title = \"Datei ausw&auml;hlen\"\r\n        .ButtonName = \"Ausw&auml;hlen\"\r\n        .InitialFilename = CurrentProject.Path & \"\\\"\r\n        .Filters.Clear\r\n        .Filters.Add \"Bilddateien\", \"*.png\"\r\n        <span style=\"color:blue;\">If <\/span>.Show = <span style=\"color:blue;\">True<\/span><span style=\"color:blue;\"> Then<\/span>\r\n            strTemp = .SelectedItems(1)\r\n        <span style=\"color:blue;\">End If<\/span>\r\n    End <span style=\"color:blue;\">With<\/span>\r\n    ChooseFolder = strTemp\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<h2>Hinzuf&uuml;gen der Bilddatei zur Tabelle tblProduktbilder<\/h2>\n<p>Zum Hinzuf&uuml;gen eines Bildes zur Tabelle <b>tblProduktbilder<\/b> k&ouml;nnen wir zun&auml;chst die folgende T-SQL-Anweisung nutzen:<\/p>\n<pre>INSERT INTO dbo.tblProduktbilder (name, file_stream)\r\nOUTPUT inserted.stream_id\r\nSELECT ''pic001.png'', BulkColumn\r\nFROM OPENROWSET(\r\n  BULK C:\\pic001.png,\r\n  SINGLE_BLOB\r\n) AS FileData;<\/pre>\n<p>In dieser sind sowohl der Name des Bildes in der Tabelle <b>tblProduktbilder <\/b>als auch der Pfad, aus dem das Bild bezogen werden soll, fest verdrahtet.<\/p>\n<p>In der Praxis m&uuml;ssen wir diese Werte jedoch variabel einf&uuml;gen k&ouml;nnen. Wir m&uuml;ssen also eine M&ouml;glichkeit finden, wie wir dem SQL Server die notwendigen Daten &uuml;bergeben.<\/p>\n<p>Die erste Idee dazu ist, eine gespeicherte Prozedur anzulegen, welche die entsprechenden Parameter entgegennimmt und diesen Befehl ausf&uuml;hrt.<\/p>\n<p>Das ist aus verschiedenen Gr&uuml;nden problematisch, zum Beispiel weil wir den Pfad der einzuf&uuml;genden Datei in dieser Anweisung nicht einfach als Parameter einf&uuml;gen k&ouml;nnen (das ist eine technische Limitierung dieser speziellen Anweisung).<\/p>\n<p>Wir m&uuml;ssten die Anweisung als SQL-String zusammenstellen und sie dann mit <b>sp_executesql <\/b>ausf&uuml;hren.<\/p>\n<p>Wenn wir die Anweisung mit fest vorgegebenem Pfad in einer gespeicherten Prozedur angeben w&uuml;rden, k&ouml;nnten wir anschlie&szlig;end leicht den Wert des Prim&auml;rschl&uuml;sselfeldes f&uuml;r die Tabelle <b>tblProduktbilder <\/b>ermitteln (das Prim&auml;rschl&uuml;sselfeld in <b>FileTable<\/b>-Tabellen ist &uuml;brigens das Feld <b>path_locator<\/b>).<\/p>\n<p>Dazu k&ouml;nnten wir die folgende Abfrage nutzen:<\/p>\n<pre>SELECT SCOPE_IDENTITY() AS ID<\/pre>\n<p>Wenn wir die Anweisung allerdings als Parameter von <b>sp_executesql <\/b>ausf&uuml;hren, findet dies in einem anderen Kontext statt als der, auf den wir mit <b>SELECT SCOPE_IDENTITY <\/b>zugreifen k&ouml;nnen. Wir m&uuml;ssten also anderweitig ermitteln, welcher Datensatz soeben angelegt wurde.<\/p>\n<p>Dazu k&ouml;nnen wir zum Beispiel eine Abfrage verwenden, die den Datensatz mit einem Kriterium ermittelt, das den Inhalt des Feldes <b>name <\/b>des neuen Datensatzes enth&auml;lt.<\/p>\n<p>Und auch das ist nur eindeutig, wenn wir die <b>FileTable<\/b>-Tabelle nur zum einfachen Ablegen von Dateien nutzen, ohne Unterverzeichnisse zu verwenden &#8211; denn dann k&ouml;nnten wiederum mehrere Datens&auml;tze den gleichen Wert im Feld <b>name <\/b>enthalten, bei unterschiedlichem Wert im Feld <b>parent_path_locator<\/b>.<\/p>\n<h2>Wert des Feldes stream_id f&uuml;r den neuen Datensatz ermitteln<\/h2>\n<p>Vorausgesetzt, dass wir die Dateien nur im Hauptverzeichnis der <b>FileTable<\/b>-Tabelle ablegen, was in den meisten F&auml;llen kein Problem ist, k&ouml;nnen wir mit diesem Wissen auch gleich eine VBA-Funktion anlegen, welche die T-SQL-Anweisung zum Hinzuf&uuml;gen der Datei zur <b>FileTable<\/b>-Tabelle hinzuf&uuml;gt und aufgrund des Wertes im Feld name beispielsweise den Wert des Feldes <b>stream_id <\/b>des neu hinzugef&uuml;gten Datensatzes ermittelt.<\/p>\n<p>Diesen k&ouml;nnen wir dann nutzen, um die Tabelle tblProdukte mit dem entsprechenden Datensatz der Tabelle tblProduktbilder zu verkn&uuml;pfen.<\/p>\n<p>Also legen wir eine Funktion an, die eine entsprechende SQL-Anweisung zusammenstellt, diese in eine tempor&auml;re PassThrough-Abfrage schreibt, ein Recordset auf Basis dieser Abfrage erstellt und damit die ID des angelegten Datensatzes ermittelt.<\/p>\n<p>Diese Funktion finden wir in Listing 1. Die Funktion erwartet die beiden folgenden Parameter:<\/p>\n<pre><span style=\"color:blue;\">Public Function <\/span>ProduktbildEinfuegen_PT(ByVal strBildpfad<span style=\"color:blue;\"> As String<\/span>, ByVal strBildname<span style=\"color:blue;\"> As String<\/span>)<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Dim <\/span>qdf<span style=\"color:blue;\"> As <\/span>DAO.QueryDef\r\n    <span style=\"color:blue;\">Dim <\/span>rst<span style=\"color:blue;\"> As <\/span>DAO.Recordset\r\n    <span style=\"color:blue;\">Dim <\/span>strSQL<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strConnect<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strStreamID<span style=\"color:blue;\"> As String<\/span>\r\n    strConnect = \"ODBC;Driver=ODBC Driver 18 for SQL Server;Server=amvDesktop2023\\SQLEXPRESS;\" _\r\n        & \"Database=FileTableDB;Trusted_Connection=yes;Encrypt=no;\"\r\n    strSQL = \"INSERT INTO dbo.tblProduktbilder (name, file_stream)\" & <span style=\"color:blue;\">vbCrLf<\/span>\r\n    strSQL = strSQL & \"OUTPUT inserted.stream_id\" & <span style=\"color:blue;\">vbCrLf<\/span>\r\n    strSQL = strSQL & \"SELECT ''\" & strBildname & \"'', BulkColumn\" & <span style=\"color:blue;\">vbCrLf<\/span>\r\n    strSQL = strSQL & \"FROM OPENROWSET(\" & <span style=\"color:blue;\">vbCrLf<\/span>\r\n    strSQL = strSQL & \"  BULK ''\" & strBildpfad & \"'',\" & <span style=\"color:blue;\">vbCrLf<\/span>\r\n    strSQL = strSQL & \"  SINGLE_BLOB\" & <span style=\"color:blue;\">vbCrLf<\/span>\r\n    strSQL = strSQL & \") AS FileData;\"\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    <span style=\"color:blue;\">Set<\/span> qdf = db.CreateQueryDef(\"\")\r\n    <span style=\"color:blue;\">With<\/span> qdf\r\n        .Connect = strConnect\r\n        .ReturnsRecords = <span style=\"color:blue;\">True<\/span>\r\n        .SQL = strSQL\r\n        <span style=\"color:blue;\">Set<\/span> rst = .OpenRecordset(dbOpenSnapshot)\r\n    End <span style=\"color:blue;\">With<\/span>\r\n    <span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Not<\/span> rst Is Nothing<span style=\"color:blue;\"> Then<\/span>\r\n        <span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Not<\/span> rst.EOF<span style=\"color:blue;\"> Then<\/span>\r\n            strStreamID = Nz(rst.Fields(0).Value, \"\")\r\n        <span style=\"color:blue;\">End If<\/span>\r\n        rst.Close\r\n    <span style=\"color:blue;\">End If<\/span>\r\n    <span style=\"color:blue;\">Set<\/span> rst = Nothing\r\n    <span style=\"color:blue;\">Set<\/span> qdf = Nothing\r\n    <span style=\"color:blue;\">Set<\/span> db = Nothing\r\n    ProduktbildEinfuegen_PT = strStreamID\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 1: Anlegen eines neuen Bildes in der Tabelle tblProduktbilder<\/span><\/b><\/p>\n<ul>\n<li><b>strBildpfad<\/b>: Pfad zu der einzuf&uuml;genden Datei<\/li>\n<li><b>strBildname<\/b>: Name, unter dem das Bild in der Tabelle <b>tblProduktbilder <\/b>gespeichert werden soll<\/li>\n<\/ul>\n<p>Die Prozedur stellt zun&auml;chst in der Variablen <b>strConnect <\/b>die Verbindungszeichenfolge f&uuml;r die SQL Server-Datenbank zusammen. Diese musst Du gegebenenfalls an Deine Konstellation anpassen.<\/p>\n<p>Danach stellt sie in der Variablen <b>strSQL <\/b>die auszuf&uuml;hrenden Anweisungen zusammen. Wenn wir vom Wert <b>c:\\pic001.png<\/b> f&uuml;r <b>strBildpfad <\/b>und <b>pic001.png <\/b>f&uuml;r <b>strBildname <\/b>ausgehen, w&uuml;rde der Inhalt von strSQL wie folgt aussehen:<\/p>\n<pre>INSERT INTO dbo.tblProduktbilder (name, file_stream)\r\nOUTPUT inserted.stream_id\r\nSELECT pic001.png, BulkColumn\r\nFROM OPENROWSET(\r\n  BULK C:\\pic001.png,\r\n  SINGLE_BLOB\r\n) AS FileData;<\/pre>\n<p>Danach holt die Funktion einen Verweis auf das aktuelle <b>Database<\/b>-Objekt in die Variable <b>db <\/b>und legt mit dessen Methode <b>CreateQueryDef<\/b> eine neue, tempor&auml;re Abfrage in der Variablen <b>qdf <\/b>an.<\/p>\n<p>F&uuml;r dieses <b>QueryDef<\/b>-Objekt stellt sie dann die Eigenschaft <b>Connect <\/b>auf die Verbindungszeichenfolge aus <b>strConnect <\/b>ein. Die Einstellung <b>True <\/b>f&uuml;r die Eigenschaft <b>ReturnsRecords <\/b>legt fest, dass die enthaltenen Anweisungen ein Ergebnis zur&uuml;ckliefern sollen.<\/p>\n<p>F&uuml;r die Eigenschaft <b>SQL <\/b>&uuml;bergeben wir die Anweisungen aus <b>strSQL<\/b>. Schlie&szlig;lich f&uuml;hren wir die Abfrage mit der <b>OpenRecordset<\/b>-Methode aus und referenzieren das resultierende Recordset mit der Variablen <b>rst<\/b>.<\/p>\n<p>Wenn das Recordset vorhanden und nicht leer ist, lesen wir daraus den Wert des ersten Feldes aus, den wir im Falle des Wertes <b>NULL <\/b>noch mit der <b>Nz<\/b>-Funktion durch eine leere Zeichenkette ersetzen. Das Ergebnis speichern wir in der Variablen <b>strStreamID<\/b>.<\/p>\n<p>Danach schlie&szlig;en wir das Recordset und leeren die verwendeten Objektvariablen. Die Funktion gibt den Wert aus <b>strStreamID <\/b>als Ergebnis zur&uuml;ck.<\/p>\n<p>Der testweise Aufruf dieser Funktion erfolgt so:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_ProduktbildEinfuegen_PT()\r\n    <span style=\"color:blue;\">Dim <\/span>strBildpfad<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strBildname<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strStreamID<span style=\"color:blue;\"> As String<\/span>\r\n    strBildname = \"pic001.png\"\r\n    strBildpfad = \"C:\\...\\pic001.png\"\r\n    strStreamID = _\r\n        ProduktbildEinfuegen_PT(strBildpfad, strBildname)\r\n    <span style=\"color:blue;\">Debug.Print<\/span> \"Neue Stream_ID: \" & strStreamID\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Die Prozedur f&uuml;llt die Parameter <b>strBildpfad <\/b>und <b>strBildname <\/b>mit den Testwerten und ruft damit die Funktion auf. Das Ergebnis wird direkt der Variablen <b>strStreamID <\/b>zugewiesen, deren Inhalt wir am Ende mit <b>Debug.Print <\/b>im Direktbereich des VBA-Editors ausgeben.<\/p>\n<p>Rufen wir diese Prozedur einmalig auf, legt diese erfolgreich einen Datensatz in der Tabelle <b>tblProduktbilder <\/b>an. Beim erneuten Aufruf mit den gleichen Parametern erhalten wir allerdings den Fehler <b>ODBC-Aufruf fehlgeschlagen<\/b> in der folgenden Zeile:<\/p>\n<pre><span style=\"color:blue;\">Set<\/span> rst = .OpenRecordset(dbOpenSnapshot)<\/pre>\n<p>Wir fassen diese Zeile in eine Fehlerbehandlung ein, um den genauen Fehler zu ermitteln:<\/p>\n<pre>On Error Resume <span style=\"color:blue;\">Next<\/span>\r\n<span style=\"color:blue;\">Set<\/span> rst = .OpenRecordset(dbOpenSnapshot)\r\n<span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Not<\/span> Err.Number = 0<span style=\"color:blue;\"> Then<\/span>\r\n    <span style=\"color:blue;\">Debug.Print<\/span> Errors(0).Description\r\n<span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">On Error GoTo<\/span> 0<\/pre>\n<p>Der Fehler lautet:<\/p>\n<pre>[Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Verletzung der UNIQUE KEY-Einschr&auml;nkung \"UQ__tblProdu__A236CBB37972D295\". Ein doppelter Schl&uuml;ssel kann in das dbo.tblProduktbilder-Objekt nicht eingef&uuml;gt werden. Der doppelte Schl&uuml;sselwert ist (&lt;NULL&gt;, pic001.png).<\/pre>\n<p>Logisch: Wir versuchen, erneut einen Datensatz einzuf&uuml;gen, dessen Dateiname im eindeutigen Feld <b>name  <\/b>bereits vorhanden ist.<\/p>\n<p>Also f&uuml;gen wir direkt hinter dem Deklarationsbereich der Funktion noch eine Bedingung ein, die pr&uuml;ft, ob bereits ein Datensatz mit dem angegebenen Bildnamen in der Tabelle <b>tblProduktbilder <\/b>vorhanden ist, und den Benutzer fragt, ob das vorhandene Bild gel&ouml;scht werden soll. <\/p>\n<p>Dazu erstellen wir im SQL Server zun&auml;chst eine gespeicherte Prozedur, die das L&ouml;schen des Bildes durchf&uuml;hrt und den Wert des Feldes <b>name<\/b> als Parameter erwartet.<\/p>\n<p>Die gespeicherte Prozedur legen wir wie folgt an:<\/p>\n<pre>CREATE PROC dbo.spDELETEProduktBild\r\n(\r\n    @Bildname   varchar(50)\r\n)\r\nAS\r\nBEGIN\r\n    SET NOCOUNT ON;\r\n    DECLARE @stream_id uniqueidentifier;\r\n    DECLARE @rows_deleted int;\r\n    SELECT @stream_id = stream_id \r\n    FROM dbo.tblProduktbilder \r\n    WHERE name = @Bildname;\r\n    UPDATE dbo.tblProdukte\r\n    SET ProduktbildID = NULL\r\n    WHERE ProduktbildID = @stream_id;\r\n    DELETE FROM dbo.tblProduktbilder\r\n    WHERE stream_id = @stream_id;\r\n    SET @rows_deleted = @@ROWCOUNT;\r\n    RETURN @rows_deleted;\r\nEND;<\/pre>\n<p>Die gespeicherte Prozedur ermittelt zun&auml;chst den Wert des Feldes <b>stream_id <\/b>des Datensatzes der Tabelle <b>tblProduktbilder <\/b>f&uuml;r den mit dem Parameter <b>@Bildname <\/b>&uuml;bergebenen Dateinamen und speichert diesen in der Variablen <b>@stream_id<\/b>.<\/p>\n<p>Dann aktualisiert sie den Datensatz der Tabelle <b>tblProdukte<\/b>, dem dieser Wert von <b>@stream_id <\/b>im Feld <b>ProduktbildID <\/b>zugeordnet ist, indem sie den Wert dieses Feldes auf <b>NULL <\/b>einstellt.<\/p>\n<p>Wir erinnern uns: Weiter oben haben wir f&uuml;r die Tabellen <b>tblProdukte <\/b>und <b>tblProduktbilder <\/b>eine Beziehung mit referenzieller Integrit&auml;t definiert. Wenn wir einen Datensatz der Tabelle <b>tblProduktbilder <\/b>einem Datensatz der Tabelle <b>tblProdukte <\/b>zugeordnet haben, k&ouml;nnen wir diesen also nicht einfach l&ouml;schen. Daher m&uuml;ssen wir die Beziehung vor dem L&ouml;schen zun&auml;chst aufheben, was wir erledigen, indem wir den Wert des Fremdschl&uuml;sselfeldes <b>ProduktbildID <\/b>der Tabelle <b>tblProdukte <\/b>leeren. Dann l&ouml;scht die gespeicherte Prozedur den entsprechenden Datensatz aus der Tabelle <b>tblProduktbilder <\/b>und gibt die Anzahl der gel&ouml;schten Datens&auml;tze mit dem Parameter <b>@rows_deleted <\/b>zur&uuml;ck.<\/p>\n<p>In der Funktion <b>ProduktbildEinfuegen_PT <\/b>f&uuml;gen wir direkt hinter den Deklarationszeilen nun Folgendes hinzu:<\/p>\n<pre>...\r\nIf <span style=\"color:blue;\">Not<\/span> IsNull(DLookup(\"ProduktbildID\", _\r\n        \"pt_spProduktbilder\", _\r\n        \"Bildname=''\" & strBildname & \"''\")) Then\r\n    If <span style=\"color:blue;\">MsgBox<\/span>(\"Der Bildname ''\" & strBildname _\r\n            & \"'' ist bereits vorhanden. Soll die Datei\" _\r\n            & \" &uuml;berschrieben werden?\", _\r\n            vbYesNo + vbExclamation, \"Datei vorhanden\") _\r\n            = vbYes Then\r\n        <span style=\"color:blue;\">If <\/span>BildLoeschen(strBildname) &gt; 0<span style=\"color:blue;\"> Then<\/span>\r\n            <span style=\"color:blue;\">MsgBox<\/span> \"Das Bild wurde gel&ouml;scht und wird \" _\r\n                & \"nun neu angelegt.\"\r\n           <span style=\"color:blue;\">Else<\/span>\r\n                <span style=\"color:blue;\">MsgBox<\/span> \"Das Bild konnte nicht gel&ouml;scht \" _\r\n                    & \"werden.\"\r\n                <span style=\"color:blue;\">Exit Function<\/span>\r\n        <span style=\"color:blue;\">End If<\/span>\r\n    <span style=\"color:blue;\">Else<\/span>\r\n        <span style=\"color:blue;\">MsgBox<\/span> \"Das Bild f&uuml;r das Produkt wurde nicht \" _\r\n           & \"gespeichert.\"\r\n        <span style=\"color:blue;\">Exit Function<\/span>\r\n    <span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">End If<\/span>\r\n...<\/pre>\n<p>Die <b>DLookup<\/b>-Funktion nutzt die PassThrough-Abfrage, die wir bereits weiter oben angelegt haben, um die Bilder samt Pfad und ID auszulesen, und liefert die ID eines Datensatzes zur&uuml;ck, der im Feld <b>Bildname <\/b>den Wert aus <b>strBildname <\/b>enth&auml;lt.<\/p>\n<p>Ist das Ergebnis nicht <b>Null<\/b>, ruft die Funktion eine weitere Funktion namens <b>BildLoeschen <\/b>auf und &uuml;bergibt dieser den Bildnamen aus <b>strBild<\/b>.<\/p>\n<p>Liefert diese einen Wert gr&ouml;&szlig;er <b>0<\/b> zur&uuml;ck, wurde die Datei erfolgreich gel&ouml;scht und wir k&ouml;nnen die Datei neu anlegen. Anderenfalls wird die Funktion mit einer entsprechenden Meldung verlassen.<\/p>\n<p>Die Funktion zum L&ouml;schen des Bildes ist wie in Listing 2 aufgebaut. Sie erwartet den Bildnamen als Parameter und erstellt &auml;hnlich wie die Funktion <b>ProduktbildEinfuegen_PT <\/b>eine tempor&auml;re PassThrough-Abfrage, dieses Mal aber mit der gespeicherten Prozedur  <b>spDELETEProduktbild<\/b>.<\/p>\n<pre><span style=\"color:blue;\">Public Function <\/span>BildLoeschen(strBildname<span style=\"color:blue;\"> As String<\/span>)<span style=\"color:blue;\"> As Long<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>cnn<span style=\"color:blue;\"> As <\/span>ADODB.Connection\r\n    <span style=\"color:blue;\">Dim <\/span>cmd<span style=\"color:blue;\"> As <\/span>ADODB.Command\r\n    <span style=\"color:blue;\">Dim <\/span>prmReturn<span style=\"color:blue;\"> As <\/span>ADODB.Parameter\r\n    <span style=\"color:blue;\">Dim <\/span>strConnection<span style=\"color:blue;\"> As String<\/span>\r\n    \r\n    <span style=\"color:blue;\">On Error GoTo<\/span> Err_Handler\r\n    strConnection = \"Provider=MSOLEDBSQL;Server=amvDesktop2023\\SQLEXPRESS;Database=FileTableDB;Trusted_Connection=Yes;\"\r\n    <span style=\"color:blue;\">Set<\/span> cnn = <span style=\"color:blue;\">New<\/span> ADODB.Connection\r\n    cnn.Open strConnection\r\n    <span style=\"color:blue;\">Set<\/span> cmd = <span style=\"color:blue;\">New<\/span> ADODB.Command\r\n    cmd.ActiveConnection = cnn\r\n    cmd.CommandType = adCmdStoredProc\r\n    cmd.CommandText = \"dbo.spDELETEProduktBild\"\r\n    <span style=\"color:blue;\">Set<\/span> prmReturn = cmd.CreateParameter(\"ReturnValue\", adInteger, adParamReturnValue)\r\n    cmd.Parameters.Append prmReturn\r\n    cmd.Parameters.Append cmd.CreateParameter(\"@Bildname\", adVarChar, adParamInput, 50, strBildname)\r\n    cmd.Execute\r\n    BildLoeschen = prmReturn.Value\r\nExit_Handler:\r\n    On Error Resume <span style=\"color:blue;\">Next<\/span>\r\n    <span style=\"color:blue;\">Set<\/span> cmd = Nothing\r\n    <span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Not<\/span> cnn Is Nothing<span style=\"color:blue;\"> Then<\/span> cnn.Close: <span style=\"color:blue;\">Set<\/span> cnn = Nothing\r\n    <span style=\"color:blue;\">Exit Function<\/span>\r\nErr_Handler:\r\n    <span style=\"color:blue;\">Debug.Print<\/span> \"Fehler: \"; Err.Number; Err.Description\r\n    Resume Exit_Handler\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 2: Funktion zum L&ouml;schen eines Eintrags der Tabelle tblProduktbilder<\/span><\/b><\/p>\n<p>Au&szlig;erdem wird die gespeicherte Prozedur hier nicht mit <b>OpenRecordset <\/b>ge&ouml;ffnet, sondern sie wird mit <b>Execute <\/b>ausgef&uuml;hrt.<\/p>\n<p>Die Funktion legt zun&auml;chst wieder die Verbindungszeichenfolge fest und &ouml;ffnet die Connection.<\/p>\n<p>Dann erstellt sie ein neues <b>ADODB.Command<\/b>-Objekt, weist die Verbindungszeichenfolge zu und legt fest, dass die gespeicherte Prozedur <b>spDELETEProduktbild <\/b>ausgef&uuml;hrt werden soll.<\/p>\n<p>Dann legt sie zuerst den R&uuml;ckgabeparameter <b>ReturnValue <\/b>als <b>Integer <\/b>fest und h&auml;ngt diesen an die <b>Parameters<\/b>-Auflistung des <b>Command<\/b>-Objekts an.<\/p>\n<p>Au&szlig;erdem definiert sie den Parameter <b>@Bildname<\/b>, der zur &Uuml;bergabe des Namens des zu l&ouml;schenden Bildes verwendet werden soll, und weist den Wert aus <b>strBildname <\/b>zu.<\/p>\n<p>Danach f&uuml;hrt sie das <b>Command <\/b>mit der <b>Execute<\/b>-Methode aus und liest den Wert des R&uuml;ckgabeparameters <b>@ReturnValue <\/b>als Funktionsergebnis ein.<\/p>\n<p>Rufen wir diese Funktion nun mit der folgenden Anweisung auf, erhalten wir gleich die Anzahl der gel&ouml;schten Eintr&auml;ge als Ergebnis:<\/p>\n<pre><span style=\"color:blue;\">Debug.Print<\/span> BildLoeschen(\"test.png\")<\/pre>\n<h2>Produkte plus Produktbilder per Formular anlegen<\/h2>\n<p>Diesen Prozess wollen wir direkt in einem Formular abbilden, das wir <b>frmProdukte <\/b>nennen.<\/p>\n<p>Hier w&uuml;rden wir bei Verwendung von Access-Tabellen eine Abfrage als Datensatzquelle des Formulars verwenden, die etwa wie die Abfrage <b>qryProdukteMitBildpfad <\/b>in Bild 8 aufgebaut ist und die genau die Daten liefert, die wir im Formular anzeigen wollen &#8211; n&auml;mlich die Daten der Tabelle <b>tblProdukte <\/b>und zus&auml;tzlich den Pfad zu der zu dem Produkt geh&ouml;renden Bilddatei aus der <b>FileTable<\/b>-Tabelle <b>tblProduktbilder<\/b>.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_020.png\" alt=\"Abfrage f&uuml;r Produkte mit Bildern\" width=\"599,6265\" height=\"403,7198\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 8: Abfrage f&uuml;r Produkte mit Bildern<\/span><\/b><\/p>\n<p>Allerdings verwenden wir hier als eine der zugrunde liegenden Quellen eine PassThrough-Abfrage auf Basis der gespeicherten Prozedur <b>spProduktbilder<\/b>, die wir bereits weiter oben erstellt haben. W&uuml;rde die Abfrage auf reinen Access-Tabellen bestehen, k&ouml;nnten wir diese sogar noch bearbeiten.<\/p>\n<p>Die Ergebnisse einer PassThrough-Abfrage sind aber grunds&auml;tzlich nicht editierbar. Deshalb m&uuml;ssen wir hier einen etwas komplizierteren Weg als bei der Verwendung mit reinen Access-Tabellen einschlagen.<\/p>\n<p>Dabei binden wir das Formular &uuml;ber die Eigenschaft <b>Datensatzquelle <\/b>einfach nur an die Tabelle <b>tblProdukte<\/b>. Aus dieser ziehen wir die Felder <b>ProduktID<\/b>, <b>Produkt <\/b>und <b>Einzelpreis <\/b>in den Formularentwurf. Zus&auml;tzlich f&uuml;gen wir ein Textfeld namens <b>txtBildpfad <\/b>und ein Bild-Steuerelement namens <b>picProdukt <\/b>hinzu sowie eine Schaltfl&auml;che namens <b>cmdDateiauswahl<\/b>. <\/p>\n<p>Danach sieht der Formularentwurf wie in Bild 9 aus.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_021.png\" alt=\"Formular zur Anzeige von Produkten mit Produktbild\" width=\"649,627\" height=\"402,7686\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 9: Formular zur Anzeige von Produkten mit Produktbild<\/span><\/b><\/p>\n<p>Nun ben&ouml;tigen wir aber noch den Pfad des Bildes, um diesen im Textfeld <b>txtBildpfad<\/b> und im Bild-Steuerelement <b>picProdukt <\/b>anzuzeigen. Diesen ermitteln wir mit der Abfrage aus Bild 10.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_025.png\" alt=\"Abfrage der Produktbilder\" width=\"499,6267\" height=\"403,3498\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 10: Abfrage der Produktbilder<\/span><\/b><\/p>\n<p>Die Abfrage ist &auml;hnlich wie die Abfrage aufgebaut, die wir eigentlich als Datensatzquelle f&uuml;r das Formular nutzen wollten, aber sie enth&auml;lt nur noch die notwendigsten Felder, n&auml;mlich <b>ProduktID <\/b>und <b>Bildpfad<\/b>. Die <b>ProduktID <\/b>ben&ouml;tigen wir, um den passenden Datensatz zum aktuell angezeigten Produkt zu ermitteln. Das Feld <b>Bildpfad <\/b>aus der gespeicherten Prozedur <b>pt_spProduktbilder <\/b>liefert uns den Pfad der anzuzeigenden Bilddatei.<\/p>\n<p>Damit das Formular jeweils das Bild zum aktuellen Produkt ermittelt, ben&ouml;tigen wir eine Prozedur, die beim Anzeigen eines jeden Datensatzes ausgel&ouml;st wird. Dazu nutzen wir das Ereignis <b>Beim Anzeigen<\/b>, f&uuml;r das wir die Ereignisprozedur aus Listing 3 hinterlegen.<\/p>\n<pre><span style=\"color:blue;\">Private Sub <\/span>Form_Current()\r\n    <span style=\"color:blue;\">Dim <\/span>strBildpfadFileTable<span style=\"color:blue;\"> As String<\/span>\r\n    strBildpfadFileTable = Nz(DLookup(\"Bildpfad\", \"qryProduktbilder\", \"ProduktID = \" & Nz(Me.ProduktID, 0)), \"\")\r\n    Me.txtBildpfad = strBildpfadFileTable\r\n    Me.picProdukt.Picture = strBildpfadFileTable\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 3: Laden und Zuweisen des Bildpfads<\/span><\/b><\/p>\n<p>Diese ermittelt per <b>DLookup <\/b>den Bildpfad aus der Abfrage <b>qryProduktbilder<\/b>, dessen Feld <b>ProduktID <\/b>mit dem Wert des gleichnamigen Feldes des aktuell angezeigten Produkts &uuml;bereinstimmt, und schreibt diesen in die Eigenschaft <b>Steuerelementinhalt <\/b>der beiden Steuerelemente <b>txtBildpfad <\/b>und <b>picProdukt<\/b>.<\/p>\n<h2>Bild zu einem Produkt hinzuf&uuml;gen<\/h2>\n<p>Allerdings haben wir nun noch keine Bilder &uuml;ber das Formular hinzugef&uuml;gt. Das haben wir bisher nur testweise &uuml;ber die Prozedur <b>Test_ProduktbildEinfuegen_PT <\/b>erledigt, welche die Funktion <b>ProduktbildEinf&uuml;gen_PT<\/b> aufgerufen und damit die angegebene Bilddatei in der <b>FileTable<\/b>-Tabelle <b>tblProduktbilder <\/b>gespeichert hat.<\/p>\n<p>Nun wollen wir f&uuml;r die Schaltfl&auml;che <b>cmdDateiauswahl <\/b>eine Funktionalit&auml;t hinterlegen, die das gew&auml;hlte Bild anlegt, den Wert des Feldes <b>Stream_ID <\/b>des neuen Datensatzes in der <b>FileTable<\/b>-Tabelle <b>tblProduktbilder <\/b>ermittelt und diesen gleich dem Feld <b>ProduktbildID <\/b>des  aktuellen Datensatzes der Tabelle <b>tblProdukte <\/b>zuweist, damit die Verbindung zwischen den Datens&auml;tzen der Tabellen <b>tblProdukte <\/b>und <b>tblProduktbilder <\/b>hergestellt ist.<\/p>\n<p>Das erledigen wir mit der Ereignisprozedur, die durch das Ereignis <b>Beim Klicken <\/b>der Schaltfl&auml;che <b>cmdDateiauswahl<\/b> ausgel&ouml;st wird (siehe Listing 4).<\/p>\n<pre><span style=\"color:blue;\">Private Sub <\/span>cmdDateiauswahl_Click()\r\n    <span style=\"color:blue;\">Dim <\/span>strBildpfad<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strBildname<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strBildpfadFileTable<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>strStreamID<span style=\"color:blue;\"> As String<\/span>\r\n    strBildpfad = ChooseFolder\r\n    \r\n    strBildname = \"pic\" & Format(Me.ProduktID, \"000000\") & \".png\"\r\n    Me.Dirty = <span style=\"color:blue;\">False<\/span>\r\n    strStreamID = ProduktbildEinfuegen_PT(strBildpfad, strBildname)\r\n    strStreamID = <span style=\"color:blue;\">Replace<\/span>(strStreamID, \"{guid \", \"\")\r\n    strStreamID = <span style=\"color:blue;\">Replace<\/span>(strStreamID, \"}}\", \"}\")\r\n    \r\n    Me.ProduktbildID = strStreamID\r\n    \r\n    strBildpfadFileTable = DLookup(\"Bildpfad\", \"pt_spProduktbilder\", \"ProduktbildID = ''\" & strStreamID & \"''\")\r\n    \r\n    Me.txtBildpfad = strBildpfadFileTable\r\n    Me.picProdukt.Picture = Me.txtBildpfad\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 4: Bild speichern und mit dem aktuellen Datensatz verkn&uuml;pfen<\/span><\/b><\/p>\n<p>Diese Prozedur ruft die Funktion <b>ChooseFolder <\/b>auf, um den Dateiauswahl-Dialog anzuzeigen und den Pfad zum einzuf&uuml;genden Bild zur&uuml;ckzugeben. Dieser landet in der Variablen <b>strBildpfad<\/b>.<\/p>\n<p>Dann ermitteln wir einen Namen, unter dem wir das einzuf&uuml;gende Bild in der <b>FileTable<\/b>-Tabelle und damit auch in dem damit verbundenen Ordner speichern wollen.<\/p>\n<p>Damit dieser Name eindeutig ist, stellen wir diesen aus der Zeichenkette <b>pic<\/b> und dem Prim&auml;rschl&uuml;sselwert des aktuell angezeigten Produkts zusammen. Den Prim&auml;rschl&uuml;sselwert wollen wir dabei als sechsstellige Nummer mit f&uuml;hrenden Nullen darstellen, die wir mit <b>&#8220;pic&#8221; &#038; Format(Me.ProduktID, &#8220;000000&#8221;) &#038; &#8220;.png&#8221; <\/b>ermitteln.<\/p>\n<p>F&uuml;r den Datensatz mit dem Wert <b>2 <\/b>im Feld <b>ProduktID <\/b>wollen wir so beispielsweise <b>pic000002.png<\/b> nutzen.<\/p>\n<p>Das gelingt allerdings aktuell noch nicht: Wenn die Tabelle <b>tblProdukte <\/b>in der Access-Datenbank l&auml;ge, k&ouml;nnten wir direkt auf den per Autowert-Funktion vergebenen Prim&auml;rschl&uuml;sselwert im Feld <b>ProduktID <\/b>zugreifen.<\/p>\n<p>Die Tabelle <b>tblProdukte <\/b>liegt aber in einer SQL Server-Tabelle, wo der Automatismus zum Anlegen des Prim&auml;rschl&uuml;sselwertes erst beim Speichern des Datensatzes ausgel&ouml;st wird.<\/p>\n<p>Solange wir den Datensatz also nicht gespeichert haben, liefert das Feld <b>ProduktID <\/b>uns den Wert <b>Null<\/b>. Wir m&uuml;ssen den Datensatz also erst speichern, bevor wir auf den Prim&auml;rschl&uuml;sselwert zugreifen k&ouml;nnen. Das erledigen wir durch das Einstellen der Eigenschaft <b>Me.Dirty <\/b>auf den Wert <b>False<\/b>. Damit steht nun der Wert des Feldes <b>ProduktID <\/b>zur Verf&uuml;gung.<\/p>\n<p>Nun nutzen wir die bereits weiter oben vorgestellte Funktion <b>ProduktbildEinfuegen_PT<\/b>, um das Bild in der <b>FileTable<\/b>-Tabelle <b>tblProduktbilder <\/b>zu speichern und den Wert des Feldes <b>stream_id <\/b>f&uuml;r den neuen Datensatz zu ermitteln.<\/p>\n<p>Dieser wird in einem Format wie <b>{guid {39A19DC8-70D2-F011-AB51-005056C00008}}<\/b> geliefert.<\/p>\n<p>Wenn wir uns allerdings das Ergebnis der PassThrough-Abfrage <b>pt_spProduktbilder <\/b>ansehen, finden wir die ID in einem Format ohne das einschlie&szlig;ende <b>{guid&#8230;} <\/b>vor (siehe Bild 11). Damit die Beziehung richtig hergestellt werden kann, m&uuml;ssen wir das f&uuml;hrende <b>{guid<\/b> und die abschlie&szlig;ende geschweifte Klammer noch entfernen. Das erledigen die folgenden beiden Anweisungen:<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_026.png\" alt=\"Aussehen der Werte des Feldes ProduktbildID in der Passthrough-Abfrage in Access\" width=\"700\" height=\"132,8859\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 11: Aussehen der Werte des Feldes ProduktbildID in der Passthrough-Abfrage in Access<\/span><\/b><\/p>\n<pre>strStreamID = <span style=\"color:blue;\">Replace<\/span>(strStreamID, \"{guid \", \"\")\r\nstrStreamID = <span style=\"color:blue;\">Replace<\/span>(strStreamID, \"}}\", \"}\")<\/pre>\n<p>Danach schreiben wir den Wert von <b>strStreamID<\/b>, der nun wie folgt aussieht, in das Fremdschl&uuml;sselfeld <b>ProduktbildID <\/b>der Tabelle <b>tblProdukte<\/b>:<\/p>\n<pre>{39A19DC8-70D2-F011-AB51-005056C00008}<\/pre>\n<p>Der aktuell angezeigte Datensatz der Tabelle <b>tblProdukte <\/b>ist damit mit der richtigen GUID f&uuml;r das soeben angelegte Bild ausgestattet.<\/p>\n<p>Nun m&uuml;ssen wir noch den Pfad des gespeicherten Bildes ermitteln und als Steuerelementinhalt zu den Steuerelementen <b>txtBildpfad <\/b>und <b>picProdukt <\/b>hinzuf&uuml;gen. Diesen ermitteln wir mit der folgenden <b>DLookup<\/b>-Funktion:<\/p>\n<pre>strBildpfadFileTable = DLookup(\"Bildpfad\", _\r\n    \"pt_spProduktbilder\", _\r\n    \"ProduktbildID = ''\" & strStreamID & \"''\")<\/pre>\n<p>Danach weisen wir den Inhalt von <b>strBildpfadFileTable <\/b>der Eigenschaft <b>ControlSource <\/b>der beiden Steuerelemente zu:<\/p>\n<pre>Me.txtBildpfad = strBildpfadFileTable\r\nMe.picProdukt.Picture = Me.txtBildpfad<\/pre>\n<p>Das Ergebnis sehen wir in Bild 12.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_489_027.png\" alt=\"Produkt mit Produktbild im Formular frmProdukte\" width=\"524,6265\" height=\"298,037\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 12: Produkt mit Produktbild im Formular frmProdukte<\/span><\/b><\/p>\n<p>Nun legen wir einen zweiten Datensatz an und weisen auch diesem ein Bild zu, das direkt angezeigt wird. Wechseln wir zum ersten Datensatz zur&uuml;ck, erhalten wir wieder das Bild zum ersten Datensatz.<\/p>\n<h2>Nicht mehr ben&ouml;tigte Bilder l&ouml;schen<\/h2>\n<p>Das Speichern eines Bildes zu einem Produkt und seine Anzeige funktionieren nun also schon. Wir m&uuml;ssen uns nun noch darum k&uuml;mmern, dass beim L&ouml;schen eines Produkts auch das dazugeh&ouml;rige Bild direkt aus der <b>FileTable<\/b>-Tabelle <b>tblProduktbilder <\/b>entfernt wird.   <\/p>\n<p>Dazu ben&ouml;tigen wir zwei Dinge:<\/p>\n<ul>\n<li>eine gespeicherte Prozedur im SQL Server, die das Bild aus der Tabelle <b>tblProduktbilder <\/b>l&ouml;scht<\/li>\n<li>eine VBA-Prozedur, welche die gespeicherte Prozedur aufruft<\/li>\n<\/ul>\n<p>Die gespeicherte Prozedur erstellen wir in einer neuen Abfrage im SQL Server mit dem folgenden Skript:<\/p>\n<pre>CREATE PROCEDURE dbo.DeleteProduktbildByID\r\n    @ProduktbildID uniqueidentifier\r\nAS\r\nBEGIN\r\n    DELETE FROM dbo.tblProduktbilder WHERE stream_id = @ProduktbildID;\r\nEND<\/pre>\n<p>Die Abfrage erwartet die ID des zu l&ouml;schenden Bildes als Parameter. Sie f&uuml;hrt dann eine <b>DELETE<\/b>-Anweisung f&uuml;r den entsprechenden Datensatz aus. Die Abfrage k&ouml;nnen wir im SQL Server mit der folgenden Anweisung aufrufen (in einer Zeile):<\/p>\n<pre>EXEC DeleteProduktbildByID     ''5D83F455-73D2-F011-AB51-005056C00008''<\/pre>\n<p>Dies f&uuml;hrt zu einem Fehler, wenn wir versuchen, einen Datensatz aus der Tabelle <b>tblProduktbilder<\/b> zu l&ouml;schen, der noch mit einem Datensatz der Tabelle <b>tblProdukte <\/b>verkn&uuml;pft ist:<\/p>\n<pre>Nachricht 547, Stufe 16, Status 0, Prozedur DeleteProduktbildByID, Zeile 5 [Batchstartzeile 8]\r\nDie DELETE-Anweisung steht in Konflikt mit der REFERENCE-Einschr&auml;nkung \"FK_tblProdukte_tblProduktbilder\". Der Konflikt trat in der FileTableDB-Datenbank, Tabelle \"dbo.tblProdukte\", column ''ProduktbildID'' auf.<\/pre>\n<p>Dies m&uuml;ssen wir gleich beim Aufruf der gespeicherten Prozedur von Access aus ber&uuml;cksichtigen, damit der Benutzer eine verst&auml;ndliche Fehlermeldung erh&auml;lt.<\/p>\n<p>In Access legen wir die Prozedur aus Listing 5 an. Diese nimmt die ID des zu l&ouml;schenden Bildes entgegen. Dann pr&uuml;ft sie zun&auml;chst per <b>DLookup<\/b>-Funktion, ob es noch einen Datensatz in der Tabelle <b>tblProdukte <\/b>gibt, in dem dieses Bild referenziert wird. Falls dies der Fall ist, erscheint eine Meldung, dass das Bild noch an ein Produkt gebunden ist.<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>DeleteProduktbildByID(ByVal strProduktbildID<span style=\"color:blue;\"> As String<\/span>)\r\n    <span style=\"color:blue;\">Dim <\/span>cnn<span style=\"color:blue;\"> As <\/span>ADODB.Connection\r\n    <span style=\"color:blue;\">Dim <\/span>prm<span style=\"color:blue;\"> As <\/span>ADODB.Parameter\r\n    <span style=\"color:blue;\">Dim <\/span>cmd<span style=\"color:blue;\"> As <\/span>ADODB.Command\r\n    <span style=\"color:blue;\">Dim <\/span>strConnection<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>lngProduktID<span style=\"color:blue;\"> As Long<\/span>\r\n    \r\n    lngProduktID = Nz(DLookup(\"ProduktID\", \"tblProdukte\", \"ProduktbildID = ''\" & strProduktbildID & \"''\"), 0)\r\n    <span style=\"color:blue;\">If <\/span>lngProduktID = 0<span style=\"color:blue;\"> Then<\/span>\r\n        strConnection = \"Provider=MSOLEDBSQL;Server=amvDesktop2023\\SQLEXPRESS;Database=FileTableDB;\" _\r\n            & \"Trusted_Connection=Yes;\"\r\n        <span style=\"color:blue;\">Set<\/span> cnn = <span style=\"color:blue;\">New<\/span> ADODB.Connection\r\n        cnn.Open strConnection\r\n        <span style=\"color:blue;\">Set<\/span> cmd = <span style=\"color:blue;\">New<\/span> ADODB.Command\r\n        cmd.ActiveConnection = cnn\r\n        cmd.CommandText = \"dbo.DeleteProduktbildByID\"\r\n        cmd.CommandType = adCmdStoredProc\r\n        \r\n        <span style=\"color:blue;\">Set<\/span> prm = cmd.CreateParameter(\"@ProduktbildID\", adVarChar, adParamInput, 50, strProduktbildID)\r\n    \r\n        cmd.Parameters.Append prm\r\n        cmd.Execute\r\n    <span style=\"color:blue;\">Else<\/span>\r\n        <span style=\"color:blue;\">MsgBox<\/span> \"Das Bild kann nicht gel&ouml;scht werden, da es noch mit einem Produkt verkn&uuml;pft ist.\"\r\n    <span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 5: Bild l&ouml;schen<\/span><\/b><\/p>\n<p>Anderenfalls wird die Verbindungszeichenfolge in <b>strConnection <\/b>zusammengestellt und eine neue Connection auf Basis dieser Zeichenfolge erstellt. Dann erstellt die Prozedur ein neues <b>Command<\/b>-Objekt und weist diesem die Verbindung zu. Als <b>CommandText <\/b>verwenden wir <b>dbo.DeleteProduktbildByID <\/b>und stellen den Typ auf <b>adCmdStoredProc <\/b>ein.<\/p>\n<p>Au&szlig;erdem ben&ouml;tigen wir ein <b>Parameter<\/b>-Objekt, das wir mit der <b>CreateParameter<\/b>-Methode des <b>Command<\/b>-Objekts erstellen. Diesem weisen wir den Namen des Parameters, den Datentyp, die Art des Parameters (<b>adParamInput<\/b>), die L&auml;nge und den Wert aus <b>strProduktbildID <\/b>zu.<\/p>\n<p>Den Parameter h&auml;ngen wir mit der <b>Append<\/b>-Methode an das <b>Command<\/b>-Objekt an und f&uuml;hren dieses anschlie&szlig;end mit der <b>Execute<\/b>-Anweisung aus.<\/p>\n<h2>Zusammenfassung und Ausblick<\/h2>\n<p>Dieser Artikel zeigt, wie wir eine <b>FileTable<\/b>-Tabelle im SQL Server nutzen k&ouml;nnen, um darin Bilddateien zu speichern, die wir sonst beispielsweise in Anlagefeldern oder im Dateisystem gespeichert h&auml;tten.<\/p>\n<p>Es hat sich gezeigt, dass dies nicht allzu trivial ist: Wir m&uuml;ssen einige zus&auml;tzliche Schritte gegen&uuml;ber der reinen Access-L&ouml;sung gehen, denn allein das Anlegen der Bilder in der <b>FileTable<\/b>-Tabelle ist aufwendig. Ist dieses System allerdings einmal eingerichtet, kann man damit beliebig viele Dateien speichern, ohne sich Sorgen um die typischen Engstellen von Access wie die Begrenzung auf eine maximale Dateigr&ouml;&szlig;e einer Access-Datenbank machen zu m&uuml;ssen. Au&szlig;erdem sind die Bilder &uuml;ber das Dateisystem verf&uuml;gbar, werden aber gleichzeitig vom SQL Server verwaltet.<\/p>\n<h2>Downloads zu diesem Beitrag<\/h2>\n<p>Enthaltene Beispieldateien:<\/p>\n<p>SQLServerUndFiletable.accdb<\/p>\n<p><a href=\"..\/fileadmin\/beispiele\/727B6D89-DBCB-40BD-9A48-529EA3B087CE\/vbe_489.zip\">Download<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Im Artikel &#8221; Access und SQL Server-FileTables&#8221; (www.vbentwickler.de\/489) haben wir gezeigt, wie man einer SQL Server-Datenbank eine sogenannte FileTable-Tabelle hinzuf&uuml;gt, in der man Dateien speichern kann, die gleichzeitig in einem vom SQL Server verwalteten Verzeichnis liegen. Im vorliegenden Artikel kommt nun Microsoft Access als Frontend ins Spiel, mit dem wir nicht nur die Dateien in der FileTable-Tabelle im SQL Server verwalten wollen, sondern wir m&ouml;chten diese am Beispiel von Bilddateien auch in Access-Formularen anzeigen. Letzteres ist leicht realisierbar, denn wir k&ouml;nnen dem Bild-Steuerelement einfach den Pfad zu der jeweiligen Datei in dem von SQL Server verwalteten Bereich des Dateisystems zuweisen. Etwas aufwendiger ist es, erst einmal &uuml;ber Access an diese Daten in der FileTable-Tabelle zu gelangen. Wie dies gelingt und wie wir die darin gespeicherten Dateien letztlich verwalten k&ouml;nnen, zeigen wir auf den folgenden Seiten.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"footnotes":""},"categories":[66012026,662026,44000006],"tags":[],"yst_prominent_words":[],"class_list":["post-55000489","post","type-post","status-publish","format-standard","hentry","category-66012026","category-662026","category-SQL_Server_und_Co"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts\/55000489","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/comments?post=55000489"}],"version-history":[{"count":0,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts\/55000489\/revisions"}],"wp:attachment":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/media?parent=55000489"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/categories?post=55000489"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/tags?post=55000489"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/yst_prominent_words?post=55000489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}