メメメモモ

プログラミング備忘録、リモートワーク生活、筋トレ、etc

CQRSとDynamoDBのデータ構造設計

概要

最近DynamoDBで設計を行っています。DynamoDBは、NoSQLであり様々な制約があります。ですので、RDBの設計とは異なる考え方をしなければなりません。

四苦八苦しながら、設計をしていて、何とか形になってきています。そんな中、CQRSという考え方があることを知りました。自分が何となくやっていたことと近いこともあり、改めてCQRSとDynamoDBのデータ構造設計についてまとめてみました。

CQRSとは

CQRSは「Command Query Responsibility Segregation:コマンドクエリ責任分離」という意味です。

DBへの書き込み(Command)と読み込み(Query)を別物として捉える考え方です。

書き込み用と読み込み用のそれぞれのDBを分離するイメージですね。

RDBのリードレプリカに似ています。
リードレプリカで、書き込み用DB(マスター)と読み込み用DB(スレーブ)があります。マスターとスレーブではデータ構造(スキーマ)は完全に同じです。
それに対してCQRSでは、書き込み用と読み込み用でデータ構造が異なっていることを許容していると言えます。

DynamoDBでのCommand用データ構造

DynamoDBには、以下のような特性があります。

  • NoSQLなのでRDBのようなトランザクションがない
    • 複数レコードにまたがる更新にアトミック性を保持することができない
  • 1レコードの更新はアトミック性がある

この特性を考慮すると、Command用データ構造を設計する場合、 アトミック性を保持したいデータは1レコードに収められるようなデータ構造にしておく必要があります。

DynamoDBでのQuery用データ構造

またDynamoDBには、以下のような特性もあります。

  • 1テーブルにつき作成できるインデックス数が限られている(GSI/LSIそれぞれ5個まで)
  • ソートするカラムはインデックスが貼られている必要がある(RangeKey)

これらの制約を考慮し、データの取得パターンに応じて、最適なデータ構造(Query用データ構造)を作っておく必要があります。

例えば、以下のようなアクセスパターンがあるでしょうか。

  • ソート/検索/ページネーションを行い、データの一覧を取得する
  • グループに所属しているユーザー数を取得する(あらかじめ集計しておくなど)

Query用のDBはDynamoDBに限らず、ElasticSearchを用いても良い場合もありそうです。

CommandデータからQueryデータへの変換について

CommandデータからQueryデータを作成する必要があります。これはDynamoDB Stream経由で、Lambdaを実行して行うのが良さそうです。作成失敗した時はLambdaが再試行するので、ほぼ変換は成功させることができます。

その他の考察

どうしてもトランザクションが欲しい場合は、Kinesis経由でRDBを用いてデータを更新することも可能だと思います(Command)。 更新が成功したら、DynamoDBやElasticSearchにQuery用データを作成することができます。

参考

www.yumemi.co.jp

postd.cc

marcy.hatenablog.com

型付きダックタイピング

概要

Go言語のinterfaceとダックタイピングについてまとめました。

ダックタイピングとは

もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである。

ダック・タイピング - Wikipedia

オブジェクト指向の話に置き換えると、
それが「何かしらのオブジェクト」
アヒルのように歩き アヒルのように鳴く が「メソッド」
に対応しています。

先ほどの文章を言い換えると、

もしもオブジェクトがDuckオブジェクトのようにWalk()メソッドを呼び出すことができ、DuckオブジェクトのようにSound()メソッドを呼び出すことができるのなら、それはDuckオブジェクトとして扱える。

となるでしょうか。

Rubyでのダックタイピング

受け取る引数fooの型が何かは制限していません。 soundメソッドを呼び出すことができれば問題がありません。

def test(foo)
  puts foo.sound
end

これの何が嬉しいか。
例えば、テストを書く時にモックオブジェクトを渡せます。

以下は、ダックタイピングではないJavaのコードです。
受け取る引数fooの型をDuckに制限しています。

