Dans cette note, je décrirai les principes de base du modèle CRUD classique bien connu, implémenté 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ées et la logique des applications. L’une de ces solutions est l’approche CRUD, qui est l’acronyme de C– Créer, R-Read, U – Mise à jour, D– Supprimer.
Généralement, ce principe est mis en œuvre via la mise en œuvre d’une interface vers la base de données, dans laquelle les éléments sont manipulés à l’aide d’un identifiant unique, tel que id. Une interface est créée pour chaque lettre CRUD – Créer (objet, identifiant), Lire (identifiant), Mettre à jour (objet, identifiant), Supprimer (objet, identifiant).
Si un objet contient un identifiant en lui-même, alors l’argument id peut être omis des méthodes (Create, Update, Delete), puisque l’objet entier y est transmis avec son champ – identifiant. Mais pour – La lecture nécessite un identifiant car nous voulons obtenir un objet de la base de données par identifiant.
Tous les noms sont fictifs
Imaginons qu’une hypothétique application AssistantAI ait été créée à l’aide du SDK de base de données EtherRelm gratuit, l’intégration était simple, l’API était très pratique et, par conséquent, l’application a été lancée sur les marchés.
Du coup, le développeur du SDK EtherRelm décide de le rendre payant, fixant le prix à 100 $ par an et par utilisateur de l’application.
Quoi? Oui! Que doivent faire les développeurs d’AssistantAI maintenant, car ils comptent déjà 1 million d’utilisateurs actifs ! Payer 100 millions de dollars ?
Au lieu de cela, il est décidé d’évaluer le transfert de l’application vers la base de données RootData native de la plateforme ; selon les programmeurs, un tel transfert prendra environ six mois, cela ne prend pas en compte l’implémentation de nouvelles fonctionnalités dans l’application. Après réflexion, il a été décidé de retirer l’application des marchés, de la réécrire sur un autre framework multiplateforme gratuit avec une base de données BueMS intégrée, cela résoudra le problème avec la base de données payante + simplifiera le développement sur d’autres plateformes.
Un an plus tard, l’application a été réécrite dans BueMS, mais soudain, le développeur du framework décide de la rendre payante. Il s’avère que l’équipe est tombée deux fois dans le même piège ; qu’elle parvienne à s’en sortir une deuxième fois est une toute autre histoire.
L’abstraction à la rescousse
Ces problèmes auraient pu être évités si les développeurs avaient utilisé une abstraction des interfaces au sein de l’application. Aux trois piliers de la POO – polymorphisme, encapsulation, héritage, il n’y a pas si longtemps, ils ont ajouté un autre – abstraction.
L’abstraction des données vous permet de décrire des idées et des modèles en termes généraux, avec un minimum de détails, tout en étant suffisamment précis pour mettre en œuvre des implémentations spécifiques utilisées pour résoudre des problèmes commerciaux.
Comment pouvons-nous abstraire le fonctionnement de la base de données afin que la logique de l’application n’en dépende pas ? Nous utilisons l’approche CRUD !
Un diagramme UML CRUD simplifié ressemble à ceci :

Exemple avec une base de données EtherRelm fictive :

Exemple avec une vraie base de données SQLite :

