いがにんのぼやき

WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

データ指向アプリケーションデザインがいい本

www.amazon.co.jp

少し前からデータ指向アプリケーションデザインを読み始めた
誰かがどこかに貼った転職ドラフトの友達紹介コードを使って転職ドラフトを登録してくれたらしく招待成立メールが来て、招待特典としてオライリー本が貰えるので前々から気になっていたこの本をお願いした
といってもこれが去年のことでずっと積んでいたんだけど最近読み始めたら面白くてもっと早く読んでおけばよかったと後悔している

この本、この画像の通り分厚い
600ページほどあり今のところ150ページくらいまで読んだ
そこまで読んだ感想としては、WEBエンジニアでサーバーサイドの開発を行っている方ならみんな読むべき本だと思った

今のところ読んだ章は以下

  • 1章 信頼性、スケーラビリティ、メンテナンス性に優れたアプリケーション
  • 2章 データモデルとクエリ言語
  • 3章 ストレージと抽出
  • 4章 エンコーディングと進化

1章ではデータシステムとはどういったものかの考察を行っている
信頼性やスケーラビリティ、メンテナンス性の考えの掘り下げを行いつつ、監視でパーセンタイルを使う考え方やTwitterを例にアプリケーションに応じてどのようにスケーラビリティを考えるかについても書かれている

2章ではリレーショナルモデルとドキュメントモデルについて扱い、それに付随するクエリ言語の説明になっている
ここではRDBやNoSQL周りの歴史や移り変わり、メリットデメリット、各クエリ言語の記法の特徴を抑えることが出来た

3章ではRDBなどのインデックスの元となる知識の説明がある
Bツリーについてはもちろん、列指向ストレージで用いられるLSMツリーなどについても掘り下げており、どうやってインデックスを作成していくのか、参照していくのか、メリットデメリットなどを説明している
この章だけでもこの本を読んでよかったと思える内容だった

4章ではエンコーディングについて扱っている
JSONやThift、Protocol Buffersなどのエンコーディングや圧縮方法などに加えて、どうやって前方互換後方互換を保つのかに踏み込んでいる
この本をDBだけを扱う本だと思っていたのでこういうところまで踏み込むのかと驚いた

この後の章にはレプリケーショントランザクション、それを分散システムで扱う場合、のような面白そうな話題が続いているので読み進めるのが楽しみな内容になっている

この本、作者があらゆる情報を網羅して分かりやすくまとまっている点がよい
凄く説明がすっと入ってくるし全く知らなかった古の知識も入ってくる
残りも読み進めていきたい

バイナリから文字エンコードを確認する

あるサーバーで問題があり、配信されているHTMLファイルが文字化けしていて謎の文字エンコードになっていたことがあり、各エディタでも正しく文字エンコードが認識できなかったのでバイナリから文字エンコードを特定できるのか調べて試してみた
(結局そのファイルは本当におかしくなっていたようで文字エンコードが特定出来はしなかったが)
試した環境はWSL上のUbuntu

以下のようなHTMLファイルがUTF-8で保存されているとき

<div>テストtest</div>

hexdump -C でバイナリを確認することが出来る

$ hexdump -C test-utf8.html
00000000  3c 64 69 76 3e e3 83 86  e3 82 b9 e3 83 88 74 65  |<div>.........te|
00000010  73 74 3c 2f 64 69 76 3e  0a                       |st</div>.|
00000019

大体文字エンコードUTF-8、Shift-JIS、EUC-JP、UTF-16あたりのどれかで間違って保存されていることが多いと思うのでそれぞれのマルチバイトの文字列がそれぞれバイナリだとどのような値になるか確認する

例えば上のHTMLには「テスト」というマルチバイト文字列が含まれているので「テスト」という文字列のそれぞれの文字エンコードでのバイナリを確認してみる
echo で文字列出力→ iconv -t [エンコード形式] で指定の文字エンコードに→ hexdump -C でバイナリ出力、という流れ
iconvを挟んだ時に改行が挟まるので最後に 0a がついていることに注意

$ echo "テスト" | iconv -t UTF-8 | hexdump -C
00000000  e3 83 86 e3 82 b9 e3 83  88 0a                    |..........|
0000000a

$ echo "テスト" | iconv -t Shift-JIS | hexdump -C
00000000  83 65 83 58 83 67 0a                              |.e.X.g.|
00000007

$ echo "テスト" | iconv -t EUC-JP | hexdump -C
00000000  a5 c6 a5 b9 a5 c8 0a                              |.......|
00000007

$ echo "テスト" | iconv -t UTF-16 | hexdump -C
00000000  ff fe c6 30 b9 30 c8 30  0a 00                    |...0.0.0..|
0000000a

