Heim  >  Artikel  >  Backend-Entwicklung  >  Gorm: Vorgeschmack auf benutzerdefinierte Datentypen

Gorm: Vorgeschmack auf benutzerdefinierte Datentypen

DDD
DDDOriginal
2024-09-13 20:15:36709Durchsuche

Willkommen zurück, Leute?! Heute besprechen wir einen konkreten Anwendungsfall, mit dem wir konfrontiert werden könnten, wenn wir Daten hin und her von/zur Datenbank verschieben. Lassen Sie mich zunächst die Grenzen für die heutige Herausforderung festlegen. Um bei einem realen Beispiel zu bleiben, leihen wir uns einige Konzepte von der US-Armee? Unser Deal besteht darin, eine kleine Software zu schreiben, um die Offiziere mit den Noten, die sie in ihrer Karriere erreicht haben, zu speichern und auszulesen.

Gorms benutzerdefinierte Datentypen

Unsere Software muss die Armeeoffiziere mit ihren jeweiligen Dienstgraden verwalten. Auf den ersten Blick mag es einfach erscheinen, und wir benötigen hier wahrscheinlich keinen benutzerdefinierten Datentyp. Um diese Funktion jedoch zu verdeutlichen, verwenden wir eine unkonventionelle Methode zur Darstellung der Daten. Aus diesem Grund werden wir gebeten, eine benutzerdefinierte Zuordnung zwischen Go-Strukturen und DB-Beziehungen zu definieren. Darüber hinaus müssen wir eine spezifische Logik zum Parsen der Daten definieren. Lassen Sie uns dies näher erläutern, indem wir uns die Ziele des Programms ansehen.

Zu behandelnder Anwendungsfall

Zur Vereinfachung verwenden wir eine Zeichnung, um die Beziehungen zwischen dem Code und den SQL-Objekten darzustellen:

Gorm: Sneak Peek of Custom Data Types

Konzentrieren wir uns einzeln auf jeden Container.

Die Go-Strukturen?

Hier haben wir zwei Strukturen definiert. Die Grade-Struktur enthält eine nicht erschöpfende Liste militärischer Grade ?️. Diese Struktur wird keine Tabelle in der Datenbank sein. Umgekehrt enthält die Officer-Struktur die ID, den Namen und einen Zeiger auf die Grade-Struktur, die angibt, welche Grade der Officer bisher erreicht hat.

Immer wenn wir einen Beamten in die DB schreiben, muss die Spalte grades_achieved eine Reihe von Zeichenfolgen enthalten, die mit den erreichten Noten gefüllt sind (diejenigen mit true in der Grade-Struktur).

Die DB-Beziehungen?

Was die SQL-Objekte betrifft, haben wir nur die Officers-Tabelle. Die Spalten „id“ und „name“ sind selbsterklärend. Dann haben wir die Spalte „grades_achieved“, die die Noten des Offiziers in einer Sammlung von Zeichenfolgen enthält.

Immer wenn wir einen Beamten aus der Datenbank dekodieren, analysieren wir die Spalte „grades_achieved“ und erstellen eine passende „Instanz“ der Grade-Struktur.

Möglicherweise ist Ihnen aufgefallen, dass das Verhalten nicht dem Standard entspricht. Wir müssen einige Vorkehrungen treffen, um es in der gewünschten Weise zu erfüllen.

Hier ist das Layout der Modelle absichtlich zu kompliziert. Bitte bleiben Sie nach Möglichkeit bei einfacheren Lösungen.

Benutzerdefinierte Datentypen

Gorm stellt uns benutzerdefinierte Datentypen zur Verfügung. Sie geben uns große Flexibilität bei der Definition des Abrufs und Speicherns in/aus der Datenbank. Wir müssen zwei Schnittstellen implementieren: Scanner und Valuer? Ersteres gibt ein benutzerdefiniertes Verhalten an, das beim Abrufen von Daten aus der Datenbank angewendet werden soll. Letzteres gibt an, wie Werte in die Datenbank geschrieben werden. Beide helfen uns dabei, die unkonventionelle Mapping-Logik zu erreichen, die wir brauchen.

Die Signaturen der Funktionen, die wir implementieren müssen, sind Scan(value interface{}) error und Value() (driver.Value, error). Schauen wir uns nun den Code an.

Der Kodex

Der Code für dieses Beispiel befindet sich in zwei Dateien: der domain/models.go und der main.go. Beginnen wir mit dem ersten, dem Umgang mit den Modellen (übersetzt als Strukturen in Go).

Die Datei domain/models.go

Lassen Sie mich zunächst den Code für diese Datei vorstellen:

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
}

Lassen Sie uns nun die relevanten Teile davon hervorheben?:

  1. Die Notenstruktur listet nur die Noten auf, die wir in unserer Software prognostiziert haben
  2. Die Officer-Struktur definiert die Eigenschaften der Entität. Diese Entität ist eine Relation in der DB. Wir haben zwei Gorm-Notationen angewendet:
    1. gorm:"primaryKey" im ID-Feld, um es als Primärschlüssel unserer Beziehung zu definieren
    2. gorm:"type:varchar[]", um das Feld GradesAchieved als Varchar-Array in der Datenbank abzubilden. Andernfalls wird es als separate DB-Tabelle oder zusätzliche Spalten in der Offizierstabelle übersetzt
  3. Die Grade-Struktur implementiert die Scan-Funktion. Hier erhalten wir den Rohwert, passen ihn an, legen einige Felder für die g-Variable fest und kehren zurück
  4. Die Grade-Struktur implementiert auch die Value-Funktion als Wertempfängertyp (diesmal müssen wir den Empfänger nicht ändern, wir verwenden nicht die *-Referenz). Wir geben den Wert zurück, der in die Spalte „grades_achieved“ der Offizierstabelle geschrieben werden soll

Dank dieser beiden Methoden können wir steuern, wie der Typ „Grade“ während DB-Interaktionen gesendet und abgerufen wird. Schauen wir uns nun die Datei main.go an.

Die main.go-Datei?

Hier bereiten wir die DB-Verbindung vor, migrieren die Objekte in Beziehungen (ORM steht für Object Relation Mapping) und fügen sie ein und holen sie ab Datensätze, um die Logik zu testen. Unten ist der 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!

Das obige ist der detaillierte Inhalt vonGorm: Vorgeschmack auf benutzerdefinierte Datentypen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn