Heim >Backend-Entwicklung >Golang >Gorm: Vorgeschmack auf benutzerdefinierte Datentypen
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 einzeln auf jeden Container.
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).
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.
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: der domain/models.go und der 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)) }
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!
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!