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

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

moriuss
April 28, 2022
1.5k

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

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

moriuss

April 28, 2022
Tweet

Transcript

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

    (2013〜) 今回はここの話 DB アプリ運用者 アプリ コンテンツ編集 コンテンツ取得
  2. • 例えばこんなテーブル • chaos_ jsonの中身 CREATE TABLE "contents" ( "id"

    integer, "chaos_json" text, PRIMARY KEY (id) ); DBには大量のJSONカラム(text型) { "name": "hoge" }
  3. • 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") }
  4. • できれば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 }
  5. • できれば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” エラー
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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") } }
  11. 保存は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
  12. 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]);
  13. 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]); • 空の配列が与えられてしまうと文字列の [] になる!
  14. 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) } }
  15. • 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, ) } // ... }