¡¿Bienvenidos de nuevo, amigos ?! Hoy, analizamos un caso de uso específico al que podríamos enfrentarnos al mover datos de un lado a otro desde/hacia la base de datos. Primero, permítanme establecer los límites del desafío de hoy. Para ceñirnos a un ejemplo de la vida real, tomemos prestados algunos conceptos del ejército de los EE. UU. Nuestro trato es escribir un pequeño software para guardar y leer a los oficiales con las calificaciones que han obtenido en sus carreras.
Nuestro software necesita manejar a los oficiales del ejército con sus respectivos grados. A primera vista, puede parecer fácil y probablemente no necesitemos ningún tipo de datos personalizados aquí. Sin embargo, para mostrar esta característica, usemos una forma no convencional de representar los datos. Gracias a esto, se nos pide que definamos un mapeo personalizado entre las estructuras Go y las relaciones DB. Además, debemos definir una lógica específica para analizar los datos. Ampliemos esto analizando los objetivos del programa.
Para simplificar las cosas, usemos un dibujo para representar las relaciones entre el código y los objetos SQL:
Centrémonos en cada contenedor uno por uno.
Aquí, definimos dos estructuras. La estructura Grado contiene una lista no exhaustiva de grados militares. Esta estructura no será una tabla en la base de datos. Por el contrario, la estructura Oficial contiene el ID, el nombre y un puntero a la estructura Grado, que indica qué calificaciones ha obtenido el oficial hasta el momento.
Siempre que escribimos un oficial en la base de datos, la columna grados_alcanzados debe contener una serie de cadenas completadas con las calificaciones obtenidas (las que tienen verdadero en la estructura Grado).
En cuanto a los objetos SQL, solo tenemos la tabla de oficiales. Las columnas de identificación y nombre se explican por sí mismas. Luego, tenemos la columna grados_alcanzados que contiene las calificaciones del oficial en una colección de cadenas.
Cada vez que decodificamos a un oficial de la base de datos, analizamos la columna grados_alcanzados y creamos una "instancia" coincidente de la estructura Grado.
Quizás hayas notado que el comportamiento no es el estándar. Debemos hacer algunos arreglos para cumplirlo de la forma deseada.
Aquí, el diseño de los modelos es demasiado complicado a propósito. Utilice soluciones más sencillas siempre que sea posible.
Gorm nos proporciona tipos de datos personalizados. Nos dan una gran flexibilidad a la hora de definir la recuperación y el guardado hacia/desde la base de datos. Debemos implementar dos interfaces: Scanner y Valuer?. El primero especifica un comportamiento personalizado que se aplicará al recuperar datos de la base de datos. Este último indica cómo escribir valores en la base de datos. Ambos nos ayudan a lograr la lógica de mapeo no convencional que necesitamos.
Las firmas de las funciones que debemos implementar son Scan(value interface{}) error y Value() (driver.Value, error). Ahora, veamos el código.
El código de este ejemplo se encuentra en dos archivos: el dominio/models.go y main.go. Comencemos con el primero, que trata de los modelos (traducidos como estructuras en Go).
Primero, déjame presentarte el código de este archivo:
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 }
Ahora, ¿resaltemos las partes relevantes del mismo?:
Gracias a estos dos métodos, podemos controlar cómo enviar y recuperar el tipo Grade durante las interacciones con la BD. Ahora, veamos el archivo main.go.
Aquí, preparamos la conexión de base de datos, migramos los objetos a relaciones (ORM significa Object Relation Mapping), e insertamos y recuperamos registros para probar la lógica. A continuación se muestra el código:
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)) }
Ahora, veamos las secciones relevantes de este archivo. Primero, definimos la función seedDB para agregar datos ficticios en la base de datos. Los datos se encuentran en el archivo data.sql con el siguiente contenido:
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}');
La función main() comienza configurando una conexión de base de datos. Para esta demostración, utilizamos PostgreSQL. Luego, nos aseguramos de que la tabla de oficiales exista en la base de datos y esté actualizada con la versión más reciente de la estructura models.Officer. Dado que este programa es una muestra, hicimos dos cosas adicionales:
Por último, para asegurarnos de que todo funcione como se espera, hacemos un par de cosas:
Eso es todo por este archivo. Ahora, ¿probemos nuestro trabajo?.
Antes de ejecutar el código, asegúrese de que se esté ejecutando una instancia de PostgreSQL en su máquina. Con Docker ?, puedes ejecutar este comando:
docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres
Ahora podemos ejecutar nuestra aplicación de forma segura emitiendo el comando: go run. ?
La salida es:
[ { "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á! Todo funciona como se esperaba. Podemos volver a ejecutar el código varias veces y obtener siempre el mismo resultado.
Espero que hayas disfrutado de esta publicación de blog sobre Gorm y los Tipos de datos personalizados. Siempre recomiendo que te ciñas al enfoque más sencillo. Opte por esto sólo si eventualmente lo necesita. Este enfoque agrega flexibilidad a cambio de hacer que el código sea más complejo y menos robusto (un pequeño cambio en las definiciones de las estructuras podría generar errores y requerir trabajo adicional).
Ten esto en cuenta. Si se apega a las convenciones, puede ser menos detallado en todo su código base.
Esa es una gran cita para finalizar esta publicación de blog.
Si se da cuenta de que se necesitan tipos de datos personalizados, esta publicación de blog debería ser un buen punto de partida para presentarle una solución funcional.
Por favor, déjame saber tus sentimientos y pensamientos. ¡Cualquier comentario siempre es apreciado! Si está interesado en un tema específico, comuníquese con nosotros y lo seleccionaré. ¡Hasta la próxima, mantente a salvo y hasta pronto!
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3