【Golang + Echo + go-playground/validator】アップロードした画像の幅や高さのバリデーションを実装
課題
- アップロードした画像の幅や高さのバリデーションしたい
サンプル実装
画像の幅が1000以下でない場合はエラーを出す。
type SampleRequest struct { File File } type File struct { Width int `json:"width" validate:"lt=1000"` Height int `json:"height" validate:"lt=1000"` } func UploadFile(d *store.Store) echo.HandlerFunc { return func(c echo.Context) error { var r SampleRequest if err := c.Bind(&r); err != nil { return c.String(http.StatusBadRequest, err.Error()) } file, _ := c.FormFile("file") //アップロードされたファイルの取得。 errの部分を_にしてるが本来であれば正しくエラー処理が必要。 src, _ := file.Open() im, _, err := image.DecodeConfig(src) if err != nil { return c.String(http.StatusBadRequest, err.Error()) } r.File.Width = im.Width //構造体のフィールドに代入 r.File.Height = im.Height if err := c.Validate(r); err != nil { //バリデーション body, err := makeCreateGroupValidationErrorResponseBody(r, err) } } }
【Golang + go-playground/validator】DBを含めたバリデーションの実装
課題
- DBを含めたバリデーションを実装したい
サンプル実装
userテーブルのemailに同じemailがあったらバリデートする。
type DBAbstraction struct { db interface{} //ここは実際のコードの型に合わせる } func (a *DBAbstraction) ValidateExistsEmail(fl validator.FieldLevel) bool { existsEmail, _ := a.db.User.ExistsEmail(fl.Field().String()) //ここでsqlを叩きemailが存在するか確認。 return existsEmail } func Setup(ds *store.Store) *echo.Echo { e := echo.New() dba := &DBAbstraction{db: ds} validator := validator.New() validator.RegisterValidation("exists-email", dba.ValidateExistsEmail) //バリデーションの登録 e.Validator = &CustomValidator{validator: validator} return e }
【Golang + go-playground/validator】他のフィールドの値を含めたカスタムバリデーションの実装
課題
- 他のフィールドの値を含めたカスタムバリデーションの実装をしたい
サンプル実装
AフィールドとBフィールドの値を比較してバリデーションする方法。こちらはそもそもカスタムせずに gtecsfieldを使用すれば実現できる。
type SampleStruct struct { A int `form:"a" json:"a" validate:"greater=B"` //Aの値がBより大きいかのバリデートを指定 B int `form:"b" json:"b" validate:"required"` } func (cv *CustomValidator) Validate(i interface{}) error { cv.validator.RegisterValidation("greater", CustomValidate) //greaterという名前のバリデートを登録 return cv.validator.Struct(i) } func CustomValidate(fl validator.FieldLevel) bool { param := fl.Param() // Bが返却される。 greater=X の Xが返却されるので。 paramFieldValue := fl.Parent().FieldByName(param) //フィールドBの値を取得。 return fl.Field().Int() > paramFieldValue.Int() //Aの値とBの値を比較 }
【Golang + Sqlx】リレーション先も含めて構造体へのbindを一度でする方法
前提
groupテーブルとcategoryテーブルがあり、groupテーブルにcategoryを参照する外部キーが存在してる。
構造体定義
package model type Group struct { ID int `json:"id" db:"id"` Name string `json:"name" db:"name"` GroupCategoryId uint64 `json:"groupCategoryId,omitempty"` CompanyName string `json:"companyName,omitempty" db:"company_name"` CompanyId int `json:"companyId,omitempty" db:"company_id"` Category *Category `json:"category" db:"category"` CategoryId int `json:"categoryId,omitempty" db:"category_id"` } type Category struct { Id int `json:"id,omitempty" db:"id"` Name string `json:"name,omitempty" db:"name"` Color string `json:"color,omitempty" db:"color"` }
方法
- 以下のようにリレーション先の構造体を
gc.name as "category.name", gc.color as "category.color"
のようにするとbindできる。
func (g *Group) GetGroups(pageSize int, page int) (groups []*model.Group, totalStore, lastPage int, err error) { logger.Info("GetGroups persistence") stmt, err := g.db.Preparex(` SELECT g.id, g.name, c.name as company_name, gc.name as "category.name", gc.color as "category.color", s.total_store, g.created_at FROM groups AS g //省略 LIMIT ? OFFSET ? `) if err != nil { return nil, 0, 0, err } const userId = 445 rows, err := stmt.Queryx(userId, pageSize, pageSize*(page-1)) if err != nil { return nil, 0, 0, err } defer rows.Close() groups = []*model.Group{} for rows.Next() { var group model.Group if err := rows.StructScan(&group); err != nil { return nil, 0, 0, err } groups = append(groups, &group) } return groups, totalCount, lastPage, nil }
【Golang + http/test】リクエストのbodyにjsonを渡す方法
- サンプルコード bodyJsonにjsonの文字列を代入し、strings.NewReaderで読み込んであげる。
package handler_test import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestCreateGroup(t *testing.T) { mockDS := store.NewMockStore() e := echo.New() bodyJSON := `{"name":"test","companyId":9,"categoryId":2,"storeIds":[1,2]}` req := httptest.NewRequest(http.MethodPost, "/v1/groups", strings.NewReader(bodyJSON)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() c := e.NewContext(req, rec) //~~省略 }
【Golang + Echo】カンマ区切りのクエリパラメータを構造体にBindする方法
やりたいこと
下記のようなUrlで companyIds
を配列として受け取りたい。
companies/pageSize=10&page=1&companyIds=22,23,21
コードサンプル
ポイントは BindWithDelimiter
の部分。ここで,
区切りの数値を CompanyIds
に格納してる。
type getCompanyRequest struct { Name string `query:"name"` CompanyIds []int `query:"companyIds"` PageSize int `query:"pageSize"` Page int `query:"page"` } func GetCompanies(d *store.Store) echo.HandlerFunc { return func(c echo.Context) error { var r getCompanyRequest if err := echo.QueryParamsBinder(c). BindWithDelimiter("companyIds", &r.CompanyIds, ","). BindError(); err != nil { return c.String(http.StatusBadRequest, err.Error()) } //省略 return c.JSON(http.StatusOK, companies) } }
ElasticSearch
概要
- スケーラビリティに優れた全文検索エンジン
- ログ集約のLogstashやfluentd、可視化ツールのkibanaと一緒に使われることが多い
- 複数のデータベースを横断して検索することが、ごく当たり前の用途として提供されている
全文検索
- 大量にあるドキュメントデータ(Jsonファイル)の中から、目的のワードを含むドキュメントデータを検索するための仕組み
用語
- クラスタ、ノード ClusterとはNodeの集合体です。 Nodeとはサーバーと同じ概念のレベルのもので、1台のサーバーに1Node用意します。 トラフィックが増加した場合は、Nodeを増やすことで、処理速度の分散ができるような仕組みになっています。
- インデックス RDBのDatabaseの概念レベルに該当します。 1つのClusterに複数のIndexが作成できます。
- タイプ RDBのTableの概念レベルに該当します。 1つのIndexに複数のTypeが作成できます。
- ドキュメント RDBのRowの概念レベルに該当します。 idをキーにして個々のデータを管理します。
- シャード RAIDで構成されたハードディスクと似た仕組みになります。 1つのindexを作成すると、合わせてShadが複数作成されます。indexを分割したものがshadになるイメージ。 ShardはPrimary(master)とReplicaで構成されており、1Nodeには1shard配置されます。 例えばPrimaryがすでに存在するShardには同じNodeにReplicaは存在できず、別のNodeに配置されます。
使い所
JSONで定義されたファイル内の検索をしたい場合
以下に実際のユースケースが何個か載ってる https://dev.classmethod.jp/articles/elasticsearch-getting-started-05/
実際に動かす
以下のelasticsearchクラスタの起動
以降を参照
https://qiita.com/kiyokiyo_kzsby/items/344fb2e9aead158a5545
docker用語
- soft 一般ユーザ権限で変更可能の最大値
- hard root権限で変更可能の最大値
- ulimits
- memlock メモリ内にロックされるアドレス空間の最大値
- nofile オープンできる最大ファイル数
- nproc 最大プロセス数
- volumes
- driver localの場合、ローカルのファイルシステムと指定のコンテナをマウントする。
templateの設定
- indexの設定を使い回したい時に使う
curl -H "Content-Type: application/json" -XPUT localhost:9200/_template/classmethod -d { index_patterns" : ["classmethod*"], "order": 0, "settings": { "number_of_replicas": 0 }, "mappings": { "employees": { "dynamic_templates": [ { "string_template": { "match": "*", "match_mapping_type": "string", "mapping": { "type": "string", "fields": { "raw": { "type": "string", "index": "not_analyzed" } } } } } ], "properties": { "location": { "type": "geo_point" }, "friends": { "type": "nested" } } } } }
上記の場合、index_pattersにマッチしたindexを作成した場合(今回はclassmethod*
という名前)、employees
がtype、location
、friends
がfieldsの設定がindexに反映される。
alias
- 別名の複数インデックスを同じ名前とすることで、複数インデックスに跨った検索を簡単に行えるようにする。
- アプリケーション側を修正することなく、対処のインデックスを変更することができる。
curl -H "Content-Type: application/json" -XPUT localhost:9200/_aliases -d { "actions": [ { "remove": { "index": "mierun-jimujigyo-master-v0.1", "alias": "mierun-jimujigyo-master" } }, { "remove": { "index": "mierun-jimujigyo-years-v0.1", "alias": "mierun-jimujigyo-years" } }, { "add": { "index": "mierun-jimujigyo-master-v0.1", "alias": "mierun-jimujigyo-master" } }, { "add": { "index": "mierun-jimujigyo-years-v0.1", "alias": "mierun-jimujigyo-years" } } ] }
少し高度なドキュメント管理方法
- 存在しない場合のみ新しいドキュメントを作成する
- ドキュメントの一部を更新する
- スクリプトを使ってドキュメントの一部を更新する
- 楽観的並行性制御(optimistic concurrency control)
- 有効期限つきドキュメントをインデックスする(TTL) https://dev.classmethod.jp/articles/elasticsearch-getting-started-03/#advanced-04