Comme vous l’avez déjà remarqué, lorsque vous changez de base de données, seule celle-ci change ; l’interface CRUD avec laquelle l’application interagit reste inchangée. CRUD est une implémentation du modèle GoF – Adaptateur, parce que en l’utilisant, nous adaptons les interfaces d’application à n’importe quelle base de données et combinons des interfaces incompatibles.
Les mots sont vides, montre-moi le code
Pour implémenter des abstractions dans les langages de programmation, des interfaces/protocoles/classes abstraites sont utilisées. Ce sont tous des phénomènes du même ordre, cependant, lors d’entretiens, on peut vous demander de nommer la différence entre eux, je pense personnellement que cela n’a pas beaucoup de sens car le seul but d’utilisation est de mettre en œuvre l’abstraction des données, sinon il s’agit de tester la mémoire de la personne interrogée.
CRUD est souvent implémenté dans le cadre du modèle Repository, cependant, le référentiel peut ou non implémenter l’interface CRUD, tout dépend de l’ingéniosité du développeur.
Considérons un code Swift assez typique pour le référentiel de structure Book, travaillant directement avec la base de données UserDefaults :
struct Book: Codable {
let title: String
let author: String
}
class BookRepository {
func save(book: Book) {
let record = try! JSONEncoder().encode(book)
UserDefaults.standard.set(record, forKey: book.title)
}
func get(bookWithTitle title: String) -> Book? {
guard let data = UserDefaults.standard.data(forKey: title) else { return nil }
let book = try! JSONDecoder().decode(Book.self, from: data)
return book
}
func delete(book: Book) {
UserDefaults.standard.removeObject(forKey: book.title)
}
}
let book = Book(title: "Fear and Loathing in COBOL", author: "Sir Edsger ZX Spectrum")
let repository = BookRepository()
repository.save(book: book)
print(repository.get(bookWithTitle: book.title)!)
repository.delete(book: book)
guard repository.get(bookWithTitle: book.title) == nil else {
print("Error: can't delete Book from repository!")
exit(1)
}
Le code ci-dessus semble simple, mais comptons le nombre de violations du principe DRY (Do not Repeat Yourself) et la cohérence du code :
Connectivité à la base de données UserDefaults
Connectivité avec les encodeurs et décodeurs JSON – JSONEncoder, JSONDecoder
Connecté à la structure Book, mais nous avons besoin d’un référentiel abstrait afin de ne pas créer de classe référentiel pour chaque structure que nous allons stocker dans la base de données (violation DRY)
Je vois assez souvent ce type de code de référentiel CRUD, il peut être utilisé, mais un couplage élevé et une duplication de code conduisent au fait qu’avec le temps, sa prise en charge deviendra très compliquée. Cela sera particulièrement visible lorsque vous tenterez de passer à une autre base de données ou lors de la modification de la logique interne de travail avec la base de données dans tous les référentiels créés dans l’application.
Au lieu de dupliquer le code, maintenez un couplage élevé : Écrivons un protocole pour le référentiel CRUD, faisant ainsi abstraction de l’interface de base de données et de la logique métier de l’application, respectant DRY, implémentant un faible couplage :
typealias Item = Codable
typealias ItemIdentifier = String
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
func delete(id: CRUDRepository.ItemIdentifier) async throws
}
Le protocole CRUDRepository décrit les interfaces et les types de données associés pour une mise en œuvre ultérieure d’un référentiel CRUD spécifique.
Ensuite, nous écrirons une implémentation spécifique pour la base de données UserDefaults :
private typealias RecordIdentifier = String
let tableName: String
let dataTransformer: DataTransformer
init(
tableName: String = "",
dataTransformer: DataTransformer = JSONDataTransformer()
) {
self.tableName = tableName
self.dataTransformer = dataTransformer
}
private func key(id: CRUDRepository.ItemIdentifier) -> RecordIdentifier {
"database_\(tableName)_item_\(id)"
}
private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
UserDefaults.standard.data(forKey: key(id: id)) != nil
}
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
let data = try await dataTransformer.encode(item)
UserDefaults.standard.set(data, forKey: key(id: id))
UserDefaults.standard.synchronize()
}
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
guard let data = UserDefaults.standard.data(forKey: key(id: id)) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let item: T = try await dataTransformer.decode(data: data)
return item
}
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let data = try await dataTransformer.encode(item)
UserDefaults.standard.set(data, forKey: key(id: id))
UserDefaults.standard.synchronize()
}
func delete(id: CRUDRepository.ItemIdentifier) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
UserDefaults.standard.removeObject(forKey: key(id: id))
UserDefaults.standard.synchronize()
}
}
Le code semble long, mais contient une implémentation concrète complète d’un référentiel CRUD contenant un couplage lâche, détails ci-dessous.
des typealias ont été ajoutés pour l’auto-documentation du code.
Couplage faible et couplage fort
Le détachement d’une structure spécifique (struct) est implémenté à l’aide du T générique, qui à son tour doit implémenter les protocoles Codable. Codable vous permet de convertir des structures à l’aide de classes qui implémentent les protocoles TopLevelEncoder et TopLevelDecoder, par exemple JSONEncoder et JSONDecoder, lors de l’utilisation de types de base (Int, String, Float, etc.), il n’est pas nécessaire d’écrire du code supplémentaire pour convertir les structures. p>
Le découplage d’un encodeur et d’un décodeur spécifiques s’effectue à l’aide de l’abstraction dans le protocole DataTransformer :
func encode<T: Encodable>(_ object: T) async throws -> Data
func decode<T: Decodable>(data: Data) async throws -> T
}
En utilisant l’implémentation d’un transformateur de données, nous avons implémenté une abstraction des interfaces de l’encodeur et du décodeur, en implémentant un couplage lâche pour garantir le travail avec différents types de formats de données.
Ce qui suit est le code d’un DataTransformer spécifique, à savoir pour JSON :
func encode<T>(_ object: T) async throws -> Data where T : Encodable {
let data = try JSONEncoder().encode(object)
return data
}
func decode<T>(data: Data) async throws -> T where T : Decodable {
let item: T = try JSONDecoder().decode(T.self, from: data)
return item
}
}
Était-ce possible ?
Qu’est-ce qui a changé ? Il suffit désormais d’initialiser un référentiel spécifique pour fonctionner avec n’importe quelle structure implémentant le protocole Codable, éliminant ainsi le besoin de dupliquer le code et de mettre en œuvre un couplage lâche de l’application.
Un exemple de client CRUD avec un référentiel spécifique, UserDefaults est la base de données, le format de données JSON, la structure client, également un exemple d’écriture et de lecture d’un tableau :
print("One item access example")
do {
let clientRecordIdentifier = "client"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
tableName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
try await repository.create(id: clientRecordIdentifier, item: clientOne)
var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print("Client Name: \(clientRecord.name)")
clientRecord.name = "Busy Client"
try await repository.update(id: clientRecordIdentifier, item: clientRecord)
let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
print("Updated Client Name: \(updatedClient.name)")
try await repository.delete(id: clientRecordIdentifier)
let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print(removedClientRecord)
}
catch {
print(error.localizedDescription)
}
print("Array access example")
let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
tableName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)
Lors de la première vérification CRUD, une gestion des exceptions a été implémentée, dans laquelle la lecture de l’élément distant ne sera plus disponible.
Changer de base de données
Je vais maintenant vous montrer comment transférer votre code actuel vers une autre base de données. Par exemple, je prendrai le code du référentiel SQLite généré par ChatGPT :
class SQLiteRepository: CRUDRepository {
private typealias RecordIdentifier = String
let tableName: String
let dataTransformer: DataTransformer
private var db: OpaquePointer?
init(
tableName: String,
dataTransformer: DataTransformer = JSONDataTransformer()
) {
self.tableName = tableName
self.dataTransformer = dataTransformer
self.db = openDatabase()
createTableIfNeeded()
}
private func openDatabase() -> OpaquePointer? {
var db: OpaquePointer? = nil
let fileURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("\(tableName).sqlite")
if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
print("error opening database")
return nil
}
return db
}
private func createTableIfNeeded() {
let createTableString = """
CREATE TABLE IF NOT EXISTS \(tableName) (
id TEXT PRIMARY KEY NOT NULL,
data BLOB NOT NULL
);
"""
var createTableStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
if sqlite3_step(createTableStatement) == SQLITE_DONE {
print("\(tableName) table created.")
} else {
print("\(tableName) table could not be created.")
}
} else {
print("CREATE TABLE statement could not be prepared.")
}
sqlite3_finalize(createTableStatement)
}
private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
var queryStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
sqlite3_bind_text(queryStatement, 1, id, -1, nil)
if sqlite3_step(queryStatement) == SQLITE_ROW {
sqlite3_finalize(queryStatement)
return true
} else {
sqlite3_finalize(queryStatement)
return false
}
} else {
print("SELECT statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
}
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
let insertStatementString = "INSERT INTO \(tableName) (id, data) VALUES (?, ?);"
var insertStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
let data = try await dataTransformer.encode(item)
sqlite3_bind_text(insertStatement, 1, id, -1, nil)
sqlite3_bind_blob(insertStatement, 2, (data as NSData).bytes, Int32(data.count), nil)
if sqlite3_step(insertStatement) == SQLITE_DONE {
print("Successfully inserted row.")
} else {
print("Could not insert row.")
throw CRUDRepositoryError.databaseError
}
} else {
print("INSERT statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(insertStatement)
}
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
var queryStatement: OpaquePointer? = nil
var item: T?
if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
sqlite3_bind_text(queryStatement, 1, id, -1, nil)
if sqlite3_step(queryStatement) == SQLITE_ROW {
let queryResultCol1 = sqlite3_column_blob(queryStatement, 0)
let queryResultCol1Length = sqlite3_column_bytes(queryStatement, 0)
let data = Data(bytes: queryResultCol1, count: Int(queryResultCol1Length))
item = try await dataTransformer.decode(data: data)
} else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
} else {
print("SELECT statement could not be prepared")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(queryStatement)
return item!
}
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let updateStatementString = "UPDATE \(tableName) SET data = ? WHERE id = ?;"
var updateStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
let data = try await dataTransformer.encode(item)
sqlite3_bind_blob(updateStatement, 1, (data as NSData).bytes, Int32(data.count), nil)
sqlite3_bind_text(updateStatement, 2, id, -1, nil)
if sqlite3_step(updateStatement) == SQLITE_DONE {
print("Successfully updated row.")
} else {
print("Could not update row.")
throw CRUDRepositoryError.databaseError
}
} else {
print("UPDATE statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(updateStatement)
}
func delete(id: CRUDRepository.ItemIdentifier) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let deleteStatementString = "DELETE FROM \(tableName) WHERE id = ?;"
var deleteStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, deleteStatementString, -1, &deleteStatement, nil) == SQLITE_OK {
sqlite3_bind_text(deleteStatement, 1, id, -1, nil)
if sqlite3_step(deleteStatement) == SQLITE_DONE {
print("Successfully deleted row.")
} else {
print("Could not delete row.")
throw CRUDRepositoryError.databaseError
}
} else {
print("DELETE statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(deleteStatement)
}
}
Ou le code CRUD du référentiel du système de fichiers, qui a également été généré par ChatGPT :
class FileSystemRepository: CRUDRepository {
private typealias RecordIdentifier = String
let directoryName: String
let dataTransformer: DataTransformer
private let fileManager = FileManager.default
private var directoryURL: URL
init(
directoryName: String = "Database",
dataTransformer: DataTransformer = JSONDataTransformer()
) {
self.directoryName = directoryName
self.dataTransformer = dataTransformer
let paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
directoryURL = paths.first!.appendingPathComponent(directoryName)
if !fileManager.fileExists(atPath: directoryURL.path) {
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
}
}
private func fileURL(id: CRUDRepository.ItemIdentifier) -> URL {
return directoryURL.appendingPathComponent("item_\(id).json")
}
private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
return fileManager.fileExists(atPath: fileURL(id: id).path)
}
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
let data = try await dataTransformer.encode(item)
let url = fileURL(id: id)
try data.write(to: url)
}
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
let url = fileURL(id: id)
guard let data = fileManager.contents(atPath: url.path) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let item: T = try await dataTransformer.decode(data: data)
return item
}
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let data = try await dataTransformer.encode(item)
let url = fileURL(id: id)
try data.write(to: url)
}
func delete(id: CRUDRepository.ItemIdentifier) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let url = fileURL(id: id)
try fileManager.removeItem(at: url)
}
}
Remplacer le dépôt dans le code client :
print("One item access example")
do {
let clientRecordIdentifier = "client"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
try await repository.create(id: clientRecordIdentifier, item: clientOne)
var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print("Client Name: \(clientRecord.name)")
clientRecord.name = "Busy Client"
try await repository.update(id: clientRecordIdentifier, item: clientRecord)
let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
print("Updated Client Name: \(updatedClient.name)")
try await repository.delete(id: clientRecordIdentifier)
let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print(removedClientRecord)
}
catch {
print(error.localizedDescription)
}
print("Array access example")
let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)
L’initialisation de UserDefaultsRepository a été remplacée par FileSystemRepository, avec les arguments appropriés.
Après avoir exécuté la deuxième version du code client, vous trouverez un répertoire « Clients Database » dans le dossier documents, qui contiendra un fichier d’un tableau sérialisé en JSON avec une structure Client.
Changement de format de stockage des données
Demandons maintenant à ChatGPT de générer un encodeur et un décodeur pour XML :
let formatExtension = "xml"
func encode<T: Encodable>(_ item: T) async throws -> Data {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
return try encoder.encode(item)
}
func decode<T: Decodable>(data: Data) async throws -> T {
let decoder = PropertyListDecoder()
return try decoder.decode(T.self, from: data)
}
}
Grâce aux types intégrés dans Swift, la tâche d’un réseau de neurones devient élémentaire.
Remplacez JSON par XML dans le code client :
print("One item access example")
do {
let clientRecordIdentifier = "client"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: XMLDataTransformer()
)
try await repository.create(id: clientRecordIdentifier, item: clientOne)
var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print("Client Name: \(clientRecord.name)")
clientRecord.name = "Busy Client"
try await repository.update(id: clientRecordIdentifier, item: clientRecord)
let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
print("Updated Client Name: \(updatedClient.name)")
try await repository.delete(id: clientRecordIdentifier)
let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print(removedClientRecord)
}
catch {
print(error.localizedDescription)
}
print("Array access example")
let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: XMLDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)
Le code client a été remplacé par une seule expression JSONDataTransformer -> XMLDataTransformer
Total
Les référentiels CRUD sont l’un des modèles de conception qui peuvent être utilisés pour implémenter un couplage lâche des composants de l’architecture d’application. Une autre des solutions – en utilisant ORM (Object-Relational Mapping), en bref, ORM utilise une approche dans laquelle les structures sont entièrement mappées à la base de données, puis les modifications avec les modèles doivent être affichées (mappées (!)) sur la base de données.
Mais c’est une toute autre histoire.
Une implémentation complète des référentiels CRUD pour Swift est disponible sur :
https://gitlab.com/demensdeum/crud-example
À propos, Swift est pris en charge en dehors de macOS depuis longtemps ; le code de l’article a été entièrement écrit et testé sur Arch Linux.
Sources
https://developer.apple.com/documentation/combine/topleveldecoder
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete