sqlx/reflectxを触ってて、他のRDBMS関係のライブラリのcreate table部分の実装が気になったので調べてみたメモ

これは自分用のメモです。

github.com

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つです。独断と偏見でその場で思いついたものの実装を見てみたという感じです。

gorp

結構素直に愚直に実装されていた。

ent

もう一段回キレイにwrapしている感ある(複雑)。gremlinなどにも対応しているせいかもしれない。

gorm

xorm

はい。

ちょっとした感想

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をそのまま使うことはオススメしない。

参考

このあたりの情報がとても助かった。

公式

記事では使っていないけれど、実際にDockerfileを書くときには便利。

gist


  1. 個人的には常にWORKDIRを指定したい。FHS的には /opt/<app> に置くのが正しい気がするが、一つしか使わないし常に/opt/appになっている。いっその事 /app で良いような気もしないでもない。

  2. pythonにおいてはalpineはあまり推奨されない

  3. この記事を書いてから、 https://hub.docker.com/_/python のHow to use this imageに書いてあったことに気づいた

  4. head -n 1の逆のイメージ。ただし捨てるのではなく標準エラー出力に出力する。ヘッダー付きのcsvみたいな出力を見るときに便利。