上のHTMLを見てみるとUTF-8e3 83 86 e3 82 b9 e3 83 88 が含まれていることが分かるのでこれはUTF-8で保存されたファイルだということが特定できる

Goでtext/templateを使ってSQLを組み立てる

今、VちゃんではGoでコードを書いており、その中でDBにアクセスするコードではクエリビルダーは使わずSQLを直書きしている

書き込み系のSQLは基本的にシンプルになるので特に問題ない
しかし、読み取り系の特定条件での検索をするようなSQLを書くときにはSQLが複雑になり、そのままSQLを文字列として扱うだけでなく、特定のJOINを追加したりWHEREを追加したり、ということが起こる
こういった検索クエリは検索エンジンに投げることが一般的に多いと思うが、今の自分のアプリケーションは検索エンジンを入れているわけではないのでここをSQLで頑張る必要がある

まずはtext/templateの使い方

import (
    "bytes"
    "fmt"
    "testing"
    "text/template"
)

func TestExample(t *testing.T) {
    var buf bytes.Buffer
    tmpl := `
{{ if .flag }}
true
{{ else }}
false
{{ end }}
`
    data := map[string]interface{}{
        "flag": true,
    }
    tm := template.Must(template.New("tmpl").Parse(tmpl))
    if err := tm.Execute(&buf, data); err != nil {
        t.Fatal(err)
    }
    fmt.Println(buf.String())
}

出力例


true

{{ if .flag }} などを書いているところの改行はそのまま出力されるので注意 html/template というのもあるのでたまにimport間違えたりするので注意

複数条件は以下のように書く

func TestExample(t *testing.T) {
    var buf bytes.Buffer
    tmpl := `
{{ if and .flag1 .flag2 }}
true
{{ end }}
`
    data := map[string]interface{}{
        "flag1": true,
        "flag2": true,
    }
    tm := template.Must(template.New("tmpl").Parse(tmpl))
    if err := tm.Execute(&buf, data); err != nil {
        t.Fatal(err)
    }
    fmt.Println(buf.String())
}

and の後に条件を並べる
and に否定条件などを入れる場合は and (not .flag1) (not flag2) みたいにかっこで囲っておく

ドキュメントを見ると比較演算子を使えたりいくつかの演算子がtemplate内での独自構文で提供されている

golang.org

SQLを書く

実際にSQLを書いていく

SELECT
    vtuber.id
FROM
    vtuber
WHERE
    vtuber.deleted_at IS NULL

これは実際のGoのアプリケーションでsqlxで実行されるSQLをシンプルにしたもの
VTuberの一覧を取得しているSQLである
一覧を取得するときにはここに検索条件が追加される
例えばある個性が設定されているVTuberのみ取得するSQLにしてみよう

SELECT
    vtuber.id
FROM
    vtuber
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
WHERE
    vtuber.deleted_at IS NULL
    AND vtuber_personality.personality_id IN (:personalities)

個性の検索をつけることで関連テーブルへのJOIN、WHEREでの絞り込みが追加された

更に配信スタイルでの絞り込みも行ってみよう

SELECT
    vtuber.id
FROM
    vtuber
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
WHERE
    vtuber.deleted_at IS NULL
    AND vtuber_personality.personality_id IN (:personalities)
    AND vtuber_video_style.vtuber_style_id IN (:video_styles)
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count

条件に全て合致していることを確認するためにGROUP BYが追加された

こんな感じで検索のクエリは条件が追加されるたびに少しづつ複雑さを増していく
じゃあこのSQLをアプリケーション内で実行しようと思ったときにいくつかの選択肢が浮かぶ

  • SQLを文字列連結で動的に変える
  • クエリビルダーを使う
  • SQL自体は変わらないようにSQL内で条件分岐を活用する(WHEREの条件などで頑張る)

それぞれの方法は良し悪しあるかと思うが今回はこのSQLをtext/templateパッケージを使用して組み立ててみようと思う

text/templateでSQLを組み立てる

上記の条件を元にSQLが組み立てられるようにしたtemplateはこのようになる

SELECT
    vtuber.id
FROM
    vtuber
{{ if .personalities }}
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
{{ end }}
{{ if .vtuberGroups }}
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
{{ end }}
WHERE
    vtuber.deleted_at IS NULL
{{ if .personalities }}
    AND vtuber_personality.personality_id IN (:personalities)
{{ end }}
{{ if .vtuberGroups }}
    AND vtuber_video_style.vtuber_style_id IN (:video_styles)
{{ end }}
{{ if or .personalities .vtuberGroups }}
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count
{{ end }}

Goで実行するとするとこんな感じ

func TestExample(t *testing.T) {
    tmpl := `
SELECT
  vtuber.id
FROM
  vtuber
{{ if .personalities }}
INNER JOIN vtuber_personality
  ON vtuber_personality.vtuber_id = vtuber.id
{{ end }}
{{ if .vtuberGroups }}
INNER JOIN vtuber_video_style
  ON vtuber_video_style.vtuber_id = vtuber.id
{{ end }}
WHERE
  vtuber.deleted_at IS NULL
{{ if .personalities }}
  AND vtuber_personality.personality_id IN (:personalities)
{{ end }}
{{ if .vtuberGroups }}
  AND vtuber_video_style.vtuber_style_id IN (:video_styles)
{{ end }}
{{ if or .personalities .vtuberGroups }}
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count
{{ end }}
`

    var buf bytes.Buffer
    data := map[string]interface{}{
        "personalities": []string{"p1"},
        "vtuberGroups": []string{"g1"},
    }
    tm := template.Must(template.New("tmpl").Parse(tmpl))
    if err := tm.Execute(&buf, data); err != nil {
        t.Fatal(err)
    }

    fmt.Println(buf.String())
}

出力されるSQLはこちら
実際にはこの buf.String() をsqlxなどの実行クエリとして流し込むことになる

SELECT
    vtuber.id
FROM
    vtuber

INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id


INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id

WHERE
    vtuber.deleted_at IS NULL

    AND vtuber_personality.personality_id IN (:personalities)


    AND vtuber_video_style.vtuber_style_id IN (:video_styles)


GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count

改行が変に入るのが気になるところ
何も指定しなかった場合、さらにこんな感じになる

SELECT
    vtuber.id
FROM
    vtuber


WHERE
    vtuber.deleted_at IS NULL




そんなときは {{ if .personalities -}}~~~{{- end }} のように波かっこに改行やスペースを削除したいところにハイフンを記述してあげればマシになる

golang.org

SELECT
    vtuber.id
FROM
    vtuber
{{ if .personalities -}}
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
{{- end }}
{{ if .vtuberGroups -}}
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
{{- end }}
WHERE
    vtuber.deleted_at IS NULL
{{ if .personalities -}}
    AND vtuber_personality.personality_id IN (:personalities)
{{- end }}
{{ if .vtuberGroups -}}
    AND vtuber_video_style.vtuber_style_id IN (:video_styles)
{{- end }}
{{ if or .personalities .vtuberGroups -}}
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count
{{- end }}

これを実行すると

SELECT
    vtuber.id
FROM
    vtuber
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
WHERE
    vtuber.deleted_at IS NULL
AND vtuber_personality.personality_id IN (:personalities)
AND vtuber_video_style.vtuber_style_id IN (:video_styles)
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count

うんうん、条件指定した場合は綺麗になった

条件未指定の場合は?

SELECT
    vtuber.id
FROM
    vtuber


WHERE
    vtuber.deleted_at IS NULL




まあまあ改行入ってしまう
ここは微妙なところ

所感

  • template内に全ての条件が入っているので条件の全容を把握しやすい
  • ただしちょっと見にくい
  • パッとコピーしてSQL流して試すとかはやりにくい
  • シンタックスハイライトが効かないかもね、SQLかつtext/templateの構文が混ざるので

Realforceのキーボードを買った

勝ったのはかなーりまえだけど下書きに残りっぱなしだったので放流


f:id:igatea:20190504211727j:image
f:id:igatea:20190504211815j:image
f:id:igatea:20190504211825j:image

 

以下付属品各種


f:id:igatea:20190504211810j:image
f:id:igatea:20190504211820j:image
f:id:igatea:20190504211839j:image
f:id:igatea:20190504211834j:image

 

 

他にも選択肢としてHHKBとかあると思うだけど、自分はメインがWindowsなのでWindowsボタンがどうしても欲しいのです

テンキーも使ったとしてもゲームくらい?不要なのでテンキーレス

使ってみて

もう数年使っているが前の標準キーボードには戻れない

音も小さいし打鍵の深さとかも調整出来てめっちゃ楽

文字を打つ時は結構浅めが好きなんだけど、浅いとゲームしているときに結構誤爆する。最初は困ったけど、それも慣れて問題なくなった

色は白がおすすめ、黒の同じものを会社で使っているがキーに書いてある文字が見えにくくてちょっと微妙。かっこいいんだけどね

 

sqlxでIN句を使う方法

最近Goでsqlxを使っているが、毎回忘れるのでメモ

IN句を用いたクエリ

sqlxでIN句を用いたクエリを使うにはそのままクエリを投げることはできない。
一度 sqlx.In を通してあげる必要がある。

type Hoge struct {
    Id string `db:"id"`
}

db, err := sqlx.Connect("mysql", "接続情報")
if err != nil {
    panic(err)
}

query := `
SELECT id FROM hoge
WHERE id IN (?)
`
idList := []string{"aaa", "bbb", "ccc"}

// 一度sqlx.Inを通してあげる必要がある
query, args, err := sqlx.In(query, idLIst)
if err != nil {
    panic(err)
}
// DBのドライバーに合わせたクエリの変換
query = db.Rebind(query)

var result []*Hoge
err = db.Select(&result, query, args...)
if err != nil {
    panic(err)
}

return result

Selectの結果を別に詰め替えるなら

// 上の結果がresult変数に入っているとして
type Fuga struct {
    Id string
}

// 結果の長さ文の容量を確保したスライスを用意
newResult := make([]*Fuga, 0, len(result))

for _, r := range result {
    newResult = append(newResult, &Fuga{Id: r.Id})
}

return newResult

ここらへんのappendがGo特有というかLLやJavaといった言語ではあまり見ないやり方

IN句に指定する値以外にバインドが必要な場合

SQL:idList のように名前を指定して値をバインドでき、それはもちろんIN句との併用も可能
以下のようにsqlx.Inとsqlx.Namedを組み合わせてクエリを組み立てていく

type Hoge struct {
    Id string `db:"id"`
}

db, err := sqlx.Connect("mysql", "接続情報")
if err != nil {
    panic(err)
}

query := `
SELECT id FROM hoge
WHERE
    id IN (:idList)
    AND data_type = :dataType
`
idList := []string{"aaa", "bbb", "ccc"}
dataType := "xxxType"

input := map[string]interface{}{
    "idList": idList,
    "dataType": dataType,
}

// 最初にsqlx.Named
query, args, err := sqlx.Named(query, input)

// 次にsqlx.In
query, args, err := sqlx.In(query, args...)
if err != nil {
    panic(err)
}
query = db.Rebind(query)

var result []*Hoge
err = db.Select(&result, query, args...)
if err != nil {
    panic(err)
}

return result

決まった手続きなので以下のようなラッパー関数を作ってもいいかもしれない

func NamedInSql(db sqlx.DB, query string, arg interface{}) (string, []interface{}, error) {
    query, args, err := sqlx.Named(query, arg)
    if err != nil {
        return "", nil, err
    }

    query, args, err = sqlx.In(query, args...)
    if err != nil {
        return "", nil, err
    }
    query = db.Rebind(query)
 
    return query, args, err
}

最近ブログ書いてないな

どーも。
お久しぶりです。

何気なくこのブログの最終更新を見たら3月。
ということで9カ月ぶりの更新です。

なんか最近どこかの記事で、ブログとかアウトプットの敷居を下げていこうみたいなのを見て、そうだなと思い筆をとった次第。

もう3月あたりからはずっとコロナでリモートワークで時間の余裕もあるのと(仕事は物凄く忙しいが)、最近はVちゃん https://vtuber-channel.com/ というVTuber情報サイトを開発していたこともあり技術的にちょっとした知見は貯まったので、適度にアウトプットをしていきたい。

今日はその意思表明ってことで。

PS

VTuberに興味がある人Vちゃん使ってくれ

vtuber-channel.com

現在時刻を扱う処理をJavaScriptで書いてPCの時刻を変更して試すとき、ブラウザによって癖がある

現在時刻を扱う処理をJavaScriptで書くことはよくあると思う。ある日時を過ぎていたらリンクを非活性にしたり。
そんなときにブラウザで試そうと思うとPCの時刻を変えるのが手軽なわけだ。
もちろん現在時刻をモックして変えられる仕組みを導入していればいいのだが、WEBフロントエンドではそういう仕組みを導入しているところは少ないんじゃないかと。

じゃあ試してみようと思ってPCの時刻を変えてChromeで試してみると数十秒待たないとその時刻が反映されなかったりする。
Firefoxで試すと即時反映される。ブラウザごとに違うんだなーってことで一旦2ブラウザでどう変わってくるか試してみた。
検証したOSはWindowsMacとかだとまた違うかもしれない。

ブラウザ 現在時刻 タイムゾーン
Chrome タブを新しく開けば更新される、そのまま待っていても数十秒更新されない、ページを更新しても数十秒更新されない 即時反映
Firefox 特に更新もせず即時反映 タブを引き直しても反映されない、Firefox自体を落とさないといけない

Windowsとかだと「日付と時刻」というところから「日付を手動で設定する」のところから日時を変更することが出来る。
んで、ブラウザの開発者ツールからConsoleを開いて new Date と叩けば現在時刻が出てくるのでそれで確認。 f:id:igatea:20200308024107p:plain

現在時刻に加えてタイムゾーンの変更時の挙動も検証してみたらちょっと仕様が違っていて面倒だなと。
SafariやEdgeは検証していないが実際に試してみる場合はこのことを頭に入れておかないとハマりそう。