Bienvenue, les amis ?! Aujourd'hui, nous discutons d'un cas d'utilisation spécifique auquel nous pourrions être confrontés lors du déplacement de données d'avant en arrière depuis/vers la base de données. Tout d’abord, permettez-moi de fixer les limites du défi d’aujourd’hui. Pour s'en tenir à un exemple concret, empruntons quelques concepts à l'armée américaine ?. Notre contrat est d'écrire un petit logiciel pour enregistrer et lire les officiers avec les notes qu'ils ont obtenues au cours de leur carrière.
Notre logiciel doit gérer les officiers de l'armée avec leurs grades respectifs. À première vue, cela peut sembler simple et nous n'avons probablement pas besoin d'un type de données personnalisé ici. Cependant, pour mettre en valeur cette fonctionnalité, utilisons une manière non conventionnelle de représenter les données. Grâce à cela, on nous demande de définir un mappage personnalisé entre les structures Go et les relations DB. De plus, nous devons définir une logique spécifique pour analyser les données. Développons cela en examinant les objectifs du programme ?.
Pour faciliter les choses, utilisons un dessin pour représenter les relations entre le code et les objets SQL :
Concentrons-nous sur chaque conteneur un par un.
Ici, nous avons défini deux structures. La structure Grade contient une liste non exhaustive de grades militaires ?️. Cette structure ne sera pas une table dans la base de données. À l'inverse, la structure Officier contient l'ID, le nom et un pointeur vers la structure Grade, indiquant les grades obtenus par l'officier jusqu'à présent.
Chaque fois que nous écrivons un officier dans la base de données, la colonne grades_achieved doit contenir un tableau de chaînes remplies avec les notes obtenues (celles avec true dans la structure Grade).
Concernant les objets SQL, nous n'avons que la table des officiers. Les colonnes id et name sont explicites. Ensuite, nous avons la colonne grades_achieved qui contient les notes de l'officier dans une collection de chaînes.
Chaque fois que nous décodons un officier de la base de données, nous analysons la colonne grades_achieved et créons une « instance » correspondante de la structure Grade.
Vous avez peut-être remarqué que le comportement n’est pas standard. Nous devons prendre certaines dispositions pour le réaliser de la manière souhaitée.
Ici, la disposition des modèles est volontairement trop compliquée. Veuillez vous en tenir à des solutions plus simples autant que possible.
Gorm nous fournit des types de données personnalisés. Ils nous donnent une grande flexibilité dans la définition de la récupération et de la sauvegarde vers/depuis la base de données. Il faut implémenter deux interfaces : Scanner et Valuer ?. Le premier spécifie un comportement personnalisé à appliquer lors de la récupération des données de la base de données. Ce dernier indique comment écrire les valeurs dans la base de données. Les deux nous aident à réaliser la logique de cartographie non conventionnelle dont nous avons besoin.
Les signatures des fonctions que nous devons implémenter sont Scan(value interface{}) error et Value() (driver.Value, error). Maintenant, regardons le code.
Le code de cet exemple se trouve dans deux fichiers : le domain/models.go et le main.go. Commençons par le premier, celui des modèles (traduits par structs dans Go).
Tout d'abord, permettez-moi de vous présenter le code de ce fichier :
package models import ( "database/sql/driver" "slices" "strings" ) type Grade struct { Lieutenant bool Captain bool Colonel bool General bool } type Officer struct { ID uint64 `gorm:"primaryKey"` Name string GradesAchieved *Grade `gorm:"type:varchar[]"` } func (g *Grade) Scan(value interface{}) error { // we should have utilized the "comma, ok" idiom valueRaw := value.(string) valueRaw = strings.Replace(strings.Replace(valueRaw, "{", "", -1), "}", "", -1) grades := strings.Split(valueRaw, ",") if slices.Contains(grades, "lieutenant") { g.Lieutenant = true } if slices.Contains(grades, "captain") { g.Captain = true } if slices.Contains(grades, "colonel") { g.Colonel = true } if slices.Contains(grades, "general") { g.General = true } return nil } func (g Grade) Value() (driver.Value, error) { grades := make([]string, 0, 4) if g.Lieutenant { grades = append(grades, "lieutenant") } if g.Captain { grades = append(grades, "captain") } if g.Colonel { grades = append(grades, "colonel") } if g.General { grades = append(grades, "general") } return grades, nil }
Maintenant, soulignons les parties pertinentes ? :
Grâce à ces deux méthodes, nous pouvons contrôler la manière d'envoyer et de récupérer le type Grade lors des interactions avec la base de données. Maintenant, regardons le fichier main.go.
Ici, nous préparons la connexion à la base de données, migrons les objets vers les relations (ORM signifie Object Relation Mapping), et insérons et récupérons enregistrements pour tester la logique. Ci-dessous le code :
package main import ( "encoding/json" "fmt" "os" "gormcustomdatatype/models" "gorm.io/driver/postgres" "gorm.io/gorm" ) func seedDB(db *gorm.DB, file string) error { data, err := os.ReadFile(file) if err != nil { return err } if err := db.Exec(string(data)).Error; err != nil { return err } return nil } // docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres func main() { dsn := "host=localhost port=54322 user=postgres password=postgres dbname=postgres sslmode=disable" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { fmt.Fprintf(os.Stderr, "could not connect to DB: %v", err) return } db.AutoMigrate(&models.Officer{}) defer func() { db.Migrator().DropTable(&models.Officer{}) }() if err := seedDB(db, "data.sql"); err != nil { fmt.Fprintf(os.Stderr, "failed to seed DB: %v", err) return } // print all the officers var officers []models.Officer if err := db.Find(&officers).Error; err != nil { fmt.Fprintf(os.Stderr, "could not get the officers from the DB: %v", err) return } data, _ := json.MarshalIndent(officers, "", "\t") fmt.Fprintln(os.Stdout, string(data)) // add a new officer db.Create(&models.Officer{ Name: "Monkey D. Garp", GradesAchieved: &models.Grade{ Lieutenant: true, Captain: true, Colonel: true, General: true, }, }) var garpTheHero models.Officer if err := db.First(&garpTheHero, 4).Error; err != nil { fmt.Fprintf(os.Stderr, "failed to get officer from the DB: %v", err) return } data, _ = json.MarshalIndent(&garpTheHero, "", "\t") fmt.Fprintln(os.Stdout, string(data)) }
Voyons maintenant les sections pertinentes de ce fichier. Tout d’abord, nous définissons la fonction seedDB pour ajouter des données factices dans la base de données. Les données se trouvent dans le fichier data.sql avec le contenu suivant :
INSERT INTO public.officers (id, "name", grades_achieved) VALUES(nextval('officers_id_seq'::regclass), 'john doe', '{captain,lieutenant}'), (nextval('officers_id_seq'::regclass), 'gerard butler', '{general}'), (nextval('officers_id_seq'::regclass), 'chuck norris', '{lieutenant,captain,colonel}');
La fonction main() commence par établir une connexion DB. Pour cette démo, nous avons utilisé PostgreSQL. Ensuite, nous nous assurons que la table des officiers existe dans la base de données et qu'elle est à jour avec la dernière version de la structure models.Officer. Puisque ce programme est un exemple, nous avons effectué deux choses supplémentaires :
Enfin, pour nous assurer que tout fonctionne comme prévu, nous procédons comme suit :
C'est tout pour ce fichier. Maintenant, testons notre travail ?.
Avant d'exécuter le code, veuillez vous assurer qu'une instance PostgreSQL est en cours d'exécution sur votre machine. Avec Docker ?, vous pouvez exécuter cette commande :
docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres
Maintenant, nous pouvons exécuter notre application en toute sécurité en émettant la commande : go run . ?
Le résultat est :
[ { "ID": 1, "Name": "john doe", "GradesAchieved": { "Lieutenant": true, "Captain": true, "Colonel": false, "General": false } }, { "ID": 2, "Name": "gerard butler", "GradesAchieved": { "Lieutenant": false, "Captain": false, "Colonel": false, "General": true } }, { "ID": 3, "Name": "chuck norris", "GradesAchieved": { "Lieutenant": true, "Captain": true, "Colonel": true, "General": false } } ] { "ID": 4, "Name": "Monkey D. Garp", "GradesAchieved": { "Lieutenant": true, "Captain": true, "Colonel": true, "General": true } }
Voilà ! Tout fonctionne comme prévu. Nous pouvons réexécuter le code plusieurs fois et avoir toujours le même résultat.
J'espère que vous avez apprécié cet article de blog concernant Gorm et les Types de données personnalisés. Je vous recommande toujours de vous en tenir à l’approche la plus simple. Optez pour cela uniquement si vous en avez éventuellement besoin. Cette approche ajoute de la flexibilité en échange de rendre le code plus complexe et moins robuste (un petit changement dans les définitions des structures pourrait entraîner des erreurs et un travail supplémentaire nécessaire).
Gardez cela à l’esprit. Si vous respectez les conventions, vous pouvez être moins verbeux dans votre base de code.
C'est une excellente citation pour terminer cet article de blog.
Si vous réalisez que des types de données personnalisés sont nécessaires, cet article de blog devrait être un bon point de départ pour vous présenter une solution efficace.
S'il vous plaît, faites-moi part de vos sentiments et de vos pensées. Tout commentaire est toujours apprécié ! Si un sujet spécifique vous intéresse, contactez-nous et je le présélectionnerai. En attendant la prochaine fois, restez prudent et à bientôt !
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3