void test(Duck foo) {
   foo.sound();
}

Javaでinterfaceを使ったダックタイピング(のようなもの)

Javaでもinterfaceを使ってダックタイピングの同じようなことができます。

interface ISomething {
  void sound();
}

void test(ISomething foo) {
  foo.sound();
}

このコードでも、引数ではSomethingを実装(soundメソッドを実装)したオブジェクトであれば、受け取れるようになっています。直接Duckオブジェクトを受け取る形より結合度は低いですね。

例えば、testメソッドにDuckMockオブジェクトを渡したい場合は以下のように定義します。

class DuckMock implements ISomething {
  void sound() {
    system.out.println("Ga-Ga-");
  }
}

DuckMockはISomethingを実装しているということを明示的に宣言しています。

Goのダックタイピング

Go言語にもinterfaceがあり、以下のように書けます。

type Something interface {
  Sound()
}

Javaインターフェイスと異なる点は、implementsで明示的に宣言しなくても良いところです。DuckMockは以下のように書きます。

type DuckMock struct {}

func (d *DuckMock) Sound() {
    fmt.Println("Ga-Ga-")
}

単純にインターフェースと同じシグニチャーを持ったメソッドを定義するだけで済みます。

この点で、よりダックタイピングに近い形になっているので、Go言語のinterfaceの機能は型付きのダックタイピングと言えます。

おまけ:一つもメソッドがないinterfaceは、どういった型を受け取るか?

以下のfooはどういった型を受け取るでしょうか?

func test(foo interface{}) {
   ...
}

答えは「どんな型でも受け取る」です。一つもメソッドが指定されていないので、どのオブジェクトでも当てはまるということですね。

Go言語では、色々な型を受け取りたい場合は、これを応用します。極力避けたほうが良いですが。

まとめ

Go言語のinterfaceをダックタイピングという概念とともに解説してみました。他のオブジェクト指向をやってきた人でも戸惑うポイントだと思うので、理解の助けになれば幸いです。

Go言語でAWS ElasticSearchを操作する

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

