文化を回す順番が回ってきた。あるいは、ソフトウェアエンジニアとして受け取ってきたものの記録
去年から仕事で新卒の子を二人見てきた。二人とも優秀だなぁと思うのと同時に、自分が見られる側ではなく見ている側になっていた事実から私にも順番が回ってきたなと明確に意識をし始めた。ここでいう順番とはソフトウェアエンジニアの文化を次世代へ伝える順番である。
退屈だと思われることを承知で自分語りをさせてほしい。
Linuxとプログラミングに没頭し文化を受け取った学生時代
私は学生時代にLinuxを触り、UNIX的な考え方やソフトウェアの思想に触れた。そこでは教員からUNIXという考え方、ソフトウェアとは何かというものを教わった。 当初Windowsしか知らなかった私はLinuxの思想にハマって先輩や教員に質問に質問を重ねていた。教員や先輩もめんどそうな顔をしながらも可愛がってもらったことを覚えている。
特別扱いしてもらったのか、春休みの間に学内に立てていたOpenPNEの更新を手伝ったことはもう15年も前だけど少し誇らしかった。
年上の教員からハッカーとは素晴らしいものだと力説されたが、この時点ではピンときていなかった。ここで文化を受け取ったなぁと振り返ると思う。
ハッカー文化の門を潜った瞬間
学生時代からLinuxを触って他の新卒より多少プログラミングができることに親近感を覚えたのか、職場のボスからあるDVDを借りた。 もうタイトルも思い出せず内容も霞がかっているが、Mozilla、旧Netscapeの人たちがOSSというものを生み出した。そいつらはOSSを使って世の中をハックしている。俺たちもOSSを盛り上げて世の中をハックしていこうぜという内容だったと思う。
そこに感銘を受けた私はOSSのコードを読み漁るようになった。当時触っていたPHPのフレームワークEthna、Symfony辺りだったと記憶している。 職場のボスは正直私のことはどうでも良く共通の話題が欲しかっただけだったんじゃないかと推測をしている。一方でここで文化を広げる火を受け取ったと思っている。
ここで受け取った火のお陰で同世代より多少は良い報酬を貰えて、飯を食えているので感謝をしている。
上の世代の口癖の正体
そこからブラック企業に入り、心身を壊した後に拾ってもらった会社では上の世代の人から色んな支援を受けた。 ほとんどの人は見て見ぬふりをしていた。それは当然のことだろう。そんな中でも見ていられなかったのか・業務上の判断としてか、一部の人は支援をしてくれた。
それは食事を奢ってもらうという金銭的なこと、ミスした仕事を裏で助けてくれたこと、心身の不調をサポートしてくれたことなど本当に色んな支援を受けた。
ある程度復活して返そうとすると総じて皆こう言っていた。
次の若者に回してくれ
当時は「いい人だな」「ありがたいな」「こういう人がいるんだな」ぐらいに思っていたが、これは一方向の善意ではなく、受け取った者が次に渡すことを前提にした文化だったんだなと確信を抱いている。
この言葉はただ優しくしろとか面倒を見ろではない。若手が潰れるとチームは弱くなる、組織文化は途切れる、次が育たない、最終的に自分たちも孤立するという打算もあるはず。 それでも仕事への向き合い方や思想・判断基準を超えた何かを途切れさせるなって意味なんだろうなと考えている。
おそらく上の世代も私と同じように、何かを受け取ってきたのだろう。そして自分が受け取ったものをここで断ち切らせないというほとんど信仰に近い倫理を引き受けていたのだと思う。
それはソフトウェアエンジニアの文化に限らず、もっと大きな、人から人へと受け渡されていく継承の文化の火を灯し続けたい。という思いや願いだったのではないか?と今は振り返っている。
文化を回す順番が回ってきた
そして今、私は仕事で若者を見ている。そこで思ったことは自分が主役として与えられる側だった順番が終わり、上の世代がやっていた文化を次世代へ回す順番が回ってきた。あの時の上の世代の人たちと同じ立場に立っている。
私のハッカー思想を誰かに押し付けたいわけではない。ただ、文化の支援を受けてきた一人として、受け取った火をここで止めたくはないと思っている。 例えば、ミスを公開処刑にさせないこと、困っていそうなときに一言だけ声をかけること、背中で守ること。そうした小さな判断の積み重ねが文化なんだと思う。
これはそういう立場になった、というだけの話。そしてそれがうまくできているかどうかは、きっと次の世代が決めることなのだと思う。
Realize から Air に移行し、 Go(echo) + Air + docker-compose でホットリロードを利用して開発を行う。
この記事は Go 2 Advent Calendar 2020 の4日目の記事です。
tl;dr
- Goでホットリロード(a.k.a Live reload)を利用する際には今までは Realize というライブラリを使っている人が多いが、開発が止まっている。
- issueも放置され、go modの対応もされておらず、2020/10/25現在のモダンな環境で構築を行うと詰まる点が多い。
- 代わりに Air というライブラリを利用してホットリロードを行う。
Go + docker-composeを利用してホットリロードを利用した開発を行いたいと検索をして一番多かったのがrealizeというライブラリだった。
が、開発が停滞しているようで、issueも溜まっており、go modの対応もされておらず、2020/10/25 現在の環境で新規に作ろうとすると、いくつもハマる点があった。
他にライブラリは無いかといくつか調べたところ、Airというライブラリが開発も頻繁にされており、star数も多く筋が良さそうだったので、こちらを利用することにした。
利用方法
Usageにもあるが、
- .air.toml ファイルを作成する
- repositoryの.air_example.toml をコピーペーストし、自分の環境に合わせる
- コマンドを実行する
air
.air.tomlの設定について
repositoryにexampleがあるので、それを参考にする。
air/air_example.toml at 71d8bf298b426e631c7f51947e8d706246b4ff16 · cosmtrek/air · GitHub
設定ファイルもそんなに難しくなく、ある程度はわかると思う。特に重要な点は
cmdbin
cmd は自分のGo Appをbuildするコマンドを記述する。
bin は cmd でbuildしたbinaryのpathを記述する
実際に利用するシーンを見たほうが早いので、GitHubにpushしたコードを参考に構築する。
ディレクトリ構成は下記
$ tree -I frontend . ├── LICENSE ├── README.md ├── backend │ ├── Dockerfile │ ├── cmd │ │ └── app │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── handler.go │ │ ├── root.go │ │ └── routes.go │ ├── models │ └── repositories └── docker-compose.yml 6 directories, 10 files
実際にホットリロードを行う。今回の場合の.air.tomlの構成は下記
$ cd backend/ $ go get -u github.com/cosmtrek/air $ air -c .air.toml __ _ ___ / /\ | | | |_) /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0 watching . watching cmd watching cmd/app watching handlers watching models watching repositories !exclude tmp building... running... ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:5000 $ curl localhost:5000 {"result":true}
handlers/root.go に手を入れてみる。
$ air -c .air.toml __ _ ___ / /\ | | | |_) /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0 watching . watching cmd watching cmd/app watching handlers watching models watching repositories !exclude tmp building... running... ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:5000 handlers/root.go has changed building... running... ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:5000 $ $ curl localhost:5000 {"result":false}
とJSON Responseが変わっていることがわかる。
docker-compose
ここまではローカル環境で行ってきたが、目的としてContainer上で行いたいのでdocker-composeで行ってみる。
Dockerfile
docker-compose.yml
$ docker-compose build api Building api Step 1/7 : FROM golang:1.15-alpine as base ---> b3bc898ad092 Step 2/7 : WORKDIR /app/go/base ---> Using cache ---> 99b11653d662 Step 3/7 : COPY go.mod . ---> 8d6ca0f99b03 Step 4/7 : COPY go.sum . ---> 72e5d889c991 Step 5/7 : RUN go mod download ---> Running in 5b61f9b803ee Removing intermediate container 5b61f9b803ee ---> 05068d1b4923 Step 6/7 : RUN go get -u github.com/cosmtrek/air ---> Running in dbefc40ddd77 go: downloading github.com/cosmtrek/air v1.21.2 go: github.com/cosmtrek/air upgrade => v1.21.2 go: downloading github.com/fsnotify/fsnotify v1.4.9 go: downloading github.com/creack/pty v1.1.9 go: downloading github.com/imdario/mergo v0.3.8 go: downloading github.com/pelletier/go-toml v1.6.0 go: downloading github.com/fatih/color v1.7.0 go: github.com/pelletier/go-toml upgrade => v1.8.1 go: github.com/mattn/go-colorable upgrade => v0.1.8 go: github.com/mattn/go-isatty upgrade => v0.0.12 go: github.com/imdario/mergo upgrade => v0.3.11 go: github.com/fatih/color upgrade => v1.9.0 go: github.com/creack/pty upgrade => v1.1.11 go: golang.org/x/sys upgrade => v0.0.0-20201022201747-fb209a7c41cd go: downloading github.com/fatih/color v1.9.0 go: downloading github.com/imdario/mergo v0.3.11 go: downloading github.com/creack/pty v1.1.11 go: downloading golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd go: downloading github.com/pelletier/go-toml v1.8.1 go: downloading github.com/mattn/go-colorable v0.1.8 Removing intermediate container dbefc40ddd77 ---> 7a9749558df6 Step 7/7 : COPY . . ---> 37c82f03a414 Successfully built 37c82f03a414 Successfully tagged go-nextjs-playground_api:latest $ docker-compose up api Recreating go-nextjs-playground_api_1 ... done Attaching to go-nextjs-playground_api_1 api_1 | api_1 | __ _ ___ api_1 | / /\ | | | |_) api_1 | /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0 api_1 | api_1 | mkdir /app/go/base/tmp api_1 | watching . api_1 | watching cmd api_1 | watching cmd/app api_1 | watching handlers api_1 | watching models api_1 | watching repositories api_1 | !exclude tmp api_1 | building... api_1 | running... api_1 | api_1 | ____ __ api_1 | / __/___/ / ___ api_1 | / _// __/ _ \/ _ \ api_1 | /___/\__/_//_/\___/ v4.1.16 api_1 | High performance, minimalist Go web framework api_1 | https://echo.labstack.com api_1 | ____________________________________O/_______ api_1 | O\ api_1 | ⇨ http server started on [::]:5000
$ curl localhost:5000 {"result":true}
また handler/root.go のコードを変える。今回はJSON Responseの値をtrueからfalseに変更する。
api_1 | handlers/root.go has changed api_1 | building... api_1 | running... api_1 | api_1 | ____ __ api_1 | / __/___/ / ___ api_1 | / _// __/ _ \/ _ \ api_1 | /___/\__/_//_/\___/ v4.1.16 api_1 | High performance, minimalist Go web framework api_1 | https://echo.labstack.com api_1 | ____________________________________O/_______ api_1 | O\ api_1 | ⇨ http server started on [::]:5000
$ curl localhost:5000 {"result":false}
Containerを起動したまま、レスポンスが変わっていること(ホットリロードされていること)がわかる。
まとめ
- Realizeは開発が停滞しており、今後扱うと負債になる可能性が高い。
- 代わりにAirというライブラリを利用することで、ホットリロード (a.k.a ライブロード) を利用することが出来るのでそちらを利用する。
ref
参考Project
各種PR
階層構造(a.k.a ツリー構造・ディレクトリ構造・フォルダ)をDBでどう設計すべきか
階層構造とは
いわゆるTree Strcture(木構造)。
例として上げるのであれば、
ここではフォルダを想定する。
隣接リスト(Adjacency List)
Tree-Structure/adjacency_list at main · teitei-tk/Tree-Structure · GitHub
単純に考えると、各フォルダに親のフォルダを参照させるやり方が想定される。 しかし、このままだとフォルダ構成が深い場合、単一のSQLで取得することが難しい。実際に見てみる。
フォルダ構成は下記

