Maison >développement back-end >Golang >Gorm : aperçu des types de données personnalisés
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 sauvegarder 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 regardant les cibles du programme ?.
Pour simplifier 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 structures 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)) }
Now, let's see the relevant sections of this file. First, we define the seedDB function to add dummy data in the DB. The data lives in the data.sql file with the following content:
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}');
The main() function starts by setting up a DB connection. For this demo, we used PostgreSQL. Then, we ensure the officers table exists in the database and is up-to-date with the newest version of the models.Officer struct. Since this program is a sample, we did two additional things:
Lastly, to ensure that everything works as expected, we do a couple of things:
That's it for this file. Now, let's test our work ?.
Before running the code, please ensure that a PostgreSQL instance is running on your machine. With Docker ?, you can run this command:
docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres
Now, we can safely run our application by issuing the command: go run . ?
The output is:
[ { "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à! Everything works as expected. We can re-run the code several times and always have the same output.
I hope you enjoyed this blog post regarding Gorm and the Custom Data Types. I always recommend you stick to the most straightforward approach. Opt for this only if you eventually need it. This approach adds flexibility in exchange for making the code more complex and less robust (a tiny change in the structs' definitions might lead to errors and extra work needed).
Keep this in mind. If you stick to conventions, you can be less verbose throughout your codebase.
That's a great quote to end this blog post.
If you realize that Custom Data Types are needed, this blog post should be a good starting point to present you with a working solution.
Please let me know your feelings and thoughts. Any feedback is always appreciated! If you're interested in a specific topic, reach out, and I'll shortlist it. Until next time, stay safe, and see you soon!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!