type User struct {
    Name      string    `json:"name"`
    Age       int       `json:"age"`
    Note      string    `json:"note"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

const URL = "https://xxxxxxxxxxx.ap-northeast-1.es.amazonaws.com"

func newRequest(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
    req, err := http.NewRequest(method, url, body)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", "application/json")

    req = req.WithContext(ctx)

    return req, nil
}

func decodeBody(resp *http.Response, out interface{}) error {
    defer resp.Body.Close()
    var r io.Reader = resp.Body
    decoder := json.NewDecoder(r)
    return decoder.Decode(out)
}

func encodeBody(jsonMap interface{}) (io.Reader, error) {
    b, err := json.Marshal(jsonMap)
    if err != nil {
        return nil, err
    }
    return bytes.NewReader(b), nil
}

func describeSetting(ctx context.Context) {
    req, err := newRequest(ctx, "GET", URL, nil)
    if err != nil {
        panic(err)
    }

    httpClient := http.DefaultClient
    res, err := httpClient.Do(req)
    if err != nil {
        panic(err)
    }

    log.Printf("%#v", res)
    var ret map[string]interface{}
    if err := decodeBody(res, &ret); err != nil {
        panic(err)
    }

    log.Printf("%#v", ret)
}

func createUsers(ctx context.Context) {
    for i := 0; i < 100; i++ {
        user := User{
            Name:      fmt.Sprintf("Name_%d", i+1),
            Age:       i + 1,
            Note:      fmt.Sprintf("Note_%d", i+1),
            CreatedAt: time.Now(),
            UpdatedAt: time.Now(),
        }

        body, err := encodeBody(user)
        if err != nil {
            panic(err)
        }

        req, err := newRequest(ctx, "POST", fmt.Sprintf("%s/users/doc", URL), body)
        if err != nil {
            panic(err)
        }

        res, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }

        log.Printf("%#v", res)
    }
}

func searchByName(ctx context.Context, name string) {
    query := map[string]interface{}{
        "query": map[string]interface{}{
            "match": map[string]interface{}{
                "name": "Name_10",
            },
        },
        "sort": map[string]interface{}{
            "name": map[string]interface{}{
                "order": "desc",
            },
        },
    }
    body, err := encodeBody(query)
    if err != nil {
        panic(err)
    }

    req, err := newRequest(ctx, "GET", fmt.Sprintf("%s/users/doc/_search?pretty", URL), body)
    if err != nil {
        panic(err)
    }

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }

    var ret map[string]interface{}
    err = decodeBody(res, &ret)
    if err != nil {
        panic(err)
    }

    log.Printf("%#v", ret)
}

func main() {
    ctx := context.Background()
    searchByName(ctx, "Name_2")
}

glogでDebug用ログ出力の関数を作るときの注意点

結論

以下のように、DEBUG環境変数が設定されているときに、ログ出力するような関数を定義した場合は、glog.XxxDepth(例ではglog.InfoDepth)を使いましょう。

package main

import (
    "flag"
    "fmt"
    "github.com/golang/glog"
    "os"
)

func DebugLog(v interface{}) {
    if os.Getenv("DEBUG") != "" {
        glog.InfoDepth(1, fmt.Sprintf("%#v", v))
    }
}

func main() {
    flag.Set("stderrthreshold", "INFO")
    flag.Parse()
    os.Setenv("DEBUG", "1")
    DebugLog("hoge") // ここが20行目
    DebugLog("fuga") // ここが21行目
}

出力結果は以下のようになります。

I0829 22:47:11.932758   24176 main.go:20] "hoge"
I0829 22:55:36.233685   24681 main.go:21] "fuga"

出力場所として、main.go:20 main.go:21 が情報として含まれていますね。多くの場合、DebugLog("hoge")DebugLog("fuga")が記述されている行数を出力して欲しいので、意図通りとなっています。

InfoDepthの代わりにInfoを使うとどうなるのか

以下のように Info関数を使うとどうなるかを示します。

package main

import (
    "flag"
    "fmt"
    "github.com/golang/glog"
    "os"
)

func DebugLog(v interface{}) {
    if os.Getenv("DEBUG") != "" {
        glog.Info(fmt.Sprintf("%#v", v))  // ここが12行目
    }
}

func main() {
    flag.Set("stderrthreshold", "INFO")
    flag.Parse()
    os.Setenv("DEBUG", "1")
    DebugLog("hoge")
    DebugLog("fuga")
}

出力結果は以下のようになります。

I0829 22:57:00.569297   24777 main.go:12] "hoge"
I0829 22:57:00.569872   24777 main.go:12] "fuga"

はい、hoge の出力も fugaの出力もmain.go:12として出力されてしまっています。つまり glog.Infoを呼び出している箇所ですね。これが InfoDepthを使う理由となっています。

コマンドラインの引数ではなくプログラムでglogの設定を行う方法

結論

flag.Setを使います。
例えば、stderrthresholdで、ログレベルの閾値を設定したい場合は、以下のようになります。

func main() {
    flag.Set("stderrthreshold", "INFO")
    flag.Parse()

    glog.Info("Hoge")
}

参照

godoc.org

Goのfor rangeでのポインタでハマったこと

概要

以下のプログラムの出力結果はどうなるでしょうか?

package main

import "fmt"

func main() {
    a := []string{"A", "B", "C"}
    var b []*string

    for _, str := range a {
        b = append(b, &str)
    }

    for _, str := range b {
        fmt.Println(*str)
    }
}

出力結果は以下のようになります。

C
C
C

解説と検証コード

strがループ中にずっと同じポインタになっていることが原因です。

以下のプログラムで&strの値を出力してみます。
つまり、strが指している領域のアドレスを出力します。

package main

import "fmt"

func main() {
    a := []string{"A", "B", "C"}

    for _, str := range a {
        fmt.Println(&str)
    }
}

以下のように、ループ中に常に同じアドレスになっています。

0xc42000e1e0
0xc42000e1e0
0xc42000e1e0

つまり _, str := range aを実行するたびに、同じ領域の値を上書きしていっているということです。

最初のコード例では、bの配列に上記の結果が入るため、ループの最後で上書きされるCが出力されます。

意図した処理にするには?

インデックス変数を使うようにすると良さそうです。

package main

import "fmt"

func main() {
    a := []string{"A", "B", "C"}
    var b []*string

    n := len(a)
    for i := 0; i < n; i++ {
        b = append(b, &a[i])
    }

    for _, str := range b {
        fmt.Println(*str)
    }
}
A
B
C

参考

qiita.com

実感した筋トレの効果について

概要

ここ2年くらいは筋トレを継続的にやっています。

筋トレを始めてから、色々なことが前よりも上手く回り始めたような気がしています。

筋トレの何が良いのか、まとめてみました。

重力に負けなくなる

冗談のようなことですが、筋トレをする前は重力に負けていました
何をするにも身体が重くて行動に移せなかったのです。

「なんか全てに対してやる気でないなー」

と思っていたのですが、
単純に身体が重力に負けているからだと気づきました。

筋トレをし始めてからは案の定、身体が軽く感じるようになりました。
その分、行動力も増したように思います。

ストレス解消になる

仕事をしているとストレスを感じることは避けられません。
よく言われていることですが、社会人はストレスを解消する方法を持っておくべきです。

自分もストレスを貯めやすい方だったので、色々な解消法を試していました。 アロマやお香をやってみたり、マッサージに行ってみたり、前向きになれそうな読書をするなど。

結局は、ストレス解消には筋トレが最強でした。

筋トレをして、脳に新鮮な酸素を送り、筋肉を全力で疲れさせると、すごく気持ちがいいです。 筋トレ中は余計なことを考えなくなるので、瞑想の効果もある気がします。

モヤモヤとした気持ちが出たら、とりあえず筋トレですね。

体調管理になる

身体が強くなり、メンタルも強くなるので、病気になりにくくなります。
仕事がちょっと忙しくなったくらいでは、体調も崩さなくなりました。

たまに油断して、裸に近い格好で寝てしまい、ちょっとした風邪を引いてしまう場合もありますが...w
その時でも、すぐに治ります。

疲れにくくなる

筋トレをしてスタミナが増えたのか、疲れにくくなりました。

スタミナが例えば2倍になったとしたら、普段の仕事などは、前よりも1/2の体力で片付けられるようになります。

この余裕が出てくるとストレスも少なくなります。

テストステロンが分泌されてやる気が出る

筋トレをすると、脳内にテストステロンという物質が増えるそうです。

テストステロンとは、男性ホルモンの一種です。もっと言うと男性ホルモンの大部分を占める重要な要素の一つで、骨・筋肉・性欲・バイタリティ作用など男性として生きるために大切なホルモン。

テストステロン値を増やす方法。モテる以外の驚くべき効果も。 | Smartlog

ポジティブシンキング といったことがよく言われていますが、
頭で考えているだけでは、なかなかできるものではありません。

物理的に脳に作用させる具体的な方法として、

筋トレをしてテストステロンを分泌させる

というのが効果的なのではないかと思います。

まとめ

筋トレの効果についてまとめてみました。

以下の本は、おすすめです。
筋トレがどれだけ世界を救うかを論じている本ですw 極論じみたことが書かれていますが、実感としてはあながちウソではないなと思っています。Twitterのツイートが基となっている本なので、とても読みやすいです。

筋トレが最強のソリューションである マッチョ社長が教える究極の悩み解決法

筋トレが最強のソリューションである マッチョ社長が教える究極の悩み解決法