🪤

【Go】ハードコードしているSQLをバイナリに埋め込んですっきりさせよう

2023/02/18に公開

はじめに

Goを書いていて、実行するSQLをハードコードしていることはないでしょうか

例えば、短いSQLだとこんなかんじ

https://pkg.go.dev/database/sql#example-DB.QueryContext

	age := 27
	rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
	if err != nil {
		log.Fatal(err)
	}

この例であれば、構文ミスがあっても実行前に気づくこともあるでしょう

ただ、次のように複数行にわたってSQLを書いてしまうこともあるのではないでしょうか

 	sqls := []string{
		`UPDATE employees
		SET salary = 5000;
		`,
		`UPDATE employees
		SET salary = salary * 1.1
		WHERE salary <= 10000;
		`,
		`UPDATE employees
		SET salary = 5000
		WHERE department = 'Sales';
		`,
		`UPDATE employees
		SET salary = 5000
		WHERE first_name = 'John' AND last_name = 'Doe';
		`,
		`UPDATE employees
		SET salary = salary * 1.1
		WHERE job_title = 'Manager' AND department = 'Sales';
		`,
	}

	for _, sql :=range sqls {
		_, err := tx.ExecContext(ctx, sql)
		if err != nil {
			log.Fatal(err)
		}
	}

O/Rマッパーをつかわない分、サッとSQLを用意して実行できるのは魅力的です
しかし、複数行にまたがるのでコードの可読性や、SQLの構文チェックがしにくいなどの懸念があると思います

SQLをファイルに分けよう

1つの解決策として、SQLをファイルに分けるという方法があるかと思います
そうすることで、コードの可読性は上がるでしょう

	input, _ := os.ReadFile("input.sql")

	// ファイルから読み込んだ複数のクエリを 1クエリずつ実行できるように処理
	sqls := Something(input)

	for _, sql := range sqls {
		_, err := tx.ExecContext(ctx, sql)
		if err != nil {
			log.Fatal(err)
		}
	}

ただし、読み込んだファイルを有効なSQLにする処理は自分で実装する必要があります

SQLファイルをバイナリに埋め込もう

そこで、今回つくったものがこちらです

https://github.com/uh-zz/sqload

このライブラリは、SQLファイルを読み込み1クエリずつ実行できる形に変換します

以下サンプルです

package main

import (
	"bytes"
	"embed"
	"fmt"

	"github.com/uh-zz/sqload"
	"github.com/uh-zz/sqload/driver/mysql"
)

//go:embed sql/*
var content embed.FS

func main() {
	var (
		buf  bytes.Buffer // sql which read from file
		sqls []string // sql after parse
	)

	loader := sqload.New(mysql.Dialector{}) // for PostgreSQL: postgresql.Dialector{}

	if err := loader.Load(&content, &buf); err != nil {
		fmt.Printf("Load error: %s", err.Error())
	}

	if err := loader.Parse(buf.String(), &sqls); err != nil {
		fmt.Printf("Parse error: %s", err.Error())
	}

	fmt.Printf("%+v", sqls)
    // [INSERT INTO table001 (name,age) VALUES ('alice', 10);]
}

go:embedディレクティブをつかってSQLファイルを実行バイナリに埋め込みます
こうすることで、プログラムからファイルを読み込むより効率的です(実行ファイルを配布するだけでよいです)

加えて、アピールポイントとしては、以下2つです

1. SQLファイルから読み込んだSQLが有効であるかをParseするときに検証します

Parserは以下を使用しています

MySQL

MySQL互換の分散DBであるTiDBのParserです

https://github.com/pingcap/tidb/tree/master/parser

PostgreSQL

分散DBであるCockroachDBから分離されたPostgreSQL Parserです

https://github.com/auxten/postgresql-parser

2. 任意のSQLクライアントを使用できる

単に、読み込んだSQLを[]stringにするだけなので、SQLを実行するクライアントを任意に選べます

さいごに

現状、構文のサポートしているのは、MySQL, PostgreSQLのみになります

今後は、Parserを自前で実装したり、サポートするシステムを拡張できればと思います

IssueやPull Requestも歓迎ですので、どしどし送ってください!

もし気に入っていただけたらGitHubのスターとTwitterのフォローをよろしくおねがいします笑

Discussion