entを試してみた
仕事で使いそうだったのでORマッパーのentを試してみた。 公式はこちら。
検証用のディレクトリ作成
mkdir test cd test go mod init test
検証用のDB(postgres)を作成
検証用のDBを作成する
touch docker-compose.yml
以下のようにDBを起動するだけの定義を作成する。
docker-compose.yml
version: "3.8" services: db: image: postgres:15.1-alpine ports: - 5432:5432 volumes: - ./.data/postgres:/var/lib/postgresql/data environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: testdb
DBを起動する。
docker-compose up -d
ライブラリのインストール
entのインストール
go get -d [entgo.io/ent/cmd/ent](http://entgo.io/ent/cmd/ent)
postgresのドライバのインストール
go get github.com/lib/pq
スキーマを追加
以下のコマンドでUserスキーマの元になるファイルが生成することができる。
今回はuserテーブルを生成するので、Userを指定。
スキーマファイルはent配下に作成される。
ent new User
ent/schema/user.goなどが作成されている。
生成されたファイルを修正してスキーマを定義
ent/schema.user.goを以下のように修正し、userテーブルにnameとageの項目を追加する。
// Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name").Default("unknown"), field.Int("age").Positive(), } }
定義したスキーマから各テーブル毎の実装を生成する
go generate ./ent
上記のコマンドでgenerate.goやuser_create.goなどの実装ファイルが生成される。
マイグレーション用のプログラムを実装
entはスキーマをマイグレーションする機能があるので、マイグレーション用の処理を記述
DB接続を取得する実装
db/db.go
package db import ( "fmt" "test/ent" ) func NewDBClient() *ent.Client { user := "user" password := "pass" port := "5432" host := "localhost" dbName := "testdb" url := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, dbName) client, err := ent.Open("postgres", url) if err != nil { fmt.Printf("failed connecting to postgres: %v", err) } return client } func CloseDB(client *ent.Client) { err := client.Close() if err != nil { fmt.Printf("failed close to db: %v", err) } }
マイグレーション処理の実装
go runコマンドで実行する予定なので、packageをmainにしています。
migrate/migrate.go
package main import ( "context" "fmt" "test/db" "test/ent/migrate" _ "github.com/lib/pq" ) func main() { client := db.NewDBClient() ctx := context.Background() err := client.Schema.Create( ctx, migrate.WithDropIndex(true), migrate.WithDropColumn(true), ) if err != nil { fmt.Printf("failed creating schema resources: %v", err) } db.CloseDB(client) }
スキーママイグレーション実行
以下のようにしてマイグレーションプログラムを実行
go run migrate/migrate.go
実際にマイグレーションされたか確認する
マイグレーションを下のように確認
docker-compose exec db /bin/sh # psql -h localhost -U user -d testdb testdb=# \d users; Table "public.users" Column | Type | Collation | Nullable | Default --------+-------------------+-----------+----------+---------------------------------- id | bigint | | not null | generated by default as identity name | character varying | | not null | 'unknown'::character varying age | bigint | | not null | Indexes: "users_pkey" PRIMARY KEY, btree (id)
CRUD操作
下記のようなプログラムで基本的なCRUDを確認。
main.go
package main import ( "context" "fmt" "log" "test/db" "test/ent/user" _ "github.com/lib/pq" ) func main() { client := db.NewDBClient() ctx := context.Background() // 1件追加 usr, err := client.Debug().User. Create(). SetName("user1"). SetAge(33). Save(ctx) if err != nil { fmt.Printf("failed creating user: %v", err) return } // 1件更新 updatedUser, err := client.Debug().User.Update().Where(user.ID(usr.ID)).SetAge(29).Save(ctx) if err != nil { fmt.Printf("failed updating user: %v", err) return } log.Printf("user: %+v", updatedUser) // 名前がuser1のユーザーを取得 users, err := client.Debug().User.Query().Where(user.Name("user1")).All(ctx) if err != nil { fmt.Printf("failed getting users: %v", err) return } for _, usr := range users { fmt.Printf("user: %+v", usr) } // 1件削除 _, err = client.Debug().User.Delete().Where(user.Name("user1")).Exec(ctx) if err != nil { fmt.Printf("failed deleting user: %v", err) return } // DB接続を閉じる db.CloseDB(client) }
上記を実行するとログとして各処理で発行されるクエリを確認して意図したクエリが発行されたことが確認できる。 最後にデータを削除しているのでDB上にはデータは残らないことに注意。
>> go run main.go 2023/04/28 22:32:37 driver.Query: query=INSERT INTO "users" ("name", "age") VALUES ($1, $2) RETURNING "id" args=[user1 33] 2023/04/28 22:32:37 driver.Exec: query=UPDATE "users" SET "age" = $1 WHERE "users"."id" = $2 args=[29 1] 2023/04/28 22:32:37 user: 1 2023/04/28 22:32:37 driver.Query: query=SELECT "users"."id", "users"."name", "users"."age" FROM "users" WHERE "users"."name" = $1 args=[user1] user: User(id=1, name=user1, age=29)2023/04/28 22:32:37 driver.Exec: query=DELETE FROM "users" WHERE "users"."name" = $1 args=[user1]
次に
次の記事でテーブル同士を関連させて動作確認してみた。 https://miyazi888.hatenablog.com/entry/2023/05/07/221705
参考
samber/loを試した
すでにgo1.19がリリースされてしまっていますが、1.18がリリースされた際にGoにGenericsが導入されて大きな話題になりました。
で、Genericsが導入されたら、絶対にどこかの誰かがfilterやmap関数みたいなののライブラリを作ると思っていたら、やっぱりありました。 その名もlo。
lodashを意識して開発された上に名前も短くしたくて、loになった模様。 lodashだと_.filterみたいな感じにしたかったんでしょうね。
install
go get github.com/samber/lo@v1
使ってみた
正直、公式のREADMEを読めばすぐに使い方はわかると思います。 それでも使い心地を試してみました。
// 奇数のみを抽出 results := lo.Filter([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, func(x int, _ int) bool { return x%2 != 0 }) fmt.Println(results) // [1, 3, 5, 7, 9]
今までforでがんばるしかなかったことを考えると、すっきり記述できてうれしい。
次は普通のオブジェクトでも絞り込んでみました。
type Item struct { Name string Type string } func main() { // オブジェクトの抽出 items := []Item{{Name: "item1", Type: "foo"}, {Name: "item2", Type: "bar"}, {Name: "item3", Type: "foo"}} filteredItem := lo.Filter(items, func(e Item, _ int) bool { // 第二引数の関数の二番目の引数はindexです return e.Type == "foo" }) fmt.Println(filteredItem) // [{item1 foo} {item3 foo}] }
ちょっとJavaScriptっぽい感じで絞り込みが記述できました。
ちなみに今回は使いませんでしたが、Filter関数に渡す関数の第2引数はindexでした。
感想
上にも記述しましたが、これまでforでがんばるしかなかったことを思うと、だいぶ楽ですっきり記述できて良いな、というのが本音です。
あと、Genericsのサンプル集的に眺めることもできます。 ソースもだいぶシンプルで楽に読めると思います。 Genericsの使い方を忘れた時なんかにこのライブラリを思い出すと参考になりそうに思いました。
使う機会があれば、積極的に使いたいライブラリだと思いました。
ところでissueでこのライブラリの名前をgodashに変更したら? 的なissueが上がっててちょっと面白かったです。 github.com
その他
同じ作者の方がやっぱりGoのGenericsに対応したDIライブラリを公開していて、それもちょっと気になりました。 これも時間を見つけて試してみようと思います。
minikubeでingressを使う
k8sを学習していく中でingressという単語に遭遇し、どうやらLoad Balancer的なものであるらしい、ということまでは理解した。
ただ、実際に触ってみた方が理解しやすいかと思い、minikubeでingressを動作させてみた。
前提
以下の環境で動作確認した。
- ubuntu 20.04
- minikube v1.24.0
- kubectl v1.20.8-dispatcher
それからdockerとdocker-composeが動作する環境であること。
動作確認用のdockerファイル作成
まずは動作確認用に2つのdocker imageを作成する。
mkdir html1 touch html1/index.html
html1/index.html
<html> <h1>sample page1</h1> </html>
mkdir html2 touch html2/index.html
<html> <h1>sample page2</h1> </html>
touch Dockerfile1
Dockerfile1
FROM 'nginx:latest' COPY html1/index.html /usr/share/nginx/html/index.html RUN service nginx start
touch Dockerfile2
Dockerfile2
FROM 'nginx:latest' COPY html2/index.html /usr/share/nginx/html/index.html RUN service nginx start
touch docker-compose.yaml
docker-compose.yaml
version: '3' services: nginx1: build: context: ./ dockerfile: Dockerfile1 image: nginx-sample1 ports: - 8080:80 nginx2: build: context: ./ dockerfile: Dockerfile2 image: nginx-sample2 ports: - 8081:80
Docker image作成と動作確認
ここまで出来たら、とりあえず上記の設定でdockerコンテナが狙ったとおりに動作するか確認。
docker-compose up -d curl localhost:8080 以下のようなレスポンスが返ってきたら成功 <html> <h1>sample page1</h1> </html> curl localhost:8081 <html> <h1>sample page2</h1> </html>
正しく動作することがわかったらコンテナを停止。
docker-compose stop
ここまでで正しく動作するdocker imageが作成されていることになる。
docker imageにタグを付ける
docker imageは出来たものの、このままではminikubeで扱えない(latestタグのimageがうまく扱えない模様)のでタグを付ける。
まずは作成されたdocker imageのIDを確認。
docker images
IDがわかったらタグ付けする。
docker tag <nginx-sample1のimage id> nginx-sample1:v1 docker tag <nginx-sample2のimage id> nginx-sample2:v1
minkube起動
minikubeを起動。
minikube start
イメージ取り込み
先程、作成したdocker imageをminikubeにロードする。
最後のminikube image lsはminikubeのimageの一覧を表示するコマンド。
minikube image load nginx-sample1:v1 minikube image load nginx-sample2:v1 minikube image ls
取り込んだイメージが使えるか確認
minikubeに取り込んだimageが本当に動作するか、ここでも動作確認する。
kubectl create deploy test1 --image=nginx-sample1:v1 kubectl expose deploy test1 --type=NodePort --port=8080 --target-port=80 minikube service test1 --url
ターミナルにURLが表示されるので、ブラウザで確認。
sample page1が表示されたら成功。
もう1つのイメージも動作確認する。
kubectl create deploy test2 --image=nginx-sample2:v1 kubectl expose deploy test2 --type=NodePort --port=8080 --target-port=80 minikue service test2 --url
こちらも表示されたURLにアクセスし、sample page2が表示されたら、OK。
ingressがminikubeで有効かどうかを確認
ingressはminikubeのaddonとして提供されている模様。
まずはaddonのリストを調べる。
minikube addons list
もしingressがenabledになっていなかったら有効にする。
minikube addons enable ingress minikube addons list
ingressを設定する
やっと本番。ingressを追加する。
touch ingress.yaml
下の設定ではhttp://test.local/にアクセスするとsample page1のpodにアクセスし、http://test.local/test2にアクセスするとsample page2のpodにアクセスする設定。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-test annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 spec: rules: - host: test.local http: paths: - path: / pathType: Prefix backend: service: name: test1 port: number: 8080 - path: /test2 pathType: Prefix backend: service: name: test2 port: number: 8080
下記のコマンドでingressを適用する。
kubectl apply -f ingress.yaml watch kubectl get ingress
watchで表示されるingressでaddressの部分にloocalhostが表示されるまで待つ。
上で作成したingressはhostを指定しているので/etc/hostsも以下ように追記する。
/etc/hosts
127.0.0.1 test.local
これで準備完了。
動作確認
別のターミナルを起動して以下のコマンドを実行する。
minikube tunnel
元のターミナルに戻ってからcurlコマンドでレスポンスを確認する。
ブラウザでももちろん、OK。
curl http://test.local
sample page1が返ってきたら動作OK。
<html> <h1>sample page1</h1> </html>
curl http://test.local/test2
こちらはsample page2が返ってきたらOK。
<html> <h1>sample page2</h1> </html>
というわけでminikubeでingressを動作させることが出来た。
後片付け
下記のコマンドでingressとserviceとdeploymentを削除。
kubectl delete ingress ingress-test kubectl delete svc test1 test2 kubectl delete deploy test1 test2
次に/etc/hostsから下記の設定を削除。
/etc/hosts
127.0.0.1 test.local
minikubeのimageから今回追加したimageを削除。
minikube image rm nginx-sample1:v1 minikube image rm nginx-sample2:v1
最後にminikubeを停止して、後片付け完了。
minikube stop
minikubeでローカルのdockerイメージを使う
きっかけ
kubernetesの学習目的でminikubeをインストール。 学習中、minikubeでもローカルのdocker imageを使用したくなった。
ネットで検索するとだいたい、下記のような方法が記載されていた。
eval $(minikube docker-env)
ところが、自分の環境では、これではローカルのdocker imageを使うことが出来なかった。
minikubeのruntimeにcontainerdを使っていることが原因ではないかと推測。 なぜなら初回のminikube start時にruntimeにcontainerdを強制されたから。
そこで他に方法はないかとネットを検索するとminikubeの公式にimageなるサブコマンドがあることに気づいた。 これを使えばどうにかローカルのイメージを使えるのでは・・・ と思い実験してみた。 https://minikube.sigs.k8s.io/docs/commands/image/
結論
minikube image loadを使うことでminikube上でもホスト側のイメージを取り込んで使用することが出来た。
minikube image load <image-name>:<image-tag>
動作確認
以下、本当にローカルのイメージをminikubeで使うことができるのか実験してみた。
前提
動作確認した環境は以下のとおり。 上にも記述したようにminikubeのruntimeはcontainerd。
- ubuntu 20.04
- minikube v1.24.0
- kubectl v1.20.8-dispatcher
動作確認用のdockerイメージ作成
htmlファイル作成
まずは動作確認用に下記のようなHTMLを表示するイメージを作成。
mkdir html touch html/index.html
html/index.html
<html> <h1>sample page</h1> </html>
dockerイメージ作成
次に以下のようにDockerFileとdocker-compose.ymlを作成。
touch Dockerfile
Dockerfile
FROM 'nginx:latest'
COPY html/index.html /usr/share/nginx/html/index.html
RUN service nginx start
touch docker-compose.yaml
docker-compose.yaml
version: '3' services: nginx: build: ./ image: nginx-sample ports: - 80:80
dockerイメージの動作確認
ここで一旦、dockerイメージの作成と動作確認を行う。
docker-compose up -d
起動したら、ブラウザでlocalhostにアクセスする。 「sample page」と表示されれば、成功。
イメージにタグ付けする
最後に作成したイメージにタグ付けする。 この段階ですでにイメージそのものは作成されているけど、タグはlatestとなっていると思う。 なぜかminikubeではlatestタグのイメージは扱えなさそうなのでlatest以外のタグを付ける(latestが使えない理由はわからなかった・・・) ここでv1タグを付ける。
docker tag <docker image id> nginx-sample:v1
minikubeに作成したdockerイメージを起動できることを確認
起動
まずはminikubeそのものを起動
minikube start
イメージを取り込む
ここでminikubeにローカルのイメージを取り込み。
minikube image load nginx-sample:v1
minikubeで扱うことができるイメージの一覧は以下のコマンドで確認できた。
minikube image ls
取り込んだイメージを使う
以下のコマンドで取り込んだイメージのdeploymentの作成と公開を行う。
kubectl create deployment test --image=nginx-sample:v1 kubectl expose deployment test --type=NodePort --port=80 minikube service test --url
画面に表示されたURLにブラウザでアクセスし、sample pageが出たらアクセスOK。 自分の環境ではこれで動作した。
後片付け
動作確認が済んだので以下のような後片付けをして完了。
minikube stop kubectl delete svc test kubectl delete deploy test minikube image rm nginx-sample:v1 docker-compose down docker rmi nginx-sample:latest docker rmi nginx-sample:v1
2020年振り返り
立ち位置
フリーでプログラマとしてプロジェクトに参加。今年は3月まではA社。4月からはB社のプロジェクトに参画中。
利用した技術
Go / Gin Frmaework
A社はGoでバックエンドを書いてた。それからGoの仕事が舞い込みそうな話しがきたので個人的にサンプル的なコードを書いてカンみたいなものを維持していたが、結局タイミング的なものもあって仕事には繋がらなかった。残念。
ES6 / Vue.js + vuex
A社のフロントエンドがvue.js + es6だったので書いた。日本でvue.jsが流行ったのはとっかかりやすさが整備されていたからなんだろうな、と改めて感じた。
Ruby / Rails
B社の仕事がRailsがAPIサーバ + ReactのSPAというよくある構成。バグ調査、機能追加の為に触った。自分の主戦場はこちらだけど、今年は仕事の都合上、フロント寄りの実装の方を多く触った。
TypeScript / React + redux B社の仕事のフロントエンドがこれだった。Reactは以前にも書いてはいたけど、ほぼ忘れていたので個人的にキャッチアップをしていった。途中フロントの全面書き直しが始まり、いきなりTypeScriptになった。まだまだ手に馴染んでないけど、型定義が柔軟すぎて面白い反面、型職人がいなくなったら混乱する現場とかあるんじゃなかろうか、とも思った。たぶん、ライブラリ以外では柔軟すぎる型定義は使わない方が良いんじゃないかと思う。今年一番触っているのはこの辺り。フロントはたぶんTypeScriptが標準言語になる雰囲気を感じた。
Python
B社のサービスのある部分が画像解析を行っていて、画像解析部分がPythonで記述されていた。運用的なことをやっていたので書かなかったけど読んだ。
Docker / docker-compose
今年は使っただけ。開発環境の構築ではほぼ必須になったと思う。来年はBuildpackを触ってみることにする。
AWS lambda
B社のサービスでPDFを出力する機能があって元々使われていた。ただPDFの容量がデカくなるとレスポンス上限に引っかかってエラーになっていたので、これを全面的に直した。PDF化にはpuppeterを使った。AWS lambdaでpuppeterを使うには工夫が必要だが、この記事のおかげでなんとかなった。 https://dev.classmethod.jp/articles/run-headless-chrome-puppeteer-on-aws-lambda/
Google Cloud Platform
なぜか今年は関わった2社共にGCPメインだった。GCPは初めてだったが、特殊な使い方をしているわけではなかった為にすぐに慣れた。一時はAWSオンリーだったけど、GCPが盛り返してきているのを感じた。
Kubernetes
B社の基盤。ほんのちょっとyamlを記述した程度で本格的に触ることはなかった。だいぶ世の中に定着した印象。来年は個人的にも学習してみることにする。ここ数年の盛り上がりを受けて良い感じの教材が増えているので学習にはなにも困らないだろう。
GraphQL
個人的に学習していた。ブログにまとめたりしたかったが、うまくまとめられなかった。残念。ServerをGo、Clientをnuxt.js + apolloで実装してみて、なんとなくのイメージは掴めたのは良かった。
gRPC
これも個人的に学習していた。Hello Worldレベルのことしか出来ていないけど、型定義書いて、Goでクライアントとサーバを建ててちょっと動かしてみた。これもイメージは掴めた。
WebRTC
仕事では微妙にさわったが、未だにピンときてない。けども数年したら当たり前に使われるようになる気がした。来年あたりちゃんと触ってみようと思う。
ワークスタイル
A社までは普通に出勤してた。 B社からいきなりフルリモートになった。もちろん、コロナの影響。 以前にもひと月ほどリモートで働いたことはあったが、今回はずっとリモート。現在の継続中。未だに誰ともリアルで対面したことがない。 おかげで生活がガラッと変わった。
通勤がなくなったり、時間の拘束がだいぶなくなったことは本当に良かったけれども、成果を出す為に働き過ぎたり、通勤がなくなったおかげか、体調を崩すことが増えてしまった。
特に11月以降は体調面に加えて、謎の無気力感に襲われて仕事にも微妙に影響が出てしまった。原因は結局のところ運動不足に起因するものだったようで、現状は午後3時になったら30分程度、散歩+縄跳びをする、軽い筋トレ+ストレッチですることで改善した。 健康、本当に大事。
あとはA社でもB社でも最年長だけど、技術的には一番下っ端な気がするので、なんとかがんばるしかないと思った次第。
まとめ
来年もなんとか生き残れるようにやれることをやるしかない。 あと本当に健康大事。
echoでリクエストパラメータをチェックするには
echoでリクエストパラメータを取得し、構造体に割り当てる方法はわかったのですが、ここまで出来ると今度はvalidateの方法が知りたくなり、調べてみました。
echoにはvalidateそのものは用意されていないのですが、インターフェイスだけが用意されていて、このインターフェイスに合わせて実装していくようです。
公式にはサンプルとして、有名なバリデーションライブラリであるところのvalidatorを使ったサンプルがあったので、これを打ち込んで検証してみました。
https://github.com/go-playground/validator
プログラム
Validateというメソッドを持った関数をechoに登録し、必要なところでValidate(構造体ポインタ)を渡す感じで呼び出すのが基本のようです。
今回であれば、CustomValidatorという構造体を登録しています。
validatorは構造体のアノテーションにvalidateしたい内容を記述していくスタイルです。今回は必須項目チェックを行うので、requiredをアノテーション内で指定しています。
package main import ( "net/http" "github.com/labstack/echo" "gopkg.in/go-playground/validator.v9" ) type User struct { Name string `json:"name" validate:"required"` } type CustomValidator struct { validator *validator.Validate } func (cv *CustomValidator) Validate(i interface{}) error { return cv.validator.Struct(i) } type Error struct { Error string `json:"error"` } func main() { e := echo.New() // validatorを登録 e.Validator = &CustomValidator{validator: validator.New()} e.POST("/", test) e.Logger.Fatal(e.Start(":1111")) } func test(c echo.Context) error { u := new(User) // ここでリクエストパラメータを構造体に if err := c.Bind(u); err != nil { return c.JSON(http.StatusBadRequest, err) } // Validateを実施 if err := c.Validate(u); err != nil { return c.JSON(http.StatusBadRequest, &Error{Error: err.Error()}) } return c.JSON(http.StatusOK, u) }
実行
JSON形式でNameに対して空文字を送信するとエラーとなります。
curl -X POST http://localhost:1111 -H 'Content-Type: application/json' -d '{"Name":""}' {"error":"Key: 'User.Name' Error:Field validation for 'Name' failed on the 'required' tag"} curl -X POST http://localhost:1111 -H 'Content-Type: application/json' -d '{"Name":"test"}' {"name":"test"}
Golangで構造体の配列を持つ構造体を初期化
微妙にわからなかったのでメモ代わりに。
Golangで以下のような構造があった時、初期化する方法がわからなかった。
構造体配列を持つ、構造体の時の初期化方法がわかってなかった。
type Store struct { Items []Item `validate:"dive"` } type Item struct { Name string `validate:"required"` // 名前は必須 Price int `validate:"lte=100"` // 値段は100以下 }
以下のようにすればよかった。
func main() { obj := Store{ Items: []Item{ {Name: "item1", Price: 99}, {Name: "", Price: 100}, {Name: "item3", Price: 101}, }, } }