„Wenn ein Arbeiter seine Arbeit gut machen will, muss er zuerst seine Werkzeuge schärfen.“ – Konfuzius, „Die Gespräche des Konfuzius. Lu Linggong“
Titelseite > Programmierung > Gorm: Vorgeschmack auf benutzerdefinierte Datentypen

Gorm: Vorgeschmack auf benutzerdefinierte Datentypen

Veröffentlicht am 08.11.2024
Durchsuche:830

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 auf jeden Container einzeln.

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

Bezüglich der SQL-Objekte 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: domain/models.go und 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 Merkmale 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 geben
  4. zurück.
  5. 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 rufen 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))
}

Sehen wir uns nun die relevanten Abschnitte dieser Datei an. Zuerst definieren wir die Funktion „seedDB“, um Dummy-Daten in die Datenbank einzufügen. Die Daten befinden sich in der Datei data.sql mit folgendem Inhalt:

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}');

Die Funktion main() beginnt mit dem Einrichten einer DB-Verbindung. Für diese Demo haben wir PostgreSQL verwendet. Anschließend stellen wir sicher, dass die Tabelle „Officers“ in der Datenbank vorhanden und mit der neuesten Version der Struktur „models.Officer“ auf dem neuesten Stand ist. Da es sich bei diesem Programm um ein Beispiel handelt, haben wir zwei zusätzliche Dinge getan:

  • Entfernung der Tabelle am Ende der main()-Funktion (wenn das Programm beendet wird, möchten wir auch die Tabelle entfernen)
  • Seeding einiger Dummy-Daten

Um sicherzustellen, dass alles wie erwartet funktioniert, tun wir schließlich noch ein paar Dinge:

  1. Alle Datensätze in der Datenbank abrufen
  2. Hinzufügen (und Zurückholen) eines neuen Offiziers

Das war's für diese Datei. Jetzt testen wir unsere Arbeit ?.

Der Moment der Wahrheit

Bevor Sie den Code ausführen, stellen Sie bitte sicher, dass eine PostgreSQL-Instanz auf Ihrem Computer ausgeführt wird. Mit Docker ? können Sie diesen Befehl ausführen:

docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres

Jetzt können wir unsere Anwendung sicher ausführen, indem wir den Befehl go run ausgeben. ?

Die Ausgabe ist:

[
        {
                "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à! Alles funktioniert wie erwartet. Wir können den Code mehrmals erneut ausführen und erhalten immer die gleiche Ausgabe.

Das ist ein Wrap

Ich hoffe, Ihnen hat dieser Blogbeitrag zu Gorm und den benutzerdefinierten Datentypen gefallen. Ich empfehle Ihnen immer, den einfachsten Ansatz zu wählen. Entscheiden Sie sich dafür nur, wenn Sie es irgendwann brauchen. Dieser Ansatz erhöht die Flexibilität und macht den Code im Gegenzug komplexer und weniger robust (eine geringfügige Änderung in den Strukturdefinitionen kann zu Fehlern und zusätzlichem Arbeitsaufwand führen).

Denken Sie daran. Wenn Sie sich an Konventionen halten, können Sie in Ihrer gesamten Codebasis weniger ausführlich vorgehen.

Das ist ein tolles Zitat zum Abschluss dieses Blogbeitrags.
Wenn Sie erkennen, dass benutzerdefinierte Datentypen erforderlich sind, sollte dieser Blogbeitrag ein guter Ausgangspunkt sein, um Ihnen eine funktionierende Lösung vorzustellen.

Bitte teilen Sie mir Ihre Gefühle und Gedanken mit. Wir freuen uns immer über Feedback! Wenn Sie an einem bestimmten Thema interessiert sind, kontaktieren Sie mich und ich werde es in die engere Auswahl nehmen. Bis zum nächsten Mal, bleiben Sie gesund und bis bald!

Freigabeerklärung Dieser Artikel ist abgedruckt unter: https://dev.to/ossan/gorm-sneak-peek-of-custom-data-types-97n?1 Bei Verstößen wenden Sie sich bitte an [email protected], um ihn zu löschen
Neuestes Tutorial Mehr>

Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.

Copyright© 2022 湘ICP备2022001581号-3