sqlx/reflectxを触ってて、他のRDBMS関係のライブラリのcreate table部分の実装が気になったので調べてみたメモ
これは自分用のメモです。
goでRDBMSを触るときに、sqxは機能が小さくて良いのですが、手元で実行例的なコードを書くときに、SQL側のtable定義とgo側のstructの定義を書いたりするのが面倒になることがあります。実運用はともかくとして、ちょっとした実行例を試すときにstructを定義したらおしまいと言う世界観もそう悪くはありません。
加えて、ElasticsearchやAWSのAthenaやBigQueryなど、新しいデータストアを追加で利用したくなった場合にschemaの定義が要求されることはしばしばあります。その種の定義をgoのstructの定義だけでおしまいにできるという利便性が欲しくなることもありそうでした。
sqlx/reflectx
実は、sqlxの中にreflectxというパッケージが存在していたりします。これはちょっとしたstruct定義とtag定義を見るのに地味に便利な機能を持っていたりします。reflect.Typeを渡してあげるといい感じにタグの情報や型の値などを取り出してくれて便利です。
package main import ( "fmt" "reflect" "github.com/jmoiron/sqlx/reflectx" ) type Person struct { Name string `db:"name,unique"` Age int64 `db:"age,default=20"` } func main() { mapper := reflectx.NewMapper("db") tmap := mapper.TypeMap(reflect.TypeOf(Person{})) for i, info := range tmap.Index { fmt.Printf("%d\t%s\ttags=%+#v\n", i, info.Name, info.Options) } }
このコードを実行すると以下のような出力になります。
$ go run 00reflectx/main.go 0 name tags=map[string]string{"unique":""} 1 age tags=map[string]string{"default":"20"}
このreflectxを使ってお遊びでcreate tableやinsertを手軽に書けるようにするコードを書いてみていました。
ここまでが前置きです。そんなことをやっているうちに他のRDBMS用のライブラリの実装がどうなっているかどうか気になったので調べてみました。
対象のライブラリ
ここからが本題。今までの内容はすべて前置きです。
調べてみたライブラリは以下の4つです。独断と偏見でその場で思いついたものの実装を見てみたという感じです。
- https://github.com/go-gorp/gorp
- https://github.com/facebook/ent
- https://gitea.com/xorm/xorm (https://github.com/go-xorm/xorm)
- https://github.com/go-gorm/gorm
gorp
- CreateTablesIfNotExists() からスタート https://github.com/go-gorp/gorp/blob/master/db.go#L432
- https://github.com/go-gorp/gorp/blob/master/table.go#L173
- 差分はdialetに逃がす https://github.com/go-gorp/gorp/blob/master/dialect.go#L14
- 差分の実装例 https://github.com/go-gorp/gorp/blob/master/dialect_postgres.go#L14
結構素直に愚直に実装されていた。
ent
- schemaのCreatorを実装するらしい https://github.com/ent/ent/blob/master/dialect/sql/schema/migrate.go#L78
- 実態としてはMigrateのstructのcreate()が呼ばれる https://github.com/ent/ent/blob/master/dialect/sql/schema/migrate.go#L154
- init,types,txCreate()してからcommitが呼ばれる
- visitorと言う程でもないけれど、差分はsqlDialectというinterfaceで吸収 https://github.com/ent/ent/blob/master/dialect/sql/schema/migrate.go#L637
- 実装例とテスト https://github.com/ent/ent/blob/master/dialect/sql/schema/postgres.go https://github.com/ent/ent/blob/master/dialect/sql/schema/postgres_test.go
もう一段回キレイにwrapしている感ある(複雑)。gremlinなどにも対応しているせいかもしれない。
gorm
- Migratorというinterfaceがある https://github.com/go-gorm/gorm/blob/master/migrator.go#L33
- 分岐は既に終わってsturctにデータが入っている前提 https://github.com/go-gorm/gorm/blob/master/migrator/migrator.go#L155
- 方言部分の吸収は別リポジトリ。実装例。と言ってもbaseのやつを実行したあとのコメント程度のようだ https://github.com/go-gorm/postgres/blob/master/migrator.go
xorm
- Engine.DumpTables()のときにSQLが出力される https://gitea.com/xorm/xorm/src/branch/master/engine.go#L1034
- DialectのCreateTableSQLで文字列を生成している https://gitea.com/xorm/xorm/src/branch/master/engine.go#L562
- Dialectはこれ https://gitea.com/xorm/xorm/src/branch/master/dialects/dialect.go#L42
- 実装例はこの辺、愚直にそれぞれの方言ごとに実装している https://gitea.com/xorm/xorm/src/branch/master/dialects/postgres.go#L901 base部分はないわけではない https://gitea.com/xorm/xorm/src/branch/master/dialects/dialect.go#L76
- ただし謎がある(?)
- それとは別にEngine.CreateTables()があり、これはsession.createTable()を呼ぶ(?) https://gitea.com/xorm/xorm/src/branch/master/engine.go#L1034
はい。
ちょっとした感想
RDBMSだけを触ることに限って言えば、それぞれのライブラリが結構真面目に作り込んでいるので、わざわざ自前で実装し直す必要もないかなと思ったというのが正直な感想でした。
一方でかなりRDBMSに寄った実装になっている面もあったりするので、冒頭にあった他のデータストア用のschemaへの対応をと考えると、どれもover killな印象を持ちました。加えて、内部での利用に閉じてしまえば、複数の種類のデータベースに対応する必要はなかったり、利用する型の種類もわずかだったりしそうでもう少し手軽にやれるのかなと思ったりはしました。メモなのでこんな感じで突然おわり。
追記
reflectxはreflectベースの実装ですが、これがgo/typesを使った値を利用する静的解析ベースのものだったらどうだろう?というのは少し考えたりしていました。
デモ程度に動くものは手を動かせば作れそうですが、実は何を読み込ませるか指定するのが地味に面倒で何らかのマーカーが必要になったり引数として渡したりする必要があったり、付加的な情報をどう注入するかが悩ましかったりしそうです。
逆にreflectベースでもbuild tags経由でデフォルトではpackageの対象外にして、go generateでgo runを実行するみたいなことをやれば邪魔にならなかったりします(一方で、静的解析ベースの場合は事前にビルドして置けるのが便利だったりします。余談も余談になってしまうのでこのへんで中断)。
gist
pythonのバッチ用のイメージを作りたくなったのでサイズがどれくらいになるか調べてみた
これはかなり個人的なメモ。
コンテナ経由でpythonで作ったバッチを実行しようと思った。そしてイメージのサイズがどれくらいかを大まかに知りたくなった。そんなわけで調べてみた。
サイズを極限まで絞りたいと言う気持ちはなかったのでdistrolessなどは省略。素直にDebianベースのイメージを選んだ。
作成する環境のメモ
baseとなるイメージは python:3.8-slim にしてみた。
$ docker images python:3.8-slim REPOSITORY TAG IMAGE ID CREATED SIZE python 3.8-slim 13172ea67a56 7 days ago 118MB
これに以下のパッケージを追加しただけの状態。
- pandas
- boto3
どういう物を作りたいか雰囲気は察せられそう。
imageのサイズ
先に作ったimageの概略を書いておく。以下のようなサイズになっていた。
REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
---|---|---|---|---|
foo | 0.1.0 | 813317bd2068 | 5 days ago | 291MB |
foo | 0.0.0 | 883eb7b407c1 | 5 days ago | 320MB |
foo 0.0.0が何も考えずに作ったシンプルなイメージ。foo 0.1.0 がマルチステージビルドで頑張ったイメージ
0.0.0
最もシンプルなDockerfileを考えてみる。何も考えずに書いた感じ。
Dockerfile
FROM python:3.8-slim RUN python3 -m pip install pandas boto3 CMD ["python3"]
dockerでbuildしてみる。
docker build -t foo:0.0.1 .
0.1.0
0.1.0はマルチステージビルドで頑張ったもの。以下の記事の内容を省略したもの。
Dockerfile.multi
FROM python:3.8-slim as builder WORKDIR /opt/app COPY requirements.lock /opt/app RUN python3 -m pip install -r requirements.lock FROM python:3.8-slim as runner COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages #COPY --from=builder /usr/local/bin/boto /usr/local/bin/boto CMD ["python3"]
(コメントしているコピーの行は何かconsole_scriptsなどでコマンドをインストールしたときのためのもの) (今見るとWORKDIRを指定する意味がこのDockerfileでは一切無い)
その他細々と思ったことがあるが省略1。
こちらもdockerでビルド
$ docker build -t 0.1.0 -f Dockerfile.multi
requirements.lock
boto3==1.17.12 botocore==1.20.12 jmespath==0.10.0 numpy==1.20.1 pandas==1.2.2 python-dateutil==2.8.1 pytz==2021.1 s3transfer==0.3.4 six==1.15.0 urllib3==1.26.3
どのレイヤーでどれだけ使われているかを見てみる
docker history
でどのレイヤーがどれだけ使っているか分かる。ちなみに、--no-trunc
をつけると省略される部分の全文が見れる。foo:0.0.0の方で見ていると、pipでインストールした時点で200Mb程度使われるようだ。それしかしていないのでそれはそう。
$ docker history foo:0.0.0 --format '{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t{{.CreatedBy}}' 883eb7b407c1 5 days ago 0B /bin/sh -c #(nop) CMD ["python3"] faca00f27605 5 days ago 202MB /bin/sh -c python3 -m pip install boto3 pand… # ここからはbaseのimageのhistory 13172ea67a56 7 days ago 0B /bin/sh -c #(nop) CMD ["python3"] <missing> 7 days ago 9.19MB /bin/sh -c set -ex; savedAptMark="$(apt-ma… <missing> 7 days ago 0B /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… <missing> 7 days ago 0B /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… <missing> 7 days ago 0B /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=21… <missing> 7 days ago 32B /bin/sh -c cd /usr/local/bin && ln -s idle3… <missing> 7 days ago 32.4MB /bin/sh -c set -ex && savedAptMark="$(apt-… <missing> 7 days ago 0B /bin/sh -c #(nop) ENV PYTHON_VERSION=3.8.8 <missing> 2 weeks ago 0B /bin/sh -c #(nop) ENV GPG_KEY=E3FF2839C048B… <missing> 2 weeks ago 7.06MB /bin/sh -c set -eux; apt-get update; apt-g… <missing> 2 weeks ago 0B /bin/sh -c #(nop) ENV LANG=C.UTF-8 <missing> 2 weeks ago 0B /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… <missing> 2 weeks ago 0B /bin/sh -c #(nop) CMD ["bash"] <missing> 2 weeks ago 69.2MB /bin/sh -c #(nop) ADD file:d5c41bfaf15180481…
マルチステージビルドをした方の0.1.0も、表示としてはほぼほぼ同様の形になる。こちらはCOPYの行が主。諸々のインストールなどはbuilderの方で動いているので。
$ docker history foo:0.1.0 --format '{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t{{.CreatedBy}}' 813317bd2068 5 days ago 0B /bin/sh -c #(nop) CMD ["python3"] 20e64f9d4a80 5 days ago 173MB /bin/sh -c #(nop) COPY dir:181eb1f8c5a0da2a6… # base imageの方は省略 13172ea67a56 7 days ago 0B /bin/sh -c #(nop) CMD ["python3"] ...
ほぼpip installでイメージのサイズが倍に
わかったのは、pandasとboto3を入れた瞬間にほぼbase imageの倍のサイズになるということ。alpine2やdistrolessを検討する意味はほぼ無い。
考えてみれば、site-packages以下をduなどで覗いてみれば分かることではあった。すべての依存を見ていないが、botocoreとpandasで100Mを超える。
$ du -sh ~/my/lib/python3.8/site-packages/boto 10M $VIRTUAL_ENV/lib/python3.8/site-packages/boto $ du -sh ~/my/lib/python3.8/site-packages/botocore 41M $VIRTUAL_ENV/lib/python3.8/site-packages/botocore $ du -sh ~/my/lib/python3.8/site-packages/boto3 1.3M $VIRTUAL_ENV/lib/python3.8/site-packages/boto3 $ du -sh ~/my/lib/python3.8/site-packages/pandas 63M $VIRTUAL_ENV/lib/python3.8/site-packages/pandas
追記
pipだけが差分なら pip install --no-cache-dir
でインストールすれば十分では?3
0.0.1がそれ(ioknife4は表示を見やすくするためだけのものなので忘れてしまっても良い)。
$ docker images | ioknife rest | ggrep -P 'foo' REPOSITORY TAG IMAGE ID CREATED SIZE foo 0.0.1 de0f50ba0477 9 seconds ago 286MB foo 0.1.0 813317bd2068 5 days ago 291MB foo 0.0.0 883eb7b407c1 5 days ago 320MB # ここでもう一度base image $ docker images python:3.8-slim REPOSITORY TAG IMAGE ID CREATED SIZE python 3.8-slim 13172ea67a56 7 days ago 118MB
はい。
補足
:warning: tzの設定やaptのupdateとか諸々やれていないので、この記事のDockerfileをそのまま使うことはオススメしない。
参考
このあたりの情報がとても助かった。
- https://future-architect.github.io/articles/20200513/
- https://future-architect.github.io/articles/20200514/
公式
- https://hub.docker.com/_/python
- https://docs.docker.com/develop/develop-images/multistage-build/
- https://docs.docker.com/engine/reference/commandline/history/
記事では使っていないけれど、実際にDockerfileを書くときには便利。
- https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- https://serverfault.com/questions/949991/how-to-install-tzdata-on-a-ubuntu-docker-image