{"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\/fr\/2024\/05\/28\/crud-repository\/","title":{"rendered":"D\u00e9p\u00f4t CRUD"},"content":{"rendered":"<p>Dans cette note, je d\u00e9crirai les principes de base du mod\u00e8le CRUD classique bien connu, impl\u00e9ment\u00e9 dans le langage Swift. Swift est un langage ouvert et multiplateforme disponible pour Windows, Linux, macOS, iOS et Android.<\/p>\n<p>Il existe de nombreuses solutions pour extraire le stockage des donn\u00e9es et la logique des applications. L&#8217;une de ces solutions est l&#8217;approche CRUD, qui est l&#8217;acronyme de C&#8211; Cr\u00e9er, R-Read, U &#8211; Mise \u00e0 jour, D&#8211; Supprimer.<br \/>G\u00e9n\u00e9ralement, ce principe est mis en \u0153uvre via la mise en \u0153uvre d&#8217;une interface vers la base de donn\u00e9es, dans laquelle les \u00e9l\u00e9ments sont manipul\u00e9s \u00e0 l&#8217;aide d&#8217;un identifiant unique, tel que id. Une interface est cr\u00e9\u00e9e pour chaque lettre CRUD &#8211; Cr\u00e9er (objet, identifiant), Lire (identifiant), Mettre \u00e0 jour (objet, identifiant), Supprimer (objet, identifiant).<br \/>Si un objet contient un identifiant en lui-m\u00eame, alors l&#8217;argument id peut \u00eatre omis des m\u00e9thodes (Create, Update, Delete), puisque l&#8217;objet entier y est transmis avec son champ &#8211; identifiant. Mais pour &#8211; La lecture n\u00e9cessite un identifiant car nous voulons obtenir un objet de la base de donn\u00e9es par identifiant.<\/p>\n<p>Tous les noms sont fictifs<\/p>\n<p>Imaginons qu&#8217;une hypoth\u00e9tique application AssistantAI ait \u00e9t\u00e9 cr\u00e9\u00e9e \u00e0 l&#8217;aide du SDK de base de donn\u00e9es EtherRelm gratuit, l&#8217;int\u00e9gration \u00e9tait simple, l&#8217;API \u00e9tait tr\u00e8s pratique et, par cons\u00e9quent, l&#8217;application a \u00e9t\u00e9 lanc\u00e9e sur les march\u00e9s.<br \/>Du coup, le d\u00e9veloppeur du SDK EtherRelm d\u00e9cide de le rendre payant, fixant le prix \u00e0 100\u00a0$ par an et par utilisateur de l&#8217;application.<br \/>Quoi? Oui! Que doivent faire les d\u00e9veloppeurs d&#8217;AssistantAI maintenant, car ils comptent d\u00e9j\u00e0 1 million d&#8217;utilisateurs actifs ! Payer 100 millions de dollars\u00a0?<br \/>Au lieu de cela, il est d\u00e9cid\u00e9 d&#8217;\u00e9valuer le transfert de l&#8217;application vers la base de donn\u00e9es RootData native de la plateforme ; selon les programmeurs, un tel transfert prendra environ six mois, cela ne prend pas en compte l&#8217;impl\u00e9mentation de nouvelles fonctionnalit\u00e9s dans l&#8217;application. Apr\u00e8s r\u00e9flexion, il a \u00e9t\u00e9 d\u00e9cid\u00e9 de retirer l&#8217;application des march\u00e9s, de la r\u00e9\u00e9crire sur un autre framework multiplateforme gratuit avec une base de donn\u00e9es BueMS int\u00e9gr\u00e9e, cela r\u00e9soudra le probl\u00e8me avec la base de donn\u00e9es payante + simplifiera le d\u00e9veloppement sur d&#8217;autres plateformes. <br \/>Un an plus tard, l&#8217;application a \u00e9t\u00e9 r\u00e9\u00e9crite dans BueMS, mais soudain, le d\u00e9veloppeur du framework d\u00e9cide de la rendre payante. Il s&#8217;av\u00e8re que l&#8217;\u00e9quipe est tomb\u00e9e deux fois dans le m\u00eame pi\u00e8ge ; qu&#8217;elle parvienne \u00e0 s&#8217;en sortir une deuxi\u00e8me fois est une toute autre histoire.<\/p>\n<p>L&#8217;abstraction \u00e0 la rescousse<\/p>\n<p>Ces probl\u00e8mes auraient pu \u00eatre \u00e9vit\u00e9s si les d\u00e9veloppeurs avaient utilis\u00e9 une abstraction des interfaces au sein de l&#8217;application. Aux trois piliers de la POO &#8211; polymorphisme, encapsulation, h\u00e9ritage, il n&#8217;y a pas si longtemps, ils ont ajout\u00e9 un autre &#8211; abstraction.<br \/>L&#8217;abstraction des donn\u00e9es vous permet de d\u00e9crire des id\u00e9es et des mod\u00e8les en termes g\u00e9n\u00e9raux, avec un minimum de d\u00e9tails, tout en \u00e9tant suffisamment pr\u00e9cis pour mettre en \u0153uvre des impl\u00e9mentations sp\u00e9cifiques utilis\u00e9es pour r\u00e9soudre des probl\u00e8mes commerciaux.<br \/>Comment pouvons-nous abstraire le fonctionnement de la base de donn\u00e9es afin que la logique de l\u2019application n\u2019en d\u00e9pende pas\u00a0? Nous utilisons l&#8217;approche CRUD\u00a0!<\/p>\n<p>Un diagramme UML CRUD simplifi\u00e9 ressemble \u00e0 ceci\u00a0:<\/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>Exemple avec une base de donn\u00e9es EtherRelm fictive\u00a0:<\/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>Exemple avec une vraie base de donn\u00e9es SQLite\u00a0:<\/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>Comme vous l&#8217;avez d\u00e9j\u00e0 remarqu\u00e9, lorsque vous changez de base de donn\u00e9es, seule celle-ci change\u00a0; l&#8217;interface CRUD avec laquelle l&#8217;application interagit reste inchang\u00e9e. CRUD est une impl\u00e9mentation du mod\u00e8le GoF &#8211; Adaptateur, parce que en l&#8217;utilisant, nous adaptons les interfaces d&#8217;application \u00e0 n&#8217;importe quelle base de donn\u00e9es et combinons des interfaces incompatibles.<br \/>Les mots sont vides, montre-moi le code<br \/>Pour impl\u00e9menter des abstractions dans les langages de programmation, des interfaces\/protocoles\/classes abstraites sont utilis\u00e9es. Ce sont tous des ph\u00e9nom\u00e8nes du m\u00eame ordre, cependant, lors d&#8217;entretiens, on peut vous demander de nommer la diff\u00e9rence entre eux, je pense personnellement que cela n&#8217;a pas beaucoup de sens car le seul but d&#8217;utilisation est de mettre en \u0153uvre l&#8217;abstraction des donn\u00e9es, sinon il s&#8217;agit de tester la m\u00e9moire de la personne interrog\u00e9e.<br \/>CRUD est souvent impl\u00e9ment\u00e9 dans le cadre du mod\u00e8le Repository, cependant, le r\u00e9f\u00e9rentiel peut ou non impl\u00e9menter l&#8217;interface CRUD, tout d\u00e9pend de l&#8217;ing\u00e9niosit\u00e9 du d\u00e9veloppeur.<\/p>\n<p>Consid\u00e9rons un code Swift assez typique pour le r\u00e9f\u00e9rentiel de structure Book, travaillant directement avec la base de donn\u00e9es UserDefaults\u00a0:<\/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>Le code ci-dessus semble simple, mais comptons le nombre de violations du principe DRY (Do not Repeat Yourself) et la coh\u00e9rence du code\u00a0:<br \/>Connectivit\u00e9 \u00e0 la base de donn\u00e9es UserDefaults<br \/>Connectivit\u00e9 avec les encodeurs et d\u00e9codeurs JSON &#8211; JSONEncoder, JSONDecoder<br \/>Connect\u00e9 \u00e0 la structure Book, mais nous avons besoin d&#8217;un r\u00e9f\u00e9rentiel abstrait afin de ne pas cr\u00e9er de classe r\u00e9f\u00e9rentiel pour chaque structure que nous allons stocker dans la base de donn\u00e9es (violation DRY)<\/p>\n<p>Je vois assez souvent ce type de code de r\u00e9f\u00e9rentiel CRUD, il peut \u00eatre utilis\u00e9, mais un couplage \u00e9lev\u00e9 et une duplication de code conduisent au fait qu&#8217;avec le temps, sa prise en charge deviendra tr\u00e8s compliqu\u00e9e. Cela sera particuli\u00e8rement visible lorsque vous tenterez de passer \u00e0 une autre base de donn\u00e9es ou lors de la modification de la logique interne de travail avec la base de donn\u00e9es dans tous les r\u00e9f\u00e9rentiels cr\u00e9\u00e9s dans l&#8217;application.<br \/>Au lieu de dupliquer le code, maintenez un couplage \u00e9lev\u00e9\u00a0: \u00c9crivons un protocole pour le r\u00e9f\u00e9rentiel CRUD, faisant ainsi abstraction de l&#8217;interface de base de donn\u00e9es et de la logique m\u00e9tier de l&#8217;application, respectant DRY, impl\u00e9mentant un faible couplage\u00a0:<\/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>Le protocole CRUDRepository d\u00e9crit les interfaces et les types de donn\u00e9es associ\u00e9s pour une mise en \u0153uvre ult\u00e9rieure d&#8217;un r\u00e9f\u00e9rentiel CRUD sp\u00e9cifique.<\/p>\n<p>Ensuite, nous \u00e9crirons une impl\u00e9mentation sp\u00e9cifique pour la base de donn\u00e9es UserDefaults\u00a0:<\/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>Le code semble long, mais contient une impl\u00e9mentation concr\u00e8te compl\u00e8te d&#8217;un r\u00e9f\u00e9rentiel CRUD contenant un couplage l\u00e2che, d\u00e9tails ci-dessous.<br \/>des typealias ont \u00e9t\u00e9 ajout\u00e9s pour l&#8217;auto-documentation du code.<br \/>Couplage faible et couplage fort<br \/>Le d\u00e9tachement d&#8217;une structure sp\u00e9cifique (struct) est impl\u00e9ment\u00e9 \u00e0 l&#8217;aide du T g\u00e9n\u00e9rique, qui \u00e0 son tour doit impl\u00e9menter les protocoles Codable. Codable vous permet de convertir des structures \u00e0 l&#8217;aide de classes qui impl\u00e9mentent les protocoles TopLevelEncoder et TopLevelDecoder, par exemple JSONEncoder et JSONDecoder, lors de l&#8217;utilisation de types de base (Int, String, Float, etc.), il n&#8217;est pas n\u00e9cessaire d&#8217;\u00e9crire du code suppl\u00e9mentaire pour convertir les structures.<\/ p><\/p>\n<p>Le d\u00e9couplage d&#8217;un encodeur et d&#8217;un d\u00e9codeur sp\u00e9cifiques s&#8217;effectue \u00e0 l&#8217;aide de l&#8217;abstraction dans le protocole DataTransformer\u00a0:<\/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>En utilisant l&#8217;impl\u00e9mentation d&#8217;un transformateur de donn\u00e9es, nous avons impl\u00e9ment\u00e9 une abstraction des interfaces de l&#8217;encodeur et du d\u00e9codeur, en impl\u00e9mentant un couplage l\u00e2che pour garantir le travail avec diff\u00e9rents types de formats de donn\u00e9es.<\/p>\n<p>Ce qui suit est le code d&#8217;un DataTransformer sp\u00e9cifique, \u00e0 savoir pour JSON\u00a0:<\/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>\u00c9tait-ce possible\u00a0?<\/p>\n<p>Qu&#8217;est-ce qui a chang\u00e9\u00a0? Il suffit d\u00e9sormais d&#8217;initialiser un r\u00e9f\u00e9rentiel sp\u00e9cifique pour fonctionner avec n&#8217;importe quelle structure impl\u00e9mentant le protocole Codable, \u00e9liminant ainsi le besoin de dupliquer le code et de mettre en \u0153uvre un couplage l\u00e2che de l&#8217;application.<\/p>\n<p>Un exemple de client CRUD avec un r\u00e9f\u00e9rentiel sp\u00e9cifique, UserDefaults est la base de donn\u00e9es, le format de donn\u00e9es JSON, la structure client, \u00e9galement un exemple d&#8217;\u00e9criture et de lecture d&#8217;un tableau\u00a0:<\/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>Lors de la premi\u00e8re v\u00e9rification CRUD, une gestion des exceptions a \u00e9t\u00e9 impl\u00e9ment\u00e9e, dans laquelle la lecture de l&#8217;\u00e9l\u00e9ment distant ne sera plus disponible.<\/p>\n<p>Changer de base de donn\u00e9es<\/p>\n<p>Je vais maintenant vous montrer comment transf\u00e9rer votre code actuel vers une autre base de donn\u00e9es. Par exemple, je prendrai le code du r\u00e9f\u00e9rentiel SQLite g\u00e9n\u00e9r\u00e9 par ChatGPT\u00a0:<\/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>Ou le code CRUD du r\u00e9f\u00e9rentiel du syst\u00e8me de fichiers, qui a \u00e9galement \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9 par ChatGPT\u00a0:<\/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>Remplacer le d\u00e9p\u00f4t dans le code client\u00a0:<\/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>L&#8217;initialisation de UserDefaultsRepository a \u00e9t\u00e9 remplac\u00e9e par FileSystemRepository, avec les arguments appropri\u00e9s.<br \/>Apr\u00e8s avoir ex\u00e9cut\u00e9 la deuxi\u00e8me version du code client, vous trouverez un r\u00e9pertoire \u00ab Clients Database \u00bb dans le dossier documents, qui contiendra un fichier d&#8217;un tableau s\u00e9rialis\u00e9 en JSON avec une structure Client.<\/p>\n<p>Changement de format de stockage des donn\u00e9es<\/p>\n<p>Demandons maintenant \u00e0 ChatGPT de g\u00e9n\u00e9rer un encodeur et un d\u00e9codeur pour XML\u00a0:<\/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>Gr\u00e2ce aux types int\u00e9gr\u00e9s dans Swift, la t\u00e2che d&#8217;un r\u00e9seau de neurones devient \u00e9l\u00e9mentaire.<\/p>\n<p>Remplacez JSON par XML dans le code client\u00a0:<\/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>Le code client a \u00e9t\u00e9 remplac\u00e9 par une seule expression JSONDataTransformer -&gt; XMLDataTransformer<\/p>\n<p>Total<\/p>\n<p>Les r\u00e9f\u00e9rentiels CRUD sont l&#8217;un des mod\u00e8les de conception qui peuvent \u00eatre utilis\u00e9s pour impl\u00e9menter un couplage l\u00e2che des composants de l&#8217;architecture d&#8217;application. Une autre des solutions &#8211; en utilisant ORM (Object-Relational Mapping), en bref, ORM utilise une approche dans laquelle les structures sont enti\u00e8rement mapp\u00e9es \u00e0 la base de donn\u00e9es, puis les modifications avec les mod\u00e8les doivent \u00eatre affich\u00e9es (mapp\u00e9es (!)) sur la base de donn\u00e9es.<br \/>Mais c&#8217;est une toute autre histoire.<\/p>\n<p>Une impl\u00e9mentation compl\u00e8te des r\u00e9f\u00e9rentiels CRUD pour Swift est disponible sur\u00a0:<br \/><a href=\"https:\/\/gitlab.com\/demensdeum\/crud-example\" rel=\"noopener\" target=\"_blank\">https:\/\/gitlab.com\/demensdeum\/crud-example<\/a><\/p>\n<p>\u00c0 propos, Swift est pris en charge en dehors de macOS depuis longtemps\u00a0; le code de l&#8217;article a \u00e9t\u00e9 enti\u00e8rement \u00e9crit et test\u00e9 sur Arch Linux.<\/p>\n<p>Sources<\/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>Dans cette note, je d\u00e9crirai les principes de base du mod\u00e8le CRUD classique bien connu, impl\u00e9ment\u00e9 dans le langage Swift. Swift est un langage ouvert et multiplateforme disponible pour Windows, Linux, macOS, iOS et Android. Il existe de nombreuses solutions pour extraire le stockage des donn\u00e9es et la logique des applications. L&#8217;une de ces solutions<a class=\"more-link\" href=\"https:\/\/demensdeum.com\/blog\/fr\/2024\/05\/28\/crud-repository\/\">Continue reading <span class=\"screen-reader-text\">&#8220;D\u00e9p\u00f4t CRUD&#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_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"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":"fr","enabled_languages":["en","ru","zh","de","fr","ja","pt","hi"],"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},"hi":{"title":false,"content":false,"excerpt":false}}},"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/posts\/3522","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/comments?post=3522"}],"version-history":[{"count":33,"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/posts\/3522\/revisions"}],"predecessor-version":[{"id":3867,"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/posts\/3522\/revisions\/3867"}],"wp:attachment":[{"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/media?parent=3522"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/categories?post=3522"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/demensdeum.com\/blog\/fr\/wp-json\/wp\/v2\/tags?post=3522"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}