「バンドルカード」をはじめとするキャッシュレス決済事業を運営する、株式会社カンムです。
当社は小規模ながらインターンの制度があり、開発やマーケティングの領域で現役学生の皆さんにご活躍いただいています。昨秋に新卒社員第一号となったテイさんのように、卒業後の入社実績も出てきました。
2018年12月からバックエンドチームのエンジニアインターン生となった _pongzuさんは、現在活躍中のおひとりです。
このたび、彼の手による初仕事「go-sqlfmt(SQL文整形ツール)」が公開されました。
インターン開始時にはプログラミング歴半年弱だったという彼が、どのような苦労を経て本ツールの開発に至ったのか?
当社Tech Blog(@Medium)からの転載で紹介いたします!
go-sqlfmtを作りました
初めに
初めまして、昨年12月からカンムのバックエンドチームにインターンとして入ったpongzuです。
今回、初仕事であるgo-sqlfmtを公開したので、その紹介と作ってみた感想等を書きます。
go-sqlfmtとは
https://github.com/kanmu/go-sqlfmt
go-sqlfmtはgoファイル内のSQL文を整形するツールです。
カンムではAPIサーバのDBアクセスの実装において、ORMを用いずに開発しているため、
database/sqlをそのまま利用し、
QueryRow(`select * from xxx`)
Query(`select * from xxx`)
Exec(`select * from xxx`)
といった形でSQLを書きます。
このように各自が自由なフォーマットでSQLを記述することで以下2点の問題が生じます。
- 見た目に統一感がない
- どのフォーマットルールに従うか考える時間が無駄になる
go-sqlfmtはこれらの問題を解決するために作られました。
開発手順
ファイルからSQL文を見つける
- GoのファイルをASTにする
- ここから Name:QueryRow,Query , ExecのValueを取り出す
***カンムではdatabase/sqlをそのまま利用しているため、引数に生SQLを受け取る上記3つの関数のValueを取得する***
112 . . . . . . . 0: *ast.CallExpr {
113 . . . . . . . . Fun: *ast.SelectorExpr {
114 . . . . . . . . . X: *ast.CallExpr {
115 . . . . . . . . . . Fun: *ast.SelectorExpr {
116 . . . . . . . . . . . X: *ast.SelectorExpr {
117 . . . . . . . . . . . . X: *ast.Ident {
118 . . . . . . . . . . . . . NamePos: dummy.go:11:16
119 . . . . . . . . . . . . . Name: “sql”
120 . . . . . . . . . . . . }
121 . . . . . . . . . . . . Sel: *ast.Ident {
122 . . . . . . . . . . . . . NamePos: dummy.go:11:20
123 . . . . . . . . . . . . . Name: “DB”
124 . . . . . . . . . . . . }
125 . . . . . . . . . . . }
126 . . . . . . . . . . . Sel: *ast.Ident {
127 . . . . . . . . . . . . NamePos: dummy.go:11:23
128 . . . . . . . . . . . . Name: “QueryRow”
129 . . . . . . . . . . . }
130 . . . . . . . . . . }
131 . . . . . . . . . . Lparen: dummy.go:11:31
132 . . . . . . . . . . Args: []ast.Expr (len = 1) {
133 . . . . . . . . . . . 0: *ast.BasicLit {
134 . . . . . . . . . . . . ValuePos: dummy.go:11:32
135 . . . . . . . . . . . . Kind: STRING
136 . . . . . . . . . . . . Value: “\”select * from table_name\”
見つけたSQL文をtokenizeする
- 文字列を端から1文字づつスキャン
- 意味のある識別子ごとにtokenizeする
SQLの句ごとにグルーピングする
- グループ化対象のキーワード(SELECT, FROM, JOIN..など)が現れたらそのグループが終了するまでトークンを調べていく。
ここで行ったグループ化はインデントする単位となる。
例) SELECT句ならFROMが現れるまでトークンを解析する - この時、検索対象のグループ内でさらにグループ化対象のキーワードが現れたら、再帰的に処理して、SQL がネストされている場合も綺麗にフォーマットされるようにする。
***入れ子のグループが何個あるか数えて、そのグループのインデントレベルを保存しておく***
グループごとにインデントする
最後にASTにインデント済みの文字列を保存する
効果
go-sqlfmtを使用する事で、上記に挙げた2点の問題は解決され、統一感のあるSQLが実現されます。
その結果、エンジニアがSQLのフォーマットを考える事にリソースを割く必要がなくなり、開発効率が上がると思います。
また、まだTODOですが、go-sqlfmtをエディタやgit hook に組み込む事で保存時に自動で整形できるようになります。
TODO
エディタやgitなどの各種ツールにインテグレーションする事を意識した設計に書き換える
- 現在、膝さん[抜粋注:CTOのmururu/knee]がテスト的にvimとvscodeへのインテグレーションを以下に書いてくれているので是非使ってみて下さい。
https://github.com/mururu/vim-go
https://github.com/mururu/vscode-go
tokenizeとグルーピングの間に、意味のある塊を取得する処理を挟むようにする
- 現在は、例えば、 GROUP がGROUP BY かWITHIN GROUPかを区別する処理が無いため、より多くの構文に対応するためには当該処理が必要となります。
トークンに位置情報を与える
- 現在は、例えば、(が関数の開始か、サブクエリの開始か、単なる括弧なのかを判別する位置情報が無いため、それをトークンに与え、後にインデントをかけやすくする必要があります。
全体的にリファクタリングをする
- 最後のインデント処理や、途中のグルーピング処理などでかなり愚直な書き方をしてしまっているので全体的にリファクタリングをしていくつもりです。
学んだ事
私がgo-sqlfmtを作り初めた当時はプログラミング経験が半年未満で、本当に何から初めたら良いのか右も左も分からない状態からのスタートでした。そこから何とか動くものを作ることができる過程において、僕が学んだ事を以下に書きます。
動くもの作り続ける事
「まずは動くものを小さく作る。」
これはメンターである佐野さん[抜粋注:フロントエンドエンジニアのhiroakis]に何度も言われた事です。当たり前と言えば当たり前の事なのですが、僕はこれがどれだけ大事かを理解してませんでした。そのため、「抽象的なコードを書かねば」と初めから焦り、余計なinterfaceを定義し、しかも、その動かないコードをプッシュしてプルリクエストを送るというとんでもない事をしておりました…。
今では、
- 動くものを書く
- テストを書く
- コミットする
- リファクタリングをする
という順番で開発をするように心がけるようになりました。
小さく作る事を意識する
「まずは動くものを小さく作る」と共通している事ですが、目標を決めて、そのゴールを達成するために必要なフローを小さい単位で分割し、計画を立てるようになりました。
例えば、go-sqlfmtの場合、まず1番基本的なselect * from xxx を整形して、ターミナルに出力するという目標を決めます。そうすると、大雑把に以下4点のフローが必要になります。
- ファイルからsql文を見つける
- sql文を select と * と from xxx に分解する
- 分解された文字列を端から調べ、selectなら改行 + SELECT、*なら
改行 + 空白 + *...といった形で標準出力に出力する
そこから、それぞれのフローで必要な事をさらに分解し、最小単位まで落とし込んだ段階でブランチを切って実際にコードを書いていきます。このような工程で作業することにより、「あれもこれもしないといけない…」と考えて余計なコードを書いたり、考えたりする必要がなくなります。また、複雑な処理もこのように分解する事で徐々にできるようになってくるという実感が持てます。
テストをかく
僕はカンムに入るまでテストを全く書いたことがなく、何のためにテストを書くのかよく理解しておりませんでした。しかし、テストは動くものを作り続けるという意味において凄いパワーを発揮することを理解するようになりました。
ただ動くものを作るだけではテストは恐らく必要ありませんが、動き続けるものを作るためにはテストは絶対に必要です。なぜなら、何か変更を加えた時やリファクタリングをした時に、もしバグが起きれば、テストで何が間違っているか1発で教えてくれるからです。
特にgo-sqlfmtの場合、例えば、 tokenize の処理を変更すると、その後の処理全てに影響が出るため、テストをしっかり書いておかないと変更にかなり時間がかかってしまいます。
このように、テストを書く重要性を理解した事で、安心して変更を加える事ができるようになりました。
パッケージやメソッドの責務を分ける
go-sqlfmtの場合パッケージ構成は以下のようになっております。
├── cmd
│ └── sqlfmt
├── lexer
├── parser
└── group
この場合、lexerパッケージは字句解析だけを担当するはずです。しかし、ANDが改行の後に来るのか否か?のような僕が後の処理で必要となる情報をトークンに加えるような事をしてしまっていました。
責務が混在するパッケージ構成にした場合、1つの変更であらゆる障害が起きたり、どこに何が書いてあるかよく分からなくなったりと、色々と問題が起きます。
まだまだ未完全ですが、go-sqlfmtを書いたことで、パッケージやメソッドの責務を意識するようになれました。その結果、責務ごとのテストをかけるのでテストも書きやすくなり、前より見通しがよく、変更に強いコードが少し書けるようになったと思います。これらの他にも沢山のことを学ばせて頂きましたが、ここでは割愛させて頂きます。
感想
以上のようにgo-sqlfmtを作った事で、沢山のことを学ばせて頂きました。
これもメンターである佐野さんや、カンムのエンジニアの方々が優しく教えてくれたおかげです…。
まだまだ課題は多いですが、徐々に直して、本当に良いものに仕上げていきたいと思っております!!
また、プルリクエストを頂ければ対応いたしますので、宜しくお願いします!