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.
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.
Zur Vereinfachung verwenden wir eine Zeichnung, um die Beziehungen zwischen dem Code und den SQL-Objekten darzustellen:
Konzentrieren wir uns auf jeden Container einzeln.
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).
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.
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 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).
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?:
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.
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:
Um sicherzustellen, dass alles wie erwartet funktioniert, tun wir schließlich noch ein paar Dinge:
Das war's für diese Datei. Jetzt testen wir unsere Arbeit ?.
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.
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!
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