Cualquier aplicación que trabaje con consultas SQL puede beneficiarse del uso de un generador de consultas para mejorar la legibilidad, el mantenimiento y la seguridad del código. De hecho, hay muchas bibliotecas diferentes que hacen precisamente eso en Golang. Aquí en Vaunt, probamos muchas opciones diferentes antes de decidir finalmente crear una nosotros mismos. En última instancia, queríamos algo que fuera seguro y que proporcionara reemplazo de variables para evitar la inyección de SQL y al mismo tiempo fuera legible y capaz de tener declaraciones condicionales. Entonces creamos una nueva biblioteca llamada tqla, lanzada y anunciada a fines del año pasado. Puedes leer más sobre esto en este artículo.
Antes de crear tqla, utilizábamos principalmente Squirrel para nuestra lógica de creación de consultas SQL, y lo recomendamos encarecidamente. Todavía usamos Squirrel en algunas áreas, pero gradualmente comenzamos a reemplazar e implementar una nueva lógica de creación de consultas con tqla. Hemos encontrado muchos casos en los que tqla ha mejorado nuestra capacidad para mantener nuestro código y solucionar problemas que encontramos al usar otros creadores de declaraciones.
En Vaunt, recientemente realizamos una migración de base de datos de CockroachDB a TiDB. Si bien CockroachDB era eficaz y confiable, finalmente nos enfrentamos a la decisión de agregar a nuestra pila tecnológica una base de datos OLAP. La necesidad de esto era respaldar nuestra carga de trabajo analítica en nuestro producto de conocimiento de la comunidad de código abierto. Para mantener pequeña nuestra huella tecnológica, decidimos seguir adelante con TiDB y aprovechar la arquitectura HTAP de la base de datos.
CockroachDB es ampliamente compatible con PostgreSQL y utilizamos la sintaxis de PostgreSQL para muchas de nuestras consultas SQL. Para cambiar a TiDB, tuvimos que cambiar algunas de nuestras tablas y actualizar consultas para usar la sintaxis MySQL. En algunos lugares durante la migración, descubrimos que estábamos usando incorrectamente declaraciones de creación de consultas condicionales y carecíamos de las pruebas adecuadas para detectar que las declaraciones se estaban generando incorrectamente.
En el README de Squirrel, hay un ejemplo de cómo se puede utilizar la creación de consultas condicionales para actualizar declaraciones con filtros opcionales:
if len(q) > 0 { users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%")) }
Aquí hay un ejemplo real, pero simplificado, de cómo actualizamos una de nuestras consultas para unir tablas condicionalmente y agregar un filtro opcional:
psql := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Question) statementBuilder := psql.Select(`i.id`). From("vaunt.installations i"). Where(`entity_name = ?`, name) if len(provider) > 0 { statementBuilder.Where(`provider = ?`, provider) } if len(repo) > 0 { statementBuilder.Join(`repositories as r on JSON_CONTAINS(i.repositories, CONCAT('["', r.id, '"]'))`) statementBuilder.Where(`r.name = ?`, repo) }
¿Puedes detectar el problema con el código? Si no, no se preocupe: es algo que también se nos pasó por alto en nuestras propias revisiones de código hasta que realizamos nuestras pruebas.
El problema aquí es que nos olvidamos de actualizar el generador de declaraciones con el resultado de las funciones del generador. Por ejemplo, el filtro de condición del proveedor debería leer:
if len(provider) > 0 { statementBuilder = statementBuilder.Where(`provider = ?`, provider) }
Este es un error relativamente sencillo de cometer y se puede detectar fácilmente con suficientes casos de prueba, pero debido a que no es un código técnicamente inválido, puede tomar un poco de tiempo darse cuenta de lo que está sucediendo de inmediato.
Otro problema de legibilidad con esta configuración es que la unión condicional está separada de la declaración de selección inicial. Podríamos reorganizar el constructor para colocar cada pieza donde debe ir, pero requeriría múltiples comprobaciones de declaraciones condicionales duplicadas y aún sufriría algunos problemas de legibilidad.
La demostración anterior usando Squirrel se ha reescrito desde entonces y el equivalente en tqla se ve así:
t, err := tqla.New(tqla.WithPlaceHolder(tqla.Question)) if err != nil { return nil, err } query, args, err := t.Compile(` SELECT i.id FROM vaunt.installations as i {{ if .Repo }} JOIN vaunt.repositories as r on JSON_CONTAINS(i.repositories, CONCAT('["', r.id, '"]'), '$') {{ end }} WHERE entity_name = {{ .Name}} {{ if .Provider }} AND i.provider = {{ .Provider }} {{ end }} {{ if .Repo }} AND r.name = {{ .Repo }} {{ end }} `, data) if err != nil { return nil, err }
Como puede ver, la sintaxis de la plantilla para tqla hace que la incorporación de cláusulas condicionales sea muy sencilla. Tqla reemplaza automáticamente las variables que estamos configurando con nuestros marcadores de posición especificados y proporciona los argumentos que podemos usar con nuestro controlador SQL para ejecutar la declaración.
Al igual que Squirrel, este enfoque de creación de declaraciones es fácil de probar, ya que podemos crear diferentes conjuntos de objetos de datos para pasarlos al generador de plantillas y validar la salida.
Puede ver que podemos agregar fácilmente partes condicionales de la consulta donde mejor encajen. Por ejemplo, aquí tenemos un JOIN condicional directamente después de la instrucción FROM y, aunque todavía tenemos varias comprobaciones de condición, no complica demasiado la plantilla.
Otra característica interesante de tqla que ayuda a mejorar la capacidad de mantenimiento de nuestros constructores SQL es la capacidad de definir funciones personalizadas que podríamos usar en las plantillas para abstraer cierta lógica de transformación.
Aquí hay un ejemplo de cómo usamos una función para convertir el valor time.Time de Golang en sql.NullTime para permitirnos realizar una inserción con nuestros objetos de datos sin necesidad de convertirlos de antemano:
funcs := template.FuncMap{ "time": func(t time.Time) sql.NullTime { if t.IsZero() { return sql.NullTime{Valid: false} } return sql.NullTime{Time: t, Valid: true} }, } t, err := tqla.New(tqla.WithPlaceHolder(tqla.Question), tqla.WithFuncMap(funcs)) if err != nil { return err }
Con esta función definida en nuestro mapa de funciones tqla, ahora podemos usarla libremente en nuestras plantillas de consulta proporcionándole un parámetro del objeto de datos que es un campo time.Time. Incluso podemos llamar a esta función varias veces en la misma plantilla con diferentes campos.
Aquí hay un ejemplo simplificado:
statement, args, err := t.Compile(` INSERT INTO events (name, created_at, merged_at, closed_at) VALUES ( {{ .Name }}, {{ time .CreatedAt }}, {{ time .MergedAt }}, {{ time .ClosedAt }} )`, eventData)
En conclusión, creemos que el uso de tqla puede ayudar a mejorar la capacidad de mantenimiento de la lógica de creación de consultas y, al mismo tiempo, ofrecer una poderosa utilidad para crear consultas dinámicas. La simplicidad de la estructura de la plantilla permite una legibilidad clara del código y puede agilizar la depuración de posibles errores.
Hicimos que tqla sea de código abierto para compartir esta biblioteca con la esperanza de que proporcione una buena opción para otros usuarios que desean una forma sencilla, fácil de mantener y segura de crear consultas SQL en muchos tipos diferentes de aplicaciones.
Si estás interesado, consulta el repositorio y dale una estrella si te ayuda de alguna manera. ¡No dudes en realizar cualquier solicitud de función o informar de errores!
Siempre estamos abiertos a recibir comentarios y contribuciones.
Para mantenerte informado sobre desarrollos futuros, ¡síguenos en X o únete a nuestro Discord!
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