Maison  >  Article  >  développement back-end  >  Gorm : aperçu des types de données personnalisés

Gorm : aperçu des types de données personnalisés

DDD
DDDoriginal
2024-09-13 20:15:36709parcourir

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.

Types de données personnalisés de Gorm

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 ?.

Cas d'utilisation à gérer

Pour faciliter les choses, utilisons un dessin pour représenter les relations entre le code et les objets SQL :

Gorm: Sneak Peek of Custom Data Types

Concentrons-nous sur chaque conteneur un par un.

Les Go Structs ?

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).

Les relations DB ?

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.

Types de données personnalisés

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

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).

Le fichier domaine/models.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 ?:

  1. La structure Grade répertorie uniquement les notes que nous avons prévues dans notre logiciel
  2. La structure Officer définit les caractéristiques de l'entité. Cette entité est une relation dans la base de données. Nous avons appliqué deux notations Gorm :
    1. gorm:"primaryKey" sur le champ ID pour le définir comme clé primaire de notre relation
    2. gorm:"type:varchar[]" pour mapper le champ GradesAchieved en tant que tableau de varchar dans la base de données. Sinon, cela se traduit par une table DB distincte ou des colonnes supplémentaires dans la table des officiers
  3. La structure Grade implémente la fonction Scan. Ici, on récupère la valeur brute, on l'ajuste, on définit quelques champs sur la variable g, et on retourne
  4. La structure Grade implémente également la fonction Value en tant que type de récepteur de valeur (nous n'avons pas besoin de changer de récepteur cette fois, nous n'utilisons pas la référence *). On renvoie la valeur à écrire dans la colonne grades_achieved de la table des officiers

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.

Le fichier main.go ?

Ici, nous préparons la connexion DB, migrons les objets vers des 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:

  • Removal of the table at the end of the main() function (when the program terminates, we would like to remove the table as well)
  • Seeding of some dummy data

Lastly, to ensure that everything works as expected, we do a couple of things:

  1. Fetching all the records in the DB
  2. Adding (and fetching back) a new officer

That's it for this file. Now, let's test our work ?.

The Truth Moment

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.

That's a Wrap

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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn