Go no provee ningún manejador de base de datos oficial, a diferencia de otros lenguajes como PHP que si lo hacen. Sin embargo, si provee una estándar de interfaz para que los desarrolladores desarrollen manejadores basados en ella. La ventaja es que si tu código es desarrollado de acuerdo a esa interfaz estándar, no vas a tener que cambiar ningún código si tu base de datos cambia. Veamos cuales son los estándares de esta interface.
Esta función está en el paquete database/sql
para registrar una base de datos cuando usas un manejador de base de datos de terceros. Todos deberían llamar a la función Register(name String, driver driver.Driver)
en la función init()
en orden de registrarse a si mismas.
Vamos a mirar los corespondientes llamados en el código de mymysql y sqlite3:
//manejador https://github.com/mattn/go-sqlite3
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}
//manejador https://github.com/mikespook/mymysql
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
Register("SET NAMES utf8")
sql.Register("mymysql", &d)
}
Vemos que los manejadores de terceros implementan esta función para registrarse a si mismos y Go utiliza un mapa para guardar los manejadores dentro de database/sql
.
var drivers = make(map[string]driver.Driver)
drivers[name] = driver
Así, la función de registro puede registrar tantos manejadores como requieras, cada uno con un nombre diferente.
Siempre veremos el siguiente código cuando usemos manejadores de terceros:
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
Aquí, el guión bajo, (también llamado 'blank') _
puede ser un poco confuso para muchos principiantes, pero es una gran característica de Go. Sabemos que el identificador de guión bajo es usado para no guardar valores que una función retorna, y también que debes usar todos los paquetes que importes en tu código Go. Entonces, cuando el guión bajo es usado con un import, significa que necesitas ejecutar la función init() de ese paquete sin usarlo directamente. lo cual encaja perfectamente en el caso de registrar el manejador de base de datos.
Driver
es una interface que contiene un método Open(name string)
que retorna una interfaz Conn
.
type Driver interface {
Open(name string) (Conn, error)
}
Esta es una conexión de una vez, que significa que solamente puede ser usada una vez por rutina. El siguiente código causará errores:
...
go goroutineA (Conn) // query
go goroutineB (Conn) // insert
...
Porque no no tiene idea de que rutina va a realizar cada operación, la operación de consulta podría obtener el resultado de la operación de incersión y viceversa.
Todos los manejadores de terceros debería tener esta función para analizar el nombre de una Conn y retornar el resultado correcto.
Esta es la interfaz de conexión con algunos métodos, como dije arriba, la misma conexión solo puede usarse una vez por rutina.
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
Prepare
retorna el estado del comando SQL ejecutado para consultas, eliminaciones, etc.Close
cierra la conexión actual y limpia los recursos. La mayoría de paquetes de terceros implementan algún tipo de piscina de conexiones, así que no necesitas almacenar las conexiones que puedan causar errores.Begin
retorna un Tx que representa una transacción para manejar. Puedes usarlo para consultar, almacenar, o volver hacia atrás algunas transacciones.
Este es un estado de listo que corresponde con una Conn, y solamente puede ser usada una vez por rutina (como en el caso de Conn).
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
Close
cierra la conexión actual, pero sigue retornando la información de la ejecución de una operación de consulta.NumInput
retorna el número de argumentos obligados. Los manejadores de bases de datos debería revisar la cantidad de argumentos cuando los resultados son mayor que 0, y retorna -1 cuando el manejador de la base de datos no conoce ningún argumento obligado.Exec
ejecuta un comandoupdate/insert
preparados enPrepare
, retorna unResult
.Query
ejecuta un comandoselect
preparado enPrepare
, retorna información.
Generalmente, las transacciones únicamente tienen métodos de envío y de vuelta atrás, y el manejador de las bases de datos solo necesita implementar estos dos métodos.
type Tx interface {
Commit() error
Rollback() error
}
Esta es una interfaz opcional.
type Execer interface {
Exec(query string, args []Value) (Result, error)
}
El manejador no implementa esta interfaz, cuando llame DB.Exec
, automáticamente llamará Prepare
, retornará un Stmt
. Después ejecutará el método Exec
de Smtt
, y cerrará el Smtm
.
Esta es la interface para los operaciones de update/insert
.
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
LastInsertId
retorna el identificador de autoincremento después de una operación de inserción.RowsAffected
retorna la cantidad de filas afectadas por la operación.
Esta interfaz es el resultado de una operación de consulta.
type Rows interface {
Columns() []string
Close() error
Next(dest []Value) error
}
Columns
retorna la información de los campos de la base de datos. El segmento tiene correspondencia con los campos SQL únicamente. y no retorna todos los campos de la tabla de la base de datos.Close
cierra el iterador sobre las columnas.Next
retorna la siguiente información y la asigna a dest, convirtiendo todas las cadenas en arreglos de bytes, y retorna un error io.EOF si no existe mas data disponible.
Este es un alias para int64, pero implementado por la interfaz Result.
type RowsAffected int64
func (RowsAffected) LastInsertId() (int64, error)
func (v RowsAffected) RowsAffected() (int64, error)
Esta es una interfaz vacía que contiene cualquier tipo de información.
type Value interface{}
El Value
debe ser algo con lo que los manejadores puedan operar, o nil, así que debería ser alguno de los siguientes tipos:
int64
float64
bool
[]byte
string [*] Exceptuando los Rows.Next, que no puede retornar una cadena.
time.Time
Define una interfaz para convertir valores nativos en valores de driver.Value.
type ValueConverter interface {
ConvertValue(v interface{}) (Value, error)
}
Esta interfaz es comunmente usada en manejadores de bases de datos y tiene muchas características útiles:
- Convierte un driver.Value al tipo de campo correspondiente, por ejemplo convierte un int64 a un uint16.
- Conviernte una consulta de base de datos a un driver.Value.
- Conviernte un driver.Value a un usuario definido en la función
scan
.
Define una interfaz para devolver un driver.Value.
type Valuer interface {
Value() (Value, error)
}
Muchos tipos implementan esta interface para la conversión entre driver.Value y ellos mismos.
En este punto, deberías saber un poco mas sobre el desarrollo de manejadores de bases de datos en Go. una vez que hayas implementado interfaces para operaciones como agregar, eliminar, actualizar, etc, existirán algunos problemas relacionados con comunicarse específicamente con algún tipo de bases de datos.
database/sql
define en un alto nivel métodos existentes en database/sql/driver
para tener las operaciones en un nivel mas conveniente, y sugiera que implementes una piscina de conexiones.
type DB struct {
driver driver.Driver
dsn string
mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed bool
}
Como puedes ver, la función Open
retorna una base de datos que tiene una freeConn
, y esta es una piscina de conexiones simple. Su implementación es simple y fea. Usa defer db.putConn(ci, err)
en la función Db.Prepare
para colocar una conexión en la piscina de conexiones. Siempre que se vaya a llamar la función Conn
, verifica la longitud de freeConn
. Si es mayor a 0, significa que hay una conexión reusable y la retorna directamente a ti. De otra manera, crea una nueva conexión y la retorna.
- Índice
- Sección anterior: Bases de Datos
- Sección siguiente: MySQL