学習備忘ログ

よく使うコードや設定のメモ

【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

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、locationfriendsが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"
      }
    }
  ]
}

少し高度なドキュメント管理方法