table
CREATE TABLE IF NOT EXISTS `adjacency_list` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `parent_id` bigint(20), `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ノードデータ
INSERT INTO `adjacency_list` ( parent_id, name ) VALUES ( null, "Home" ), ( 1, "Downloads" ), ( 2, "20201126" ), ( 1, "Desktop" ), ( 1, "Documents" ), ( 5, "Adobe" ), ( 6, "PremierePro" ), ( 6, "PhotoShop" )
このデータを一括で取得する場合は下記のSQLになる。
SELECT * FROM adjacency_list as al LEFT OUTER JOIN adjacency_list as al2 ON al.parent_id = al2.id LEFT OUTER JOIN adjacency_list as al3 ON al2.parent_id = al3.id LEFT OUTER JOIN adjacency_list as al4 ON al3.parent_id = al4.id
結果は下記の通りになる。
| id | parent_id | name | id | parent_id | name | id | parent_id | name | id | parent_id | name |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL |
| 2 | 1 | Downloads | 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL |
| 3 | 2 | 20201126 | 2 | 1 | Downloads | 1 | NULL | Home | NULL | NULL | NULL |
| 4 | 1 | Desktop | 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL |
| 5 | 1 | Documents | 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL |
| 6 | 5 | Adobe | 5 | 1 | Documents | 1 | NULL | Home | NULL | NULL | NULL |
| 7 | 6 | PremierePro | 6 | 5 | Adobe | 5 | 1 | Documents | 1 | NULL | Home |
| 8 | 6 | PhotoShop | 6 | 5 | Adobe | 5 | 1 | Documents | 1 | NULL | Home |
クエリを見ての通り階層分 n回 LEFT OUTER JOINで直近の子と親を取得しているが、 このクエリのままではすべてのフォルダを一括で取得することが出来ない。やるとしても大量かつ複雑なSQLが必要になる。
この隣接リストモデルの代替となるものには、下記のような設計がある。
- 経路列挙モデル(Path Enumeration)
- 入れ子集合モデル(Nested Set)
- 閉包テーブルモデル(Closure Table)など
順に解説をする
経路列挙 (Path Enumeration)
Tree-Structure/path_enumeration at main · teitei-tk/Tree-Structure · GitHub
経路列挙モデル(Path Enumeration)は先祖の系譜を表す文字列をノードとして扱うことで解決している。
具体的にはUnixのディレクトリ構造。eg. /bin/sh
テーブル定義
CREATE TABLE IF NOT EXISTS `path_enumeration` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `path` varchar(1000), `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ノードデータ
INSERT INTO `path_enumeration` ( path, name ) VALUES ( "1/", "Home" ), ( "1/2/", "Downloads" ), ( "1/2/3/", "20201126" ), ( "1/4/", "Desktop" ), ( "1/5/", "Documents" ), ( "1/5/6/", "Adobe" ), ( "1/5/6/7/", "PremierePro" ), ( "1/5/6/8/", "PhotoShop" );
結果
| id | path | name |
|---|---|---|
| 1 | 1/ | Home |
| 2 | 1/2/ | Downloads |
| 3 | 1/2/3/ | 20201126 |
| 4 | 1/4/ | Desktop |
| 5 | 1/5/ | Documents |
| 6 | 1/5/6/ | Adobe |
| 7 | 1/5/6/7/ | PremierePro |
| 8 | 1/5/6/8/ | PhotoShop |
例として、PhotoShopディレクトリのすべての先祖を取得するクエリは下記のようになる。
SELECT pe.*, CONCAT(pe.path, "%") as LikeQuery FROM path_enumeration AS pe WHERE "1/5/6/8" LIKE CONCAT(pe.path, "%");
結果
| id | path | name | LikeQuery |
|---|---|---|---|
| 1 | 1/ | Home | 1/% |
| 5 | 1/5/ | Documents | 1/5/% |
| 6 | 1/5/6/ | Adobe | 1/5/6/% |
| 8 | 1/5/6/8/ | PhotoShop | 1/5/6/8% |
CONCAT(pe.path, "%")が味噌。クエリ結果にLikeQueryを表示しているのでわかると思うが先祖のパスがLIKEパターンマッチしている。具体的には下記のノードと一致する。
1/%1/5/%1/5/6/%
子孫を取得する場合にはLIKE句の引数を逆にする。
SELECT pe.*, CONCAT("1/5/", "%") AS LikeQuery FROM path_enumeration AS pe WHERE pe.path LIKE CONCAT("1/5/", "%");
結果
| id | path | name | LikeQuery |
|---|---|---|---|
| 5 | 1/5/ | Documents | 1/5/% |
| 6 | 1/5/6/ | Adobe | 1/5/% |
| 7 | 1/5/6/7/ | PremierePro | 1/5/% |
| 8 | 1/5/6/8/ | PhotoShop | 1/5/% |
1/5/%は下記のノードと一致する。
1/5/1/5/6/1/5/6/7/1/5/6/8/
ノードの挿入
Home以下にMusicというノードを挿入する。
Query
INSERT INTO `path_enumeration` ( name ) VALUES ( "Music" ); Update `path_enumeration` SET path = CONCAT(( SELECT x.path FROM ( SELECT path FROM path_enumeration WHERE id = 1 ) as x ), LAST_INSERT_ID()) WHERE id = LAST_INSERT_ID();
主キーがAutoIncremntの場合、挿入時点では自分のIDがわからないのでpathを構築出来ない。一旦pathを除いたノードを挿入し、Update文でpathを更新する。
デメリット
- pathの管理はアプリケーションのコードに依存することになる。validationが複雑になりメンテンス性の心配が残る。
- pathの型はvarchar型で、無限に保存出来るわけではない。
入れ子集合(Nested Set)
Tree-Structure/nested_set at main · teitei-tk/Tree-Structure · GitHub
入れ子集合(Nested Set)は、直近の親ではなく、子孫の集合に関する情報を各ノードに保存する。各ノードのnsleftおよびnsrightと呼ばれる数値で階層を表現する。
テーブル定義
CREATE TABLE IF NOT EXISTS `nested_set` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `nsleft` integer NOT NULL, `nsright` integer NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ノードデータ
INSERT INTO `nested_set` ( name, nsleft, nsright ) VALUES ( "Home", 1, 15 ), ( "Downloads", 2, 5 ), ( "20201126", 3, 4 ), ( "Desktop", 6, 7 ), ( "Documents", 8, 14 ), ( "Adobe", 9, 12 ), ( "PremierePro", 10, 11 ), ( "PhotoShop", 12, 13 )
| id | name | nsleft | nsright |
|---|---|---|---|
| 1 | Home | 1 | 15 |
| 2 | Downloads | 2 | 5 |
| 3 | 20201126 | 3 | 4 |
| 4 | Desktop | 6 | 7 |
| 5 | Documents | 8 | 14 |
| 6 | Adobe | 9 | 12 |
| 7 | PremierePro | 10 | 11 |
| 8 | PhotoShop | 12 | 13 |
例として、Documents以下のノードを取得する場合は下記のクエリになる。
SELECT s2.* FROM nested_set as s1 INNER JOIN nested_set as s2 ON s2.nsleft BETWEEN s1.nsleft AND s1.nsright WHERE s1.id = 5
BEETWEENで範囲指定しているところが味噌。この例で言うと、id = 5であるDocumentsのnsleft.8とnsright.14の間にある、nsleftの値を持っているすべての子孫ノードを取得することが出来る。
| id | name | nsleft | nsright |
|---|---|---|---|
| 5 | Documents | 8 | 14 |
| 6 | Adobe | 9 | 12 |
| 7 | PremierePro | 10 | 11 |
| 8 | PhotoShop | 12 | 13 |
先祖を取得する場合は逆にする。
SELECT s1.*, s2.* FROM nested_set as s1 INNER JOIN nested_set as s2 ON s1.nsleft BETWEEN s2.nsleft AND s2.nsright WHERE s1.id = 5
id = 5であるDocumentsのnsleft.8の値が nsleftとnsrightの間にあるすべてのノードを取得出来る。この例でいうと、nsleftが1でnsrightが15であるHomeが該当する。
デメリット
- ノードの挿入や移動などの際にツリー構造の操作が他のモデルに比べて複雑になる。そのためノードの挿入が頻繁に求められる場合は最適解にはならない。
- ノードの挿入では新しいノードの左(nsleft)の値より大きいすべての左右のノードの値を計算し直す必要がある。
閉包テーブル(Closure Table)
Tree-Structure/closure_table at main · teitei-tk/Tree-Structure · GitHub
閉包テーブル(Closure Table)は今までもモデルとは違い直接の親子関係だけではなく、ツリー全体のパスをテーブルに格納する。
テーブル定義
今までのようなフォルダの定義に加えて、TreePathというテーブル定義する。
CREATE TABLE IF NOT EXISTS `closure_table` ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `tree_paths` ( `ancestor` BIGINT(20) UNSIGNED NOT NULL, `descendant` BIGINT(20) UNSIGNED NOT NULL, PRIMARY KEY (`ancestor`, `descendant`), FOREIGN KEY (`ancestor`) REFERENCES closure_table(`id`), FOREIGN KEY (`descendant`) REFERENCES closure_table(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
閉包テーブルでは今までモデルとは違い、ツリー構造のデータをTreePathsテーブルに保存する。このテーブルの各行には先祖(ancestor)/子孫(descendant)を共有するノードの組み合わせを保存する。これはツリー状の離れた位置にあるノードも含めたすべてのノードが対象になる。また、各行のノードには自分自身を参照するレコードも追加する。
ノードデータ
INSERT INTO `closure_table` ( name ) VALUES ( "Home" ), ( "Downloads" ), ( "20201126" ), ( "Desktop" ), ( "Documents" ), ( "Adobe" ), ( "PremierePro" ), ( "PhotoShop" ); INSERT INTO `tree_paths` ( ancestor, descendant ) VALUES ( 1, 1 ), ( 1, 2 ), ( 1, 3 ), ( 1, 4 ), ( 1, 5 ), ( 1, 6 ), ( 1, 7 ), ( 1, 8 ), ( 2, 2 ), ( 2, 3 ), ( 3, 3 ), ( 4, 4 ), ( 5, 5 ), ( 5, 6 ), ( 5, 7 ), ( 5, 8 ), ( 6, 6 ), ( 6, 7 ), ( 6, 8 ), ( 7, 7 ), ( 8, 8 );
closure_table
| id | name |
|---|---|
| 1 | Home |
| 2 | Downloads |
| 3 | 20201126 |
| 4 | Desktop |
| 5 | Documents |
| 6 | Adobe |
| 7 | PremierePro |
| 8 | PhotoShop |
tree_paths
| ancestor | descendant |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 1 | 3 |
| 2 | 3 |
| 3 | 3 |
| 1 | 4 |
| 4 | 4 |
| 1 | 5 |
| 5 | 5 |
| 1 | 6 |
| 5 | 6 |
| 6 | 6 |
| 1 | 7 |
| 5 | 7 |
| 6 | 7 |
| 7 | 7 |
| 1 | 8 |
| 5 | 8 |
| 6 | 8 |
| 8 | 8 |

ノードを取得するクエリは入れ子集合よりも更に簡単になる。ID=2のDownloads以下を取得するにはtree_pathsをJoinして、anestor(先祖)が2の行を探すだけ。
SELECT c.* FROM closure_table as c INNER JOIN tree_paths as t ON c.id = t.descendant WHERE t.ancestor = 2
result
| id | name |
|---|---|
| 2 | Downloads |
| 3 | 20201126 |
先祖の取得も簡単で、先程のクエリのancestorとdescendantを逆に書くだけ。ID=8の先祖を取得する。
SELECT c.* FROM closure_table as c INNER JOIN tree_paths as t ON c.id = t.ancestor WHERE t.descendant = 8
result
| id | name |
|---|---|
| 1 | Home |
| 5 | Documents |
| 6 | Adobe |
| 8 | PhotoShop |
新たにノードを挿入する場合、例えばID=4のDesktop以下にノードを挿入するには、まずclosure_tableにフォルダのデータを挿入する。
INSERT INTO `closure_table` ( name ) VALUES ( "Macintosh HD" )
次にID=4を子孫として参照する行の集合を取得し、子孫をLAST_INSERT_ID()で置き換えて新たに挿入する。
INSERT INTO `tree_paths` ( ancestor, descendant ) SELECT t.ancestor, LAST_INSERT_ID() as descendant FROM tree_paths as t WHERE t.descendant = 4 UNION ALL SELECT LAST_INSERT_ID(), LAST_INSERT_ID()
INSERT句で利用しているSELECT句の結果は下記になる。
| ancestor | descendant |
|---|---|
| 1 | 9 |
| 4 | 9 |
| 9 | 9 |
- 先祖が1(Home)で、子孫が9(Macintosh HD)
- 先祖が4(Desktop)で、子孫が9(Macintosh HD)
- 先祖が9(Macintosh HD)で、子孫が9(Macintosh HD)
となっていることがわかる。
ノードを削除する場合、例えば先程挿入した`ID=9'を削除する場合には、子孫として9を参照するすべての行を削除する。具体的に削除するレコードは上記の結果である。
DELETE FROM `tree_paths` WHERE descendant = LAST_INSERT_ID();
サブツリー全体、例えばID=6とその子孫をすべて削除するには、子孫としてID=6を参照する全ての行と、ID=6の子孫を子孫として参照するすべての行を削除する。
具体的には下記のSQLになる
DELETE FROM `tree_paths` WHERE descendant IN ( SELECT x.id FROM ( SELECT descendant as id FROM `tree_paths` WHERE ancestor = 6 ) as x )
DELETE文の条件に利用しているSELECT文の結果は下記
SELECT x.id FROM ( SELECT descendant as id FROM `tree_paths` WHERE ancestor = 6 )
結果
| id |
|---|
| 6 |
| 7 |
| 8 |
IDが6, 7, 8と、ID=6以下のツリーが削除されることがわかる。
結局どの設計を使うべきなのか
| 設計 | 子へのクエリ実行 | ツリーへのクエリ実行 | 挿入 | 削除 | 参照整合性維持 |
|---|---|---|---|---|---|
| 隣接リスト | 簡単 | 難しい | 難しい | 簡単 | 可能 |
| 経路列挙モデル | 簡単 | 簡単 | 簡単 | 簡単 | 不可 |
| 入れ子集合モデル | 難しい | 難しい | 難しい | 難しい | 不可 |
| 閉包テーブルモデル | 簡単 | 簡単 | 簡単 | 簡単 | 可能 |
個人的な意見
仕様にもよりますが、消去法で閉包テーブル設計かなとなります。
隣接リスト
単純に筋が悪い設計だと考えている。要件がかなり簡易な場合(一つの親しか持たない)にはありかもしれないが、そのような仕様は時間が経つに連れて変わっていくし、すぐに新しく親を設計したいと出てくる。それを考えると別の手法を設計すべきと考える。
経路列挙
筋は良いが、経路列挙のpathの管理が辛そうと考えている。アプリケーション側でpathのValidationなどが長大になりそうなのを考えるとメンテンス性に欠ける。メンテンス性に欠けるということはバグの温床になりやすい。そう考えると閉包テーブルモデルの方が良いと考える。
また、pathに複数の値(フォルダの経路)を格納する事はSQLアンチパターンのジェイウォークに当てはまる。
www.slideshare.net
入れ子集合モデル
ノードの挿入や移動などの際にツリー構造の操作が他のモデルに比べて複雑になるのが欠点。挿入や更新が多い要件には向いていない。レコード数が大量にあると更新に時間がかかる。個人的にはクエリが複雑になる事が一番の欠点。コードもクエリも設計もシンプルに収めたほうが良いと考える人間なので、筋は良いが、それなら閉包テーブルモデルを採用したほうが良いと判断。
閉包テーブル
ツリー構造を格納するテーブルのレコードが爆発的に増えるが、その分取得、挿入、更新、削除もシンプルに出来る。トレードオフだが、複雑性が増して不具合の温床になるよりはレコード数が増えるデメリットを受け入れたほうが長期的に見て良いと思っている。
Docker with Multi Stage Buildを利用したNext.jsのDockerイメージを作る
tl;dr
- Next.jsのSSG(Static Site Generator)を利用したDocker Imageを作る際にMulti Stage BuildでImageを作ると、利用しない場合に比べて453MB変わる
Dockerfile
以前とは違い、Production環境でも利用出来るように構築をした。SSGを利用する前提で書いている。 ただ、実際にはSSGをProductionで利用する場合はNetlifyやVercel、S3など静的コンテンツを置くのに適したプラットフォームを利用すると思うので、あまり意味はないかもしれない。
それぞれbase, builder, productionとステージを分けている。
base
依存関係にあるnpm packageをインストールする。開発環境ではbase stageを利用し、docker build時の引数でnext コマンドを指定するだけで良い。
$ docker build -t next-app:base --target base . Sending build context to Docker daemon 177.8MB Step 1/7 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/7 : WORKDIR /app/js/base ---> Using cache ---> 3bcd585979e7 Step 3/7 : COPY package.json . ---> Using cache ---> 73e62066ac85 Step 4/7 : COPY yarn.lock . ---> Using cache ---> d6ed6b01b3ed Step 5/7 : COPY tsconfig.json . ---> Using cache ---> 7a8ae367da60 Step 6/7 : RUN yarn install ---> Using cache ---> dd874aced5fe Step 7/7 : COPY . . ---> Using cache ---> 5ad130f7577a Successfully built 5ad130f7577a Successfully tagged next-app:base $ docker run -it --rm -p 3000:3000 next-app:base yarn next yarn run v1.22.5 $ /app/js/base/node_modules/.bin/next ready - started server on http://localhost:3000 info - ready on http://localhost:3000 event - build page: / wait - compiling... A codemod is available to fix the most common cases: https://nextjs.link/codemod-ndc info - ready on http://localhost:3000
builder
アプリケーションをbuildする層。この層でアプリケーションbuildしproduction stageへは不要なもの(node_modules, ソースコード etc)を持ち込まないようにする。
production
Production環境に適応したイメージを作る層。ここで不要なファイルが作成されるとその分Imageサイズが大きくなるので不要なものは入れてはいけない。 このstageでbuildをしてしまうとソースコードなど不要なものが必要になるので、一つ前のbuilder stageから必要なものをコピーする。
最後のyarn install --productionでproductionに必要な依存関係のみをインストールする。
結果
Multi Stage Buildを利用した場合と利用しない場合で比較をしてみる。
Multi Stage Buildを利用する
$ docker build -t next-app:base . --target base Sending build context to Docker daemon 194.2MB Step 1/7 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/7 : WORKDIR /app/js/base ---> Using cache ---> 7a04d844b125 Step 3/7 : COPY package.json . ---> Using cache ---> ab5127932992 Step 4/7 : COPY yarn.lock . ---> Using cache ---> e8ba5b6cb1ab Step 5/7 : COPY tsconfig.json . ---> Using cache ---> 10b359198a18 Step 6/7 : RUN yarn install ---> Using cache ---> affbace5d646 Step 7/7 : COPY . . ---> Using cache ---> ce9e93809dc2 Successfully built ce9e93809dc2 Successfully tagged next-app:base
builder stage
$ docker build -t next-app:builder . --target builder Sending build context to Docker daemon 194.2MB Step 1/12 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/12 : WORKDIR /app/js/base ---> Running in ad2141be97fb Removing intermediate container ad2141be97fb ---> 6d182a28bf59 Step 3/12 : COPY package.json . ---> 4a44d0936af9 Step 4/12 : COPY yarn.lock . ---> 489f697cba57 Step 5/12 : COPY tsconfig.json . ---> b155046ac03c Step 6/12 : RUN yarn install ---> Running in 937d5fb38b0e yarn install v1.22.5 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.13: The platform "linux" is incompatible with this module. info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation. info fsevents@2.1.3: The platform "linux" is incompatible with this module. info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... warning " > file-loader@6.0.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning " > url-loader@4.1.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". [4/4] Building fresh packages... Done in 49.13s. Removing intermediate container 937d5fb38b0e ---> 27a2d7a30d34 Step 7/12 : COPY . . ---> c19dc009a6a6 Step 8/12 : FROM node:14-alpine as builder ---> fa2fa5d4e6f4 Step 9/12 : ENV NODE_ENV=production ---> Running in 3bd3364115b5 Removing intermediate container 3bd3364115b5 ---> 0cfcd40518b3 Step 10/12 : WORKDIR /app/js/builder ---> Running in 0e20dd40600f Removing intermediate container 0e20dd40600f ---> 9dac22ae0e11 Step 11/12 : COPY --from=base /app/js/base /app/js/builder ---> 208200318cf4 Step 12/12 : RUN ["yarn", "build"] ---> Running in ef55ba375411 yarn run v1.22.5 $ next build && next export Creating an optimized production build... Compiled successfully. Automatically optimizing pages... Page Size First Load JS ┌ ● / 9.31 kB 90.9 kB ├ /_app 1.9 kB 60 kB ├ ○ /404 3.25 kB 63.2 kB └ ● /articles/[id] 344 B 81.9 kB ├ /articles/1 └ /articles/2 + First Load JS shared by all 60 kB ├ static/pages/_app.js 1.9 kB ├ chunks/444ecc389d1b2833cd1c90b9f6d2e962a7bb15f6.c2e7b8.js 10.7 kB ├ chunks/62e1b0f2.e05b9c.js 64 B ├ chunks/framework.4dd100.js 40.3 kB ├ runtime/main.9f98e5.js 6.28 kB ├ runtime/webpack.c21266.js 746 B ├ css/7ed58071346528c353bf.css 97.4 kB └ css/cfb837a2694c3ad390b0.css 108 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps) > using build directory: /app/js/builder/.next copying "static build" directory > No "exportPathMap" found in "next.config.js". Generating map from "./pages" launching 1 workers Exporting (0/2) copying "public" directory Exporting (1/2) Exporting (2/2) Export successful Done in 42.64s. Removing intermediate container ef55ba375411 ---> ef487ea233e3 Successfully built ef487ea233e3 Successfully tagged next-app:builder
production stage
$ docker build -t next-app:production . Sending build context to Docker daemon 194.2MB Step 1/18 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/18 : WORKDIR /app/js/base ---> Running in 141707b10d1b Removing intermediate container 141707b10d1b ---> 7a04d844b125 Step 3/18 : COPY package.json . ---> ab5127932992 Step 4/18 : COPY yarn.lock . ---> e8ba5b6cb1ab Step 5/18 : COPY tsconfig.json . ---> 10b359198a18 Step 6/18 : RUN yarn install ---> Running in 25c5e101426f yarn install v1.22.5 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.13: The platform "linux" is incompatible with this module. info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation. info fsevents@2.1.3: The platform "linux" is incompatible with this module. info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... warning " > file-loader@6.0.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning " > url-loader@4.1.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". [4/4] Building fresh packages... Done in 78.52s. Removing intermediate container 25c5e101426f ---> affbace5d646 Step 7/18 : COPY . . ---> ce9e93809dc2 Step 8/18 : FROM node:14-alpine as builder ---> fa2fa5d4e6f4 Step 9/18 : ENV NODE_ENV=production ---> Running in 05b08f765682 Removing intermediate container 05b08f765682 ---> 43348a423c2a Step 10/18 : WORKDIR /app/js/builder ---> Running in e924b7124b7e Removing intermediate container e924b7124b7e ---> d872eca49f96 Step 11/18 : COPY --from=base /app/js/base /app/js/builder ---> 5aa288be27aa Step 12/18 : RUN ["yarn", "build"] ---> Running in c1762e58a430 yarn run v1.22.5 $ next build && next export Creating an optimized production build... Compiled successfully. Automatically optimizing pages... Page Size First Load JS ┌ ● / 9.31 kB 90.9 kB ├ /_app 1.9 kB 60 kB ├ ○ /404 3.25 kB 63.2 kB └ ● /articles/[id] 344 B 81.9 kB ├ /articles/1 └ /articles/2 + First Load JS shared by all 60 kB ├ static/pages/_app.js 1.9 kB ├ chunks/62e1b0f2.e05b9c.js 64 B ├ chunks/9a9e7c821a17e6790c99026b07c1c46382c721c2.c2e7b8.js 10.7 kB ├ chunks/framework.4dd100.js 40.3 kB ├ runtime/main.9f98e5.js 6.28 kB ├ runtime/webpack.c21266.js 746 B ├ css/7ed58071346528c353bf.css 97.4 kB └ css/cfb837a2694c3ad390b0.css 108 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps) > using build directory: /app/js/builder/.next copying "static build" directory > No "exportPathMap" found in "next.config.js". Generating map from "./pages" launching 1 workers Exporting (0/2) copying "public" directory Exporting (1/2) Exporting (2/2) Export successful Done in 51.92s. Removing intermediate container c1762e58a430 ---> d264d497a0a5 Step 13/18 : FROM nginx:latest as production ---> f35646e83998 Step 14/18 : WORKDIR /app/js/src ---> Running in 89ce96f55a15 Removing intermediate container 89ce96f55a15 ---> fd0fb921aa75 Step 15/18 : RUN rm -rf /etc/nginx/conf.d ---> Running in d73698e7006d Removing intermediate container d73698e7006d ---> 85cc19c895e4 Step 16/18 : ADD conf/nginx/conf.d /etc/nginx/conf.d ---> 27ee1c3cfac8 Step 17/18 : COPY --from=builder /app/js/builder/out ./out ---> 5d4d1bd4881e Step 18/18 : EXPOSE 80 ---> Running in ef6862c2ccd5 Removing intermediate container ef6862c2ccd5 ---> 46fc04190def Successfully built 46fc04190def Successfully tagged next-app:production
Multi Stage Buildを利用しない
$ cat Dockerfile_not_msb FROM alpine:latest RUN apk update RUN apk add --update nodejs nodejs-npm yarn WORKDIR /app/js COPY package.json . COPY yarn.lock . COPY tsconfig.json . RUN yarn install COPY . . RUN ["yarn", "build"] RUN apk add nginx openrc RUN rm -rf /etc/nginx/conf.d ADD conf/nginx/conf.d /etc/nginx/conf.d EXPOSE 80 $ docker build -t next-app:not_msb -f Dockerfile_not_msb . Sending build context to Docker daemon 224MB Step 1/14 : FROM alpine:latest ---> a24bb4013296 Step 2/14 : RUN apk update ---> Using cache ---> 5a65f08f5db7 Step 3/14 : RUN apk add --update nodejs nodejs-npm yarn ---> Using cache ---> c97ad0a3e846 Step 4/14 : WORKDIR /app/js ---> Using cache ---> a37951715a8d Step 5/14 : COPY package.json . ---> Using cache ---> 6faddfc45d20 Step 6/14 : COPY yarn.lock . ---> Using cache ---> bd166cc22fbc Step 7/14 : COPY tsconfig.json . ---> Using cache ---> f827439198ba Step 8/14 : RUN yarn install ---> Using cache ---> 46c450c588da Step 9/14 : COPY . . ---> Using cache ---> 7b18d341a97a Step 10/14 : RUN ["yarn", "build"] ---> Using cache ---> 578c525c3ce1 Step 11/14 : RUN apk add nginx openrc ---> Using cache ---> 94434ea28089 Step 12/14 : RUN rm -rf /etc/nginx/conf.d ---> Using cache ---> 571eedb7cbb5 Step 13/14 : ADD conf/nginx/conf.d /etc/nginx/conf.d ---> Using cache ---> 1a765adec9ca Step 14/14 : EXPOSE 80 ---> Using cache ---> 5df8f8cc2b74 Successfully built 5df8f8cc2b74 Successfully tagged next-app:not_msb
結果
$ docker images | grep next-app next-app not_msb 5df8f8cc2b74 7 minutes ago 614MB next-app builder ef487ea233e3 About an hour ago 327MB next-app production 46fc04190def About an hour ago 161MB next-app base ce9e93809dc2 About an hour ago 599MB
まとめ
- Multi Stage Buildを利用することで一つのDockerfileで開発環境・Production環境をすぐに作ることが出来る。
- Multi Stage Buildを利用した場合、利用しない場合に比べて453MB変わる。
GitLab CIでTrivyを利用しDocker Imageの脆弱性を検知し、Slackに投稿する
tl;dr
Trivyについて
昔書いたのでそちらを参照してください。 teitei-tk.hatenablog.com
コードを見せろ
Sample Repository
Merge Request
工夫したところ
gitlab-ci.ymlは特殊な事は書いておらず、ほとんどGitHubと、GitLabのサンプルコードをコピーしただけです。
工夫した点としては、Trivyの検知した結果のレポートをJUnitのフォーマットにしています。 GitHubのサンプルコードではGitLabのContainer scanningフォーマットで書かれていますが、Container ScanningはGold/Ultimate Planでないと利用できないため、JUnitフォーマットを利用しています。
今回はあえて、サンプルのため脆弱性のあるRack Gemなど、古いgemを利用しています。
JUnitフォーマットを利用した場合、MergeRequestの画面ではこのように表示されます。

CVE-2019-16782などが表示されていることが確認できると思います。 リンクをクリックすると、CVEの詳細が表示されます。

Schedule Pipelineを利用して一定周期でImageのScanを行う
新しくSchedule Pipelineを設定します。特に難しい点は無いと思うのでので設定は省略。
gitlab-ci.ymlの内容は下記です。特に変な点は書いていませんが、only: schedulesでSchedule Pipelineでのみ実行されるようにしています。
image_scan: image: docker:stable services: - name: docker:dind variables: DOCKER_HOST: tcp://docker:2375/ DOCKER_DRIVER: overlay2 # See https://github.com/docker-library/docker/pull/166 DOCKER_TLS_CERTDIR: "" IMAGE: trivy-ci-test:$CI_COMMIT_SHA before_script: - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') - echo $TRIVY_VERSION - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - script: # Build image - docker build -t $IMAGE . # Fail on high and critical vulnerabilities - ./trivy --exit-code 1 --cache-dir .trivycache/ --severity CRITICAL,HIGH,MEDIUM --no-progress $IMAGE cache: paths: - .trivycache/ only: - schedules
設定すると、CI/CDページにてSchduleと表示されていることが確認できると思います。

SlackへScan結果を通知する。
ここも特に難しいことはないです。SlackにてApps -> Incoming WebHooksを追加。WebHook URLをコピーし、GitLabRepositoryのSettings -> CI/CD -> Slack notificationに移動。
Pipelineにチェックを入れ、通知するSlack Channelを記入するだけ。
その後、Schedule Pipelineを実行すると、このようにSlackに通知されます。
