{"id":3522,"date":"2024-05-28T23:49:24","date_gmt":"2024-05-28T20:49:24","guid":{"rendered":"https:\/\/demensdeum.com\/blog\/?p=3522"},"modified":"2024-12-16T22:32:13","modified_gmt":"2024-12-16T19:32:13","slug":"crud-repository","status":"publish","type":"post","link":"https:\/\/demensdeum.com\/blog\/de\/2024\/05\/28\/crud-repository\/","title":{"rendered":"CRUD-Repository"},"content":{"rendered":"<p>In dieser Notiz beschreibe ich die Grundprinzipien des bekannten klassischen CRUD-Musters und die Implementierung in der Swift-Sprache. Swift ist eine offene, plattform\u00fcbergreifende Sprache, die f\u00fcr Windows, Linux, macOS, iOS und Android verf\u00fcgbar ist.<\/p>\n<p>Es gibt viele L\u00f6sungen zur Abstraktion von Datenspeicherung und Anwendungslogik. Eine solche L\u00f6sung ist der CRUD-Ansatz, ein Akronym f\u00fcr C&#8211; Erstellen, R -Lesen, U &#8211; Update, D&#8211; L\u00f6schen.<br \/>Typischerweise wird dieses Prinzip durch die Implementierung einer Schnittstelle zur Datenbank implementiert, in der Elemente mithilfe einer eindeutigen Kennung, beispielsweise einer ID, manipuliert werden. F\u00fcr jeden Buchstaben CRUD &#8211; wird eine Schnittstelle erstellt. Erstellen (Objekt, ID), Lesen (ID), Aktualisieren (Objekt, ID), L\u00f6schen (Objekt, ID).<br \/>Wenn ein Objekt eine ID in sich selbst enth\u00e4lt, kann das ID-Argument in den Methoden (Create, Update, Delete) weggelassen werden, da das gesamte Objekt zusammen mit seinem Feld &#8211; Ausweis. Aber f\u00fcr &#8211; F\u00fcr das Lesen ist eine ID erforderlich, da wir anhand der ID ein Objekt aus der Datenbank abrufen m\u00f6chten.<\/p>\n<p>Alle Namen sind fiktiv<\/p>\n<p>Stellen wir uns vor, dass eine hypothetische AssistantAI-Anwendung mit dem kostenlosen EtherRelm-Datenbank-SDK erstellt wurde. Die Integration war einfach, die API war sehr praktisch und als Ergebnis wurde die Anwendung auf den Markt gebracht.<br \/>Pl\u00f6tzlich beschlie\u00dft der SDK-Entwickler EtherRelm, es kostenpflichtig zu machen und legt den Preis auf 100 US-Dollar pro Jahr und Anwendungsbenutzer fest.<br \/>Was? Ja! Was sollen die Entwickler von AssistantAI jetzt tun, da sie bereits 1 Million aktive Benutzer haben! 100 Millionen US-Dollar zahlen?<br \/>Stattdessen wird beschlossen, die \u00dcbertragung der Anwendung in die plattformeigene RootData-Datenbank zu evaluieren. Nach Angaben der Programmierer wird eine solche \u00dcbertragung etwa sechs Monate dauern, wobei die Implementierung neuer Funktionen in der Anwendung nicht ber\u00fccksichtigt ist. Nach einigem \u00dcberlegen wurde beschlossen, die Anwendung vom Markt zu nehmen und sie auf einem anderen kostenlosen plattform\u00fcbergreifenden Framework mit integrierter BueMS-Datenbank neu zu schreiben. Dadurch wird das Problem mit der kostenpflichtigen Datenbank gel\u00f6st und die Entwicklung auf anderen Plattformen vereinfacht. <br \/>Ein Jahr sp\u00e4ter wurde die Anwendung in BueMS umgeschrieben, doch dann beschlie\u00dft der Framework-Entwickler pl\u00f6tzlich, sie kostenpflichtig zu machen. Es stellt sich heraus, dass das Team zweimal in die gleiche Falle getappt ist; ob es beim zweiten Mal wieder herauskommt, ist eine ganz andere Geschichte.<\/p>\n<p>Abstraktion zur Rettung<\/p>\n<p>Diese Probleme h\u00e4tten vermieden werden k\u00f6nnen, wenn Entwickler eine Abstraktion von Schnittstellen innerhalb der Anwendung verwendet h\u00e4tten. Zu den drei S\u00e4ulen von OOP &#8211; Polymorphismus, Kapselung, Vererbung, vor nicht allzu langer Zeit haben sie ein weiteres &#8211; Abstraktion.<br \/>Mit der Datenabstraktion k\u00f6nnen Sie Ideen und Modelle allgemein und mit einem Minimum an Details beschreiben und gleichzeitig genau genug sein, um spezifische Implementierungen zu implementieren, die zur L\u00f6sung von Gesch\u00e4ftsproblemen verwendet werden.<br \/>Wie k\u00f6nnen wir den Datenbankbetrieb abstrahieren, sodass die Anwendungslogik nicht davon abh\u00e4ngt? Wir verwenden den CRUD-Ansatz!<\/p>\n<p>Ein vereinfachtes UML CRUD-Diagramm sieht so aus:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/demensdeum.com\/blog\/wp-content\/uploads\/2024\/05\/image3.png\" alt=\"\" width=\"263\" height=\"118\" class=\"alignnone size-full wp-image-3537\" \/><\/p>\n<p>Beispiel mit einer fiktiven EtherRelm-Datenbank:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/demensdeum.com\/blog\/wp-content\/uploads\/2024\/05\/image2.png\" alt=\"\" width=\"272\" height=\"118\" class=\"alignnone size-full wp-image-3538\" \/><\/p>\n<p>Beispiel mit einer echten SQLite-Datenbank:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/demensdeum.com\/blog\/wp-content\/uploads\/2024\/05\/image1.png\" alt=\"\" width=\"243\" height=\"118\" class=\"alignnone size-full wp-image-3539\" \/><\/p>\n<p>Wie Sie bereits bemerkt haben, \u00e4ndert sich beim Wechseln der Datenbank nur die CRUD-Schnittstelle, mit der die Anwendung interagiert. CRUD ist eine Implementierung des GoF-Musters &#8211; Adapter, weil Damit passen wir Anwendungsschnittstellen an beliebige Datenbanken an und kombinieren inkompatible Schnittstellen.<br \/>Worte sind leer, zeig mir den Code<br \/>Um Abstraktionen in Programmiersprachen zu implementieren, werden Schnittstellen\/Protokolle\/abstrakte Klassen verwendet. All dies sind Ph\u00e4nomene der gleichen Art. In Interviews werden Sie jedoch m\u00f6glicherweise gebeten, den Unterschied zwischen ihnen zu benennen. Ich pers\u00f6nlich denke, dass dies nicht viel Sinn macht, weil Der einzige Verwendungszweck besteht darin, eine Datenabstraktion zu implementieren, andernfalls dient es dazu, das Ged\u00e4chtnis des Befragten zu testen.<br \/>CRUD wird oft im Rahmen des Repository-Musters implementiert. Das Repository kann jedoch die CRUD-Schnittstelle implementieren oder auch nicht, alles h\u00e4ngt vom Einfallsreichtum des Entwicklers ab.<\/p>\n<p>Stellen Sie sich einen ziemlich typischen Swift-Code f\u00fcr das Book-Struktur-Repository vor, der direkt mit der UserDefaults-Datenbank arbeitet:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\nstruct Book: Codable {\n\tlet title: String\n\tlet author: String\n}\n\nclass BookRepository {\n\tfunc save(book: Book) {\n    \t\tlet record = try! JSONEncoder().encode(book)\n    \t\tUserDefaults.standard.set(record, forKey: book.title)\n\t}\n    \n\tfunc get(bookWithTitle title: String) -&gt; Book? {\n    \t\tguard let data = UserDefaults.standard.data(forKey: title) else { return nil }\n    \t\tlet book = try! JSONDecoder().decode(Book.self, from: data)\n    \t\treturn book\n\t}\n    \n\tfunc delete(book: Book) {\n    \t\tUserDefaults.standard.removeObject(forKey: book.title)\n\t}\n}\n\nlet book = Book(title: \"Fear and Loathing in COBOL\", author: \"Sir Edsger ZX Spectrum\")\nlet repository = BookRepository()\nrepository.save(book: book)\nprint(repository.get(bookWithTitle: book.title)!)\nrepository.delete(book: book)\nguard repository.get(bookWithTitle: book.title) == nil else {\n\tprint(\"Error: can't delete Book from repository!\")\n\texit(1)\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Der obige Code scheint einfach zu sein, aber z\u00e4hlen wir die Anzahl der Verst\u00f6\u00dfe gegen das DRY-Prinzip (Do not Repeat Yourself) und die Koh\u00e4renz des Codes:<br \/>Konnektivit\u00e4t zur UserDefaults-Datenbank<br \/>Konnektivit\u00e4t mit JSON-Encodern und -Decodern &#8211; JSONEncoder, JSONDecoder<br \/>Verbunden mit der Book-Struktur, aber wir ben\u00f6tigen ein abstraktes Repository, um nicht f\u00fcr jede Struktur, die wir in der Datenbank speichern, eine Repository-Klasse zu erstellen (DRY-Verletzung)<\/p>\n<p>Ich sehe diese Art von CRUD-Repository-Code ziemlich oft, er kann verwendet werden, aber eine hohe Kopplung und Duplizierung des Codes f\u00fchrt dazu, dass seine Unterst\u00fctzung mit der Zeit sehr kompliziert wird. Dies macht sich besonders bemerkbar, wenn Sie versuchen, zu einer anderen Datenbank zu wechseln, oder wenn Sie die interne Logik der Arbeit mit der Datenbank in allen in der Anwendung erstellten Repositorys \u00e4ndern.<br \/>Anstatt Code zu duplizieren, halten Sie die Kopplung hoch &#8211; Schreiben wir ein Protokoll f\u00fcr das CRUD-Repository und abstrahieren so die Datenbankschnittstelle und die Anwendungsgesch\u00e4ftslogik unter Ber\u00fccksichtigung von DRY und implementieren eine geringe Kopplung:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>    typealias Item = Codable\n    typealias ItemIdentifier = String\n    \n    func create&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws\n    func read&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier) async throws -&gt; T\n    func update&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws\n    func delete(id: CRUDRepository.ItemIdentifier) async throws\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Das CRUDRepository-Protokoll beschreibt Schnittstellen und zugeh\u00f6rige Datentypen f\u00fcr die weitere Implementierung eines bestimmten CRUD-Repositorys.<\/p>\n<p>Als n\u00e4chstes schreiben wir eine spezifische Implementierung f\u00fcr die UserDefaults-Datenbank:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>    private typealias RecordIdentifier = String\n    \n    let tableName: String\n    let dataTransformer: DataTransformer\n    \n    init(\n   \t tableName: String = \"\",\n   \t dataTransformer: DataTransformer = JSONDataTransformer()\n    ) {\n   \t self.tableName = tableName\n   \t self.dataTransformer = dataTransformer\n    }\n    \n    private func key(id: CRUDRepository.ItemIdentifier) -&gt; RecordIdentifier {\n   \t \"database_\\(tableName)_item_\\(id)\"\n    }\n   \t \n    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -&gt; Bool {\n   \t UserDefaults.standard.data(forKey: key(id: id)) != nil\n    }\n    \n    func create&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws {\n   \t let data = try await dataTransformer.encode(item)\n   \t UserDefaults.standard.set(data, forKey: key(id: id))\n   \t UserDefaults.standard.synchronize()\n    }\n    \n    func read&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier) async throws -&gt; T {\n   \t guard let data = UserDefaults.standard.data(forKey: key(id: id)) else {\n   \t\t throw CRUDRepositoryError.recordNotFound(id: id)\n   \t }\n   \t let item: T = try await dataTransformer.decode(data: data)\n   \t return item\n    }\n    \n    func update&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws {\n   \t guard try await isExists(id: id) else {\n   \t\t throw CRUDRepositoryError.recordNotFound(id: id)\n   \t }\n   \t let data = try await dataTransformer.encode(item)\n   \t UserDefaults.standard.set(data, forKey: key(id: id))\n   \t UserDefaults.standard.synchronize()\n    }\n    \n    func delete(id: CRUDRepository.ItemIdentifier) async throws {\n   \t guard try await isExists(id: id) else {\n   \t\t throw CRUDRepositoryError.recordNotFound(id: id)\n   \t }\n   \t UserDefaults.standard.removeObject(forKey: key(id: id))\n   \t UserDefaults.standard.synchronize()\n    }\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Der Code sieht lang aus, enth\u00e4lt aber eine vollst\u00e4ndige konkrete Implementierung eines CRUD-Repositorys mit loser Kopplung, Details unten.<br \/>Typalias wurden zur Selbstdokumentation des Codes hinzugef\u00fcgt.<br \/>Schwache Kopplung und starke Kopplung<br \/>Die Losl\u00f6sung von einer bestimmten Struktur (Struktur) wird mithilfe des generischen T implementiert, das wiederum die Codable-Protokolle implementieren muss. Mit Codable k\u00f6nnen Sie Strukturen mithilfe von Klassen konvertieren, die die Protokolle TopLevelEncoder und TopLevelDecoder implementieren, beispielsweise JSONEncoder und JSONDecoder. Bei Verwendung von Basistypen (Int, String, Float usw.) ist es nicht erforderlich, zus\u00e4tzlichen Code zum Konvertieren von Strukturen zu schreiben.<\/ p><\/p>\n<p>Die Entkopplung von einem bestimmten Encoder und Decoder erfolgt mithilfe der Abstraktion im DataTransformer-Protokoll:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\tfunc encode&lt;T: Encodable&gt;(_ object: T) async throws -&gt; Data\n\tfunc decode&lt;T: Decodable&gt;(data: Data) async throws -&gt; T\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Durch die Implementierung eines Datentransformators haben wir eine Abstraktion der Encoder- und Decoder-Schnittstellen implementiert und eine lose Kopplung implementiert, um die Arbeit mit verschiedenen Arten von Datenformaten sicherzustellen.<\/p>\n<p>Das Folgende ist der Code f\u00fcr einen bestimmten DataTransformer, n\u00e4mlich f\u00fcr JSON:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\tfunc encode&lt;T&gt;(_ object: T) async throws -&gt; Data where T : Encodable {\n    \t\tlet data = try JSONEncoder().encode(object)\n    \t\treturn data\n\t}\n    \n\tfunc decode&lt;T&gt;(data: Data) async throws -&gt; T where T : Decodable {\n    \t\tlet item: T = try JSONDecoder().decode(T.self, from: data)\n    \t\treturn item\n\t}\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>War es m\u00f6glich?<\/p>\n<p>Was hat sich ge\u00e4ndert? Jetzt reicht es aus, ein bestimmtes Repository zu initialisieren, um mit jeder Struktur zu arbeiten, die das Codable-Protokoll implementiert, wodurch die Notwendigkeit entf\u00e4llt, Code zu duplizieren und eine lose Kopplung der Anwendung zu implementieren.<\/p>\n<p>Ein Beispiel f\u00fcr ein Client-CRUD mit einem bestimmten Repository, UserDefaults ist die Datenbank, JSON-Datenformat, Client-Struktur, au\u00dferdem ein Beispiel f\u00fcr das Schreiben und Lesen eines Arrays:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\nprint(\"One item access example\")\n\ndo {\n\tlet clientRecordIdentifier = \"client\"\n\tlet clientOne = Client(name: \"Chill Client\")\n\tlet repository = UserDefaultsRepository(\n    \ttableName: \"Clients Database\",\n    \tdataTransformer: JSONDataTransformer()\n\t)\n\ttry await repository.create(id: clientRecordIdentifier, item: clientOne)\n\tvar clientRecord: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(\"Client Name: \\(clientRecord.name)\")\n\tclientRecord.name = \"Busy Client\"\n\ttry await repository.update(id: clientRecordIdentifier, item: clientRecord)\n\tlet updatedClient: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(\"Updated Client Name: \\(updatedClient.name)\")\n\ttry await repository.delete(id: clientRecordIdentifier)\n\tlet removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(removedClientRecord)\n}\ncatch {\n\tprint(error.localizedDescription)\n}\n\nprint(\"Array access example\")\n\nlet clientArrayRecordIdentifier = \"clientArray\"\nlet clientOne = Client(name: \"Chill Client\")\nlet repository = UserDefaultsRepository(\n\ttableName: \"Clients Database\",\n\tdataTransformer: JSONDataTransformer()\n)\nlet array = [clientOne]\ntry await repository.create(id: clientArrayRecordIdentifier, item: array)\nlet savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)\nprint(savedArray.first!)\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>W\u00e4hrend der ersten CRUD-Pr\u00fcfung wurde eine Ausnahmebehandlung implementiert, bei der das Lesen des Remote-Elements nicht mehr m\u00f6glich ist.<\/p>\n<p>Datenbank wechseln<\/p>\n<p>Jetzt zeige ich Ihnen, wie Sie Ihren aktuellen Code in eine andere Datenbank \u00fcbertragen. Als Beispiel nehme ich den SQLite-Repository-Code, den ChatGPT generiert hat:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\nclass SQLiteRepository: CRUDRepository {\n    private typealias RecordIdentifier = String\n    \n    let tableName: String\n    let dataTransformer: DataTransformer\n    private var db: OpaquePointer?\n\n    init(\n   \t tableName: String,\n   \t dataTransformer: DataTransformer = JSONDataTransformer()\n    ) {\n   \t self.tableName = tableName\n   \t self.dataTransformer = dataTransformer\n   \t self.db = openDatabase()\n   \t createTableIfNeeded()\n    }\n    \n    private func openDatabase() -&gt; OpaquePointer? {\n   \t var db: OpaquePointer? = nil\n   \t let fileURL = try! FileManager.default\n   \t\t .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)\n   \t\t .appendingPathComponent(\"\\(tableName).sqlite\")\n   \t if sqlite3_open(fileURL.path, &amp;db) != SQLITE_OK {\n   \t\t print(\"error opening database\")\n   \t\t return nil\n   \t }\n   \t return db\n    }\n    \n    private func createTableIfNeeded() {\n   \t let createTableString = \"\"\"\n   \t CREATE TABLE IF NOT EXISTS \\(tableName) (\n   \t id TEXT PRIMARY KEY NOT NULL,\n   \t data BLOB NOT NULL\n   \t );\n   \t \"\"\"\n   \t var createTableStatement: OpaquePointer? = nil\n   \t if sqlite3_prepare_v2(db, createTableString, -1, &amp;createTableStatement, nil) == SQLITE_OK {\n   \t\t if sqlite3_step(createTableStatement) == SQLITE_DONE {\n       \t\t print(\"\\(tableName) table created.\")\n   \t\t } else {\n       \t\t print(\"\\(tableName) table could not be created.\")\n   \t\t }\n   \t } else {\n   \t\t print(\"CREATE TABLE statement could not be prepared.\")\n   \t }\n   \t sqlite3_finalize(createTableStatement)\n    }\n    \n    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -&gt; Bool {\n   \t let queryStatementString = \"SELECT data FROM \\(tableName) WHERE id = ?;\"\n   \t var queryStatement: OpaquePointer? = nil\n   \t if sqlite3_prepare_v2(db, queryStatementString, -1, &amp;queryStatement, nil) == SQLITE_OK {\n   \t\t sqlite3_bind_text(queryStatement, 1, id, -1, nil)\n   \t\t if sqlite3_step(queryStatement) == SQLITE_ROW {\n       \t\t sqlite3_finalize(queryStatement)\n       \t\t return true\n   \t\t } else {\n       \t\t sqlite3_finalize(queryStatement)\n       \t\t return false\n   \t\t }\n   \t } else {\n   \t\t print(\"SELECT statement could not be prepared.\")\n   \t\t throw CRUDRepositoryError.databaseError\n   \t }\n    }\n    \n    func create&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws {\n   \t let insertStatementString = \"INSERT INTO \\(tableName) (id, data) VALUES (?, ?);\"\n   \t var insertStatement: OpaquePointer? = nil\n   \t if sqlite3_prepare_v2(db, insertStatementString, -1, &amp;insertStatement, nil) == SQLITE_OK {\n   \t\t let data = try await dataTransformer.encode(item)\n   \t\t sqlite3_bind_text(insertStatement, 1, id, -1, nil)\n   \t\t sqlite3_bind_blob(insertStatement, 2, (data as NSData).bytes, Int32(data.count), nil)\n   \t\t if sqlite3_step(insertStatement) == SQLITE_DONE {\n       \t\t print(\"Successfully inserted row.\")\n   \t\t } else {\n       \t\t print(\"Could not insert row.\")\n       \t\t throw CRUDRepositoryError.databaseError\n   \t\t }\n   \t } else {\n   \t\t print(\"INSERT statement could not be prepared.\")\n   \t\t throw CRUDRepositoryError.databaseError\n   \t }\n   \t sqlite3_finalize(insertStatement)\n    }\n    \n    func read&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier) async throws -&gt; T {\n   \t let queryStatementString = \"SELECT data FROM \\(tableName) WHERE id = ?;\"\n   \t var queryStatement: OpaquePointer? = nil\n   \t var item: T?\n   \t if sqlite3_prepare_v2(db, queryStatementString, -1, &amp;queryStatement, nil) == SQLITE_OK {\n   \t\t sqlite3_bind_text(queryStatement, 1, id, -1, nil)\n   \t\t if sqlite3_step(queryStatement) == SQLITE_ROW {\n       \t\t let queryResultCol1 = sqlite3_column_blob(queryStatement, 0)\n       \t\t let queryResultCol1Length = sqlite3_column_bytes(queryStatement, 0)\n       \t\t let data = Data(bytes: queryResultCol1, count: Int(queryResultCol1Length))\n       \t\t item = try await dataTransformer.decode(data: data)\n   \t\t } else {\n       \t\t throw CRUDRepositoryError.recordNotFound(id: id)\n   \t\t }\n   \t } else {\n   \t\t print(\"SELECT statement could not be prepared\")\n   \t\t throw CRUDRepositoryError.databaseError\n   \t }\n   \t sqlite3_finalize(queryStatement)\n   \t return item!\n    }\n    \n    func update&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws {\n   \t guard try await isExists(id: id) else {\n   \t\t throw CRUDRepositoryError.recordNotFound(id: id)\n   \t }\n   \t let updateStatementString = \"UPDATE \\(tableName) SET data = ? WHERE id = ?;\"\n   \t var updateStatement: OpaquePointer? = nil\n   \t if sqlite3_prepare_v2(db, updateStatementString, -1, &amp;updateStatement, nil) == SQLITE_OK {\n   \t\t let data = try await dataTransformer.encode(item)\n   \t\t sqlite3_bind_blob(updateStatement, 1, (data as NSData).bytes, Int32(data.count), nil)\n   \t\t sqlite3_bind_text(updateStatement, 2, id, -1, nil)\n   \t\t if sqlite3_step(updateStatement) == SQLITE_DONE {\n       \t\t print(\"Successfully updated row.\")\n   \t\t } else {\n       \t\t print(\"Could not update row.\")\n       \t\t throw CRUDRepositoryError.databaseError\n   \t\t }\n   \t } else {\n   \t\t print(\"UPDATE statement could not be prepared.\")\n   \t\t throw CRUDRepositoryError.databaseError\n   \t }\n   \t sqlite3_finalize(updateStatement)\n    }\n    \n    func delete(id: CRUDRepository.ItemIdentifier) async throws {\n   \t guard try await isExists(id: id) else {\n   \t\t throw CRUDRepositoryError.recordNotFound(id: id)\n   \t }\n   \t let deleteStatementString = \"DELETE FROM \\(tableName) WHERE id = ?;\"\n   \t var deleteStatement: OpaquePointer? = nil\n   \t if sqlite3_prepare_v2(db, deleteStatementString, -1, &amp;deleteStatement, nil) == SQLITE_OK {\n   \t\t sqlite3_bind_text(deleteStatement, 1, id, -1, nil)\n   \t\t if sqlite3_step(deleteStatement) == SQLITE_DONE {\n       \t\t print(\"Successfully deleted row.\")\n   \t\t } else {\n       \t\t print(\"Could not delete row.\")\n       \t\t throw CRUDRepositoryError.databaseError\n   \t\t }\n   \t } else {\n   \t\t print(\"DELETE statement could not be prepared.\")\n   \t\t throw CRUDRepositoryError.databaseError\n   \t }\n   \t sqlite3_finalize(deleteStatement)\n    }\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Oder der CRUD-Code des Repositorys f\u00fcr das Dateisystem, der ebenfalls von ChatGPT generiert wurde:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\nclass FileSystemRepository: CRUDRepository {\n\tprivate typealias RecordIdentifier = String\n    \n\tlet directoryName: String\n\tlet dataTransformer: DataTransformer\n\tprivate let fileManager = FileManager.default\n\tprivate var directoryURL: URL\n    \n\tinit(\n    \tdirectoryName: String = \"Database\",\n    \tdataTransformer: DataTransformer = JSONDataTransformer()\n\t) {\n    \tself.directoryName = directoryName\n    \tself.dataTransformer = dataTransformer\n   \t \n    \tlet paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask)\n    \tdirectoryURL = paths.first!.appendingPathComponent(directoryName)\n   \t \n    \tif !fileManager.fileExists(atPath: directoryURL.path) {\n        \ttry? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)\n    \t}\n\t}\n    \n\tprivate func fileURL(id: CRUDRepository.ItemIdentifier) -&gt; URL {\n    \treturn directoryURL.appendingPathComponent(\"item_\\(id).json\")\n\t}\n    \n\tprivate func isExists(id: CRUDRepository.ItemIdentifier) async throws -&gt; Bool {\n    \treturn fileManager.fileExists(atPath: fileURL(id: id).path)\n\t}\n    \n\tfunc create&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws {\n    \tlet data = try await dataTransformer.encode(item)\n    \tlet url = fileURL(id: id)\n    \ttry data.write(to: url)\n\t}\n    \n\tfunc read&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier) async throws -&gt; T {\n    \tlet url = fileURL(id: id)\n    \tguard let data = fileManager.contents(atPath: url.path) else {\n        \tthrow CRUDRepositoryError.recordNotFound(id: id)\n    \t}\n    \tlet item: T = try await dataTransformer.decode(data: data)\n    \treturn item\n\t}\n    \n\tfunc update&lt;T: CRUDRepository.Item&gt;(id: CRUDRepository.ItemIdentifier, item: T) async throws {\n    \tguard try await isExists(id: id) else {\n        \tthrow CRUDRepositoryError.recordNotFound(id: id)\n    \t}\n    \tlet data = try await dataTransformer.encode(item)\n    \tlet url = fileURL(id: id)\n    \ttry data.write(to: url)\n\t}\n    \n\tfunc delete(id: CRUDRepository.ItemIdentifier) async throws {\n    \tguard try await isExists(id: id) else {\n        \tthrow CRUDRepositoryError.recordNotFound(id: id)\n    \t}\n    \tlet url = fileURL(id: id)\n    \ttry fileManager.removeItem(at: url)\n\t}\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Ersetzen Sie das Repository im Client-Code:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\nprint(\"One item access example\")\n\ndo {\n\tlet clientRecordIdentifier = \"client\"\n\tlet clientOne = Client(name: \"Chill Client\")\n\tlet repository = FileSystemRepository(\n    \tdirectoryName: \"Clients Database\",\n    \tdataTransformer: JSONDataTransformer()\n\t)\n\ttry await repository.create(id: clientRecordIdentifier, item: clientOne)\n\tvar clientRecord: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(\"Client Name: \\(clientRecord.name)\")\n\tclientRecord.name = \"Busy Client\"\n\ttry await repository.update(id: clientRecordIdentifier, item: clientRecord)\n\tlet updatedClient: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(\"Updated Client Name: \\(updatedClient.name)\")\n\ttry await repository.delete(id: clientRecordIdentifier)\n\tlet removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(removedClientRecord)\n}\ncatch {\n\tprint(error.localizedDescription)\n}\n\nprint(\"Array access example\")\n\nlet clientArrayRecordIdentifier = \"clientArray\"\nlet clientOne = Client(name: \"Chill Client\")\nlet repository = FileSystemRepository(\n\tdirectoryName: \"Clients Database\",\n\tdataTransformer: JSONDataTransformer()\n)\nlet array = [clientOne]\ntry await repository.create(id: clientArrayRecordIdentifier, item: array)\nlet savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)\nprint(savedArray.first!)\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Die Initialisierung von UserDefaultsRepository wurde durch FileSystemRepository mit den entsprechenden Argumenten ersetzt.<br \/>Nachdem Sie die zweite Version des Client-Codes ausgef\u00fchrt haben, finden Sie im Dokumentenordner ein Verzeichnis \u201eClients Database\u201c, das eine Datei eines in JSON serialisierten Arrays mit einer Client-Struktur enth\u00e4lt.<\/p>\n<p>Datenspeicherformat wechseln<\/p>\n<p>Jetzt bitten wir ChatGPT, einen Encoder und Decoder f\u00fcr XML zu generieren:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\tlet formatExtension = \"xml\"\n    \n\tfunc encode&lt;T: Encodable&gt;(_ item: T) async throws -&gt; Data {\n    \tlet encoder = PropertyListEncoder()\n    \tencoder.outputFormat = .xml\n    \treturn try encoder.encode(item)\n\t}\n    \n\tfunc decode&lt;T: Decodable&gt;(data: Data) async throws -&gt; T {\n    \tlet decoder = PropertyListDecoder()\n    \treturn try decoder.decode(T.self, from: data)\n\t}\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Dank der in Swift integrierten Typen wird die Aufgabe f\u00fcr ein neuronales Netzwerk elementar.<\/p>\n<p>Ersetzen Sie JSON durch XML im Client-Code:<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-unknown\" data-lang=\"unknown\"><code>\nprint(\"One item access example\")\n\ndo {\n\tlet clientRecordIdentifier = \"client\"\n\tlet clientOne = Client(name: \"Chill Client\")\n\tlet repository = FileSystemRepository(\n    \tdirectoryName: \"Clients Database\",\n    \tdataTransformer: XMLDataTransformer()\n\t)\n\ttry await repository.create(id: clientRecordIdentifier, item: clientOne)\n\tvar clientRecord: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(\"Client Name: \\(clientRecord.name)\")\n\tclientRecord.name = \"Busy Client\"\n\ttry await repository.update(id: clientRecordIdentifier, item: clientRecord)\n\tlet updatedClient: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(\"Updated Client Name: \\(updatedClient.name)\")\n\ttry await repository.delete(id: clientRecordIdentifier)\n\tlet removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)\n\tprint(removedClientRecord)\n}\ncatch {\n\tprint(error.localizedDescription)\n}\n\nprint(\"Array access example\")\n\nlet clientArrayRecordIdentifier = \"clientArray\"\nlet clientOne = Client(name: \"Chill Client\")\nlet repository = FileSystemRepository(\n\tdirectoryName: \"Clients Database\",\n\tdataTransformer: XMLDataTransformer()\n)\nlet array = [clientOne]\ntry await repository.create(id: clientArrayRecordIdentifier, item: array)\nlet savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)\nprint(savedArray.first!)\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Der Client-Code wurde in nur einen Ausdruck JSONDataTransformer ge\u00e4ndert -&gt; XMLDataTransformer<\/p>\n<p>Gesamt<\/p>\n<p>CRUD-Repositorys sind eines der Entwurfsmuster, die zur Implementierung einer losen Kopplung von Anwendungsarchitekturkomponenten verwendet werden k\u00f6nnen. Eine weitere L\u00f6sung &#8211; unter Verwendung von ORM (Object-Relational Mapping), kurz gesagt, ORM verwendet einen Ansatz, bei dem Strukturen vollst\u00e4ndig auf die Datenbank abgebildet werden und dann \u00c4nderungen an Modellen in der Datenbank angezeigt (zugeordnet (!)) werden sollen.<br \/>Aber das ist eine ganz andere Geschichte.<\/p>\n<p>Eine vollst\u00e4ndige Implementierung von CRUD-Repositorys f\u00fcr Swift ist verf\u00fcgbar unter:<br \/><a href=\"https:\/\/gitlab.com\/demensdeum\/crud-example\" rel=\"noopener\" target=\"_blank\">https:\/\/gitlab.com\/demensdeum\/crud-example<\/a><\/p>\n<p>Swift wird \u00fcbrigens schon lange au\u00dferhalb von macOS unterst\u00fctzt; der Code aus dem Artikel wurde vollst\u00e4ndig unter Arch Linux geschrieben und getestet.<\/p>\n<p>Quellen<\/p>\n<p><a href=\"https:\/\/developer.apple.com\/documentation\/combine\/topleveldecoder\" rel=\"noopener\" target=\"_blank\">https:\/\/developer.apple.com\/documentation\/combine\/topleveldecoder <\/a><br \/><a href=\"https:\/\/developer.apple.com\/documentation\/combine\/toplevelencoder\" rel=\"noopener\" target=\"_blank\">https:\/\/developer.apple.com\/documentation\/combine\/toplevelencoder<\/a><br \/>\n<a href=\"https:\/\/en.wikipedia.org\/wiki\/Create,_read,_update_and_delete\" rel=\"noopener\" target=\"_blank\">https:\/\/en.wikipedia.org\/wiki\/Create,_read,_update_and_delete<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In dieser Notiz beschreibe ich die Grundprinzipien des bekannten klassischen CRUD-Musters und die Implementierung in der Swift-Sprache. Swift ist eine offene, plattform\u00fcbergreifende Sprache, die f\u00fcr Windows, Linux, macOS, iOS und Android verf\u00fcgbar ist. Es gibt viele L\u00f6sungen zur Abstraktion von Datenspeicherung und Anwendungslogik. Eine solche L\u00f6sung ist der CRUD-Ansatz, ein Akronym f\u00fcr C&#8211; Erstellen, R<a class=\"more-link\" href=\"https:\/\/demensdeum.com\/blog\/de\/2024\/05\/28\/crud-repository\/\">Continue reading <span class=\"screen-reader-text\">&#8220;CRUD-Repository&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[61],"tags":[224,223,225],"class_list":["post-3522","post","type-post","status-publish","format-standard","hentry","category-techie","tag-crud","tag-swift","tag-225","entry"],"translation":{"provider":"WPGlobus","version":"3.0.2","language":"de","enabled_languages":["en","ru","zh","de","fr","ja","pt"],"languages":{"en":{"title":true,"content":true,"excerpt":false},"ru":{"title":true,"content":true,"excerpt":false},"zh":{"title":true,"content":true,"excerpt":false},"de":{"title":true,"content":true,"excerpt":false},"fr":{"title":true,"content":true,"excerpt":false},"ja":{"title":true,"content":true,"excerpt":false},"pt":{"title":true,"content":true,"excerpt":false}}},"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/posts\/3522","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/comments?post=3522"}],"version-history":[{"count":33,"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/posts\/3522\/revisions"}],"predecessor-version":[{"id":3867,"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/posts\/3522\/revisions\/3867"}],"wp:attachment":[{"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/media?parent=3522"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/categories?post=3522"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/de\/wp-json\/wp\/v2\/tags?post=3522"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}