Upgrade to Pro — share decks privately, control downloads, hide ads and more …

創業以来のPHPシステムが生み出した混沌をGoへの移行で乗り越えた話

moriuss
April 28, 2022
750

 創業以来のPHPシステムが生み出した混沌をGoへの移行で乗り越えた話

Go Conference 2022 Springでの登壇資料です。
https://gocon.jp/2022spring/sessions/a2-c/

moriuss

April 28, 2022
Tweet

Transcript

  1. 創業以来のPHPシステムが生み出した 混沌をGoへの移行で乗り越えた話 株式会社ヤプリ 森谷洋祐

  2. • 株式会社ヤプリ 森谷洋祐 • サーバーサイドエンジニア • ヤプリは4年目 • Go歴も4年目 •

    前職はPHPを2年ほど 自己紹介
  3. 1. ヤプリについて 2. アーキテクチャと移行前のつらみ 3. 混沌が蔓延るDBの値をGoで解釈する戦い 4. Goに移行してみて 目次

  4. 1. ヤプリについて

  5. ヤプリについて

  6. ヤプリについて

  7. ヤプリについて

  8. ヤプリについて https://news.yappli.co.jp/n/n91f4b67ffd80

  9. 2. アーキテクチャと移行前のつらみ

  10. ヤプリのアーキテクチャ CMS Native用APIサーバー PHP (2013〜2020) Go + Nuxt (2018〜) PHP

    (2013〜) 今回はここの話 DB アプリ運用者 アプリ コンテンツ編集 コンテンツ取得
  11. • フレームワークを使わない生のPHP • 創業当時のCTOがほぼ1人で書いた、開発速度最優先のコード ◦ テスト無し ◦ 静的解析無し ◦ クラスほぼ無し

    ◦ 関数ほぼ無し • 創業フェーズでは最適 • しかし拡大フェーズでは負債 移行前のつらみ
  12. • メモリ・CPU効率への期待 • 静的解析の恩恵 • 学習コストの低さ • 複雑なコードの生まれにくさ • 社内で小さな導入実績があった

    • メンバーの興味 • 将来の採用力向上 Goの選定理由
  13. 3. 混沌が蔓延るDBの値をGoで解釈する戦い

  14. • 例えばこんなテーブル • chaos_ jsonの中身 CREATE TABLE "contents" ( "id"

    integer, "chaos_json" text, PRIMARY KEY (id) ); DBには大量のJSONカラム(text型) { "name": "hoge" }
  15. • sqlxパッケージのSelectで構造体に当てはめたい • ChaosJsonフィールドはstring??? Go側の構造体定義 type Content struct { ID

    int64 `db:"id"` ChaosJson string `db:"chaos_json"` } func main() { db, _ := sqlx.Open("driver", "dsn") var contents []*Content _ = db.Select(&contents, "SELECT id, chaos_json FROM contents") }
  16. • できればmapで定義したい • もっと言えば構造体で定義したい Go側の構造体定義 type Content struct { ID

    int64 `db:"id"` ChaosJson map[string]string `db:"chaos_json"` } type Content struct { ID int64 `db:"id"` ChaosJson *JsonStruct `db:"chaos_json"` } type JsonStruct struct { Name string }
  17. • できればmapで定義したい • もっと言えば構造体で定義したい Go側の構造体定義 type Content struct { ID

    int64 `db:"id"` ChaosJson map[string]string `db:"chaos_json"` } type Content struct { ID int64 `db:"id"` ChaosJson *JsonStruct `db:"chaos_json"` } type JsonStruct struct { Name string } “unsupported Scan” エラー “unsupported Scan” エラー
  18. func (db *DB) Select(dest interface{}, query string, args ...interface{}) error

    { return Select(db, dest, query, args...) } func Select(q Queryer, dest interface{}, query string, args ...interface{}) error { rows, err := q.Queryx(query, args...) // ... return scanAll(rows, dest, false) } func scanAll(rows rowsi, dest interface{}, structOnly bool) error { // destの種別判定や、reflectでゴニョゴニョ頑張っている処理 // ... for rows.Next() { // destが構造体のsliceの場合、valuesは構造体の各フィールド([]interface{}型) err = rows.Scan(values...) // ... } } sqlxパッケージが構造体に値を入れている仕組みを見てみる github.com/jmoiron/sqlx
  19. sqlパッケージのRows.Scanを見てみる func (rs *Rows) Scan(dest ...any) error { // Mutexのロック処理など

    // ... // 現在行のカラムでループ for i, sv := range rs.lastcols { err := convertAssignRows(dest[i], sv, rs) // ... } return nil } database/sql
  20. sqlパッケージのRows.Scanを見てみる func convertAssignRows(dest, src any, rows *Rows) error { //

    Common cases, without reflect. switch s := src.(type) { case string: switch d := dest.(type) { case *string: if d == nil { return errNilPtr } *d = s return nil case *[]byte: if d == nil { return errNilPtr } *d = []byte(s) return nil case *RawBytes: if d == nil { return errNilPtr } *d = append((*d)[:0], s...) return nil } case []byte: switch d := dest.(type) { case *string: if d == nil { return errNilPtr } *d = string(s) return nil case *any: if d == nil { return errNilPtr } *d = cloneBytes(s) return nil case *[]byte: if d == nil { return errNilPtr } *d = cloneBytes(s) return nil case time.Time: switch d := dest.(type) { case *time.Time: *d = s return nil case *string: *d = s.Format(time.RFC3339Nano) return nil case *[]byte: if d == nil { return errNilPtr } *d = []byte(s.Format(time.RFC3339Nano)) return nil case *RawBytes: if d == nil { return errNilPtr } *d = s.AppendFormat((*d)[:0], time.RFC3339Nano) return nil } database/sql
  21. sqlパッケージのRows.Scanを見てみる func convertAssignRows(dest, src any, rows *Rows) error { //

    ... if scanner, ok := dest.(Scanner); ok { return scanner.Scan(src) } // ... } type Scanner interface { Scan(src any) error } database/sql
  22. Scanメソッドを実装 type Content struct { ID int64 `db:"id"` ChaosJson *JsonStruct

    `db:"chaos_json"` } type JsonStruct struct { Name string `json:"name"` } func (j *JsonStruct) Scan(src any) error { switch src := src.(type) { case string: return json.Unmarshal([]byte(src), j) default: return errors.New("unexpected type") } }
  23. 保存はdriver.Valuerで type Value any type Valuer interface { Value() (Value,

    error) } type Content struct { ID int64 `db:"id"` ChaosJson *JsonStruct `db:"chaos_json"` } type JsonStruct struct { Name string `json:"name"` } func (o *JsonStruct) Value() (driver.Value, error) { return json.Marshal(o) } database/sql/driver
  24. ここからが本当の戦いの始まり

  25. PHP側でJSONカラムを保存するコード • (再掲)移行前のコードはクラスほぼ無し • 全ては連想配列 • 組み込みのjson_encode関数により連想配列をjson文字列に変換 $chaos_json = ['name'

    => 'hoge']; $chaos_json_str = json_encode($chaos_json); // {"name": "hoge"} $pdo = new PDO('dsn'); $stmt = $pdo->prepare('INSERT INTO contents (`id`, `chaos_json`) VALUES (?, ?)'); $stmt->execute([$id, $chaos_json_str]);
  26. PHP側でJSONカラムを保存するコード $chaos_json = []; $chaos_json_str = json_encode($chaos_json); // [] $pdo

    = new PDO('dsn'); $stmt = $pdo->prepare('INSERT INTO contents (`id`, `chaos_json`) VALUES (?, ?)'); $stmt->execute([$id, $chaos_json_str]); • 空の配列が与えられてしまうと文字列の [] になる!
  27. Scanで腐敗防止 func (j *JsonStruct) Scan(src any) error { switch src

    := src.(type) { case string: // 混沌を感じ始めるがScanを拡張すれば根元で腐敗防止できる if src == "[]" { return nil } return json.Unmarshal([]byte(src), j) } }
  28. • JSON内にnumberまたはstringを取り うるフィールドがいる • boolで扱いたいフィールドがstringで 保存されている • 微妙にRFC3339じゃない時刻フォー マットが紛れている •

    etc… • とはいえ全てScanやUnmarshalJSON などに閉じ込められた🎉 その他にも「創業以来の秘伝のタレ」が故の苦しみが type NumberOrString string func (s *NumberOrString) UnmarshalJSON(src []byte) error { if err := json.Unmarshal(src, s); err != nil { // intで読み取ってstring変換 // ... } return nil } type ChaosTime time.Time func (c *ChaosTime) Scan(src interface{}) error { // ... t, err = time.Parse(time.RFC3339, srcString) if err != nil { t, err = time.Parse( "2006-01-02T15:04:05Z0700", srcString, ) } // ... }
  29. 4. Goに移行してみて

  30. • 型がある素晴らしさ • エラーの握り潰しが激減 • 環境構築が楽 • 標準パッケージを読むのが楽 技術面で良かったこと

  31. • 採用力UP! ◦ この半年でサーバーサイドが10人→15人 • ポテンシャルの高い若手層をGopherとして育てる体制が回り始める ◦ 例: 社内勉強会「GoStudy」で標準パッケージのコードリーディング ◦

    tenntenn Conference 2022でも事例として紹介されました! 採用面で良かったこと https://tenn.in/codereading
  32. • 持ち回りでファシリテーターが目的と対象 範囲を事前に決める • 当日Zoomで集まる • Slackにわいわいスレを立てる • 前半30分、各々がもくもくリーディングを しつつSlackでわいわいする

    • 後半30分、スレの内容などなどを元にZoom で盛り上がる 「GoStudy」でのコードリーディング会
  33. これからのYappliを一緒に作りませんか? カジュアル面談はこちら👉 Tech Blog👉 ©tottie / Renée French