GolangとRedisで遊んでみた

#始めに
Golangで[CQRS](https://martinfowler.com/bliki/CQRS.html)のQに使える高速なDBは無いもんかと考えてたりしたので、とりあえず[Redis](https://redis.io/)で遊んでみた。
クライアントは[github.com/gomodule/redigo](https://github.com/gomodule/redigo)を使います。

※Redisの解説はしません（そもそもSETとGETくらいしか知らない）。

##構成##
Golang: 1.9
Redis（[Docker](https://hub.docker.com/_/redis/)）: 4.0
redigo: 2.0

##成果物##
https://github.com/lightstaff/go-redis-example
※実装はexampleディレクトリの中にあります。

#モデル
遊びなのでシンプルに、「下図のような感じのモデルをRedisに保存し、それをビュー（ビューモデル）として求めたい」こととします。

![Untitled (1).png](https://qiita-image-store.s3.amazonaws.com/0/94252/189cd03f-faa9-38a7-b8c2-d78b889c472c.png)


Redisには`json.Marshal`でバイナリ化したものを保存するつもり（というか他の保存方法が分からないの）ですが、悩みどころなのはキーとバイナリ化前のデータです。

- シンプルにユーザーIDをキーにしてユーザー単位で保存した場合・・・一覧取得が面倒。
- キーを単一にして全ユーザーをsliceにして保存した場合・・・一覧取得は一発、ID検索等はsliceを走査＝面倒。
- キーを単一にしてユーザーIDをキーにしたmapで保存した場合・・・一覧取得も、ID検索も楽そう。

というわけで、map化を採用し、定義します。コードでニュアンスが伝わればいいなぁ・・・。

```golang:model.go
package example

// Radisにはmap[string]*Userのバイナリが保存される

// User is user model
type User struct {
	ID       string
	Name     string
	Contacts map[string]*Contact // ContactをIDで捕まえるため
}

// Contact is contact model
type Contact struct {
	ID     string
	Email  string
	UserID string
}
```

```golang:view_model.go
package example

// UserView is user view
type UserView struct {
	ID       string
	Name     string
	Contacts []*ContactView
}

// ContactView is contact view
type ContactView struct {
	ID    string
	Email string
}
```

#サービス
一応やり取りをサービスとして定義します。

- `SetUser(u *User) error`・・・ユーザーを保存
- `SetContact(c *Contact) error`・・・連絡先を保存
- `GetUserViews() ([]*UserView, error)`・・・ユーザービューを一覧取得
- `GetUserView(id string) (*UserView, error)`・・・ユーザービューをID指定取得
- `Remove() error`・・・キーごと削除（テスト用）

```golang:service.go
package example

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"

	"github.com/gomodule/redigo/redis"
)

// Service is service interface
type Service interface {
	SetUser(u *User) error
	SetContact(c *Contact) error
	GetUserViews() ([]*UserView, error)
	GetUserView(id string) (*UserView, error)
	Remove() error
}

// ServiceImpl is service implement
type ServiceImpl struct {
	Conn redis.Conn
}

// SetUser is set user
func (s *ServiceImpl) SetUser(u *User) error {
	um, err := s.get()
	if err != nil {
		return err
	}

	um[u.ID] = u

	if err := s.set(um); err != nil {
		return err
	}

	log.Printf("[INFO] set user. id: %s\n", u.ID)

	return nil
}

// SetContact is set contact
func (s *ServiceImpl) SetContact(c *Contact) error {
	um, err := s.get()
	if err != nil {
		return err
	}

	if v, ok := um[c.UserID]; ok {
		if v.Contacts == nil {
			v.Contacts = map[string]*Contact{c.ID: c}
		} else {
			v.Contacts[c.ID] = c
		}

		um[c.UserID] = v
	} else {
		return fmt.Errorf("unknown user. id: %s", c.UserID)
	}

	if err := s.set(um); err != nil {
		return err
	}

	log.Printf("[INFO] set contact. id: %s\n", c.ID)

	return nil
}

// GetUserViews is get user views
func (s *ServiceImpl) GetUserViews() ([]*UserView, error) {
	um, err := s.get()
	if err != nil {
		return nil, err
	}

	users := make([]*UserView, 0)

	for _, u := range um {
		contacts := make([]*ContactView, 0)

		for _, c := range u.Contacts {
			contacts = append(contacts, &ContactView{
				ID:    c.ID,
				Email: c.Email,
			})
		}

		users = append(users, &UserView{
			ID:       u.ID,
			Name:     u.Name,
			Contacts: contacts,
		})
	}

	log.Printf("[INFO] get user views. length: %d\n", len(users))

	return users, nil
}

// GetUserView is get user view by id
func (s *ServiceImpl) GetUserView(id string) (*UserView, error) {
	um, err := s.get()
	if err != nil {
		return nil, err
	}

	u, ok := um[id]
	if !ok {
		return nil, fmt.Errorf("not found user. id: %s", id)
	}

	contacts := make([]*ContactView, 0)

	for _, c := range u.Contacts {
		contacts = append(contacts, &ContactView{
			ID:    c.ID,
			Email: c.Email,
		})
	}

	log.Printf("[INFO] get user view. id: %s\n", u.ID)

	return &UserView{
		ID:       u.ID,
		Name:     u.Name,
		Contacts: contacts,
	}, nil
}

// Remove is remove key
func (s *ServiceImpl) Remove() error {
	if s.Conn == nil {
		return errors.New("not initialized redis conn")
	}

	if _, err := s.Conn.Do("DEL", "users"); err != nil {
		return err
	}

	log.Print("[WARN] remove users")

	return nil
}

func (s *ServiceImpl) set(um map[string]*User) error {
	if s.Conn == nil {
		return errors.New("not initialized redis conn")
	}

	bytes, err := json.Marshal(um)
	if err != nil {
		return err
	}

	if _, err := s.Conn.Do("SET", "users", bytes); err != nil {
		return err
	}

	return nil
}

func (s *ServiceImpl) get() (map[string]*User, error) {
	if s.Conn == nil {
		return nil, errors.New("not initialized redis conn")
	}

	um := make(map[string]*User)

	exists, err := redis.Int(s.Conn.Do("EXISTS", "users"))
	if err != nil {
		return nil, err
	}
	if exists == 0 {
		return um, nil
	}

	bytes, err := redis.Bytes(s.Conn.Do("GET", "users"))
	if err != nil {
		return nil, err
	}
	if bytes == nil {
		return um, nil
	}

	if err := json.Unmarshal(bytes, &um); err != nil {
		return nil, err
	}

	return um, nil
}
```

やってることはシンプルなmapの置き換えとmap→sliceの変換なので特に解説することはありません。Redisが死んだときのリカバーとかまでは考えません！。

#テスト
今回は雑にテストしてお終いにします。

```golang:service_test.go
package example

import (
	"testing"

	"github.com/gomodule/redigo/redis"
	"github.com/google/uuid"
)

func TestService(t *testing.T) {
	conn, err := redis.DialURL("redis://192.168.99.100")
	if err != nil {
		t.Fatal(err)
	}

	s := &ServiceImpl{
		Conn: conn,
	}

	// remove
	if err := s.Remove(); err != nil {
		t.Fatal(err)
	}

	// set user
	uID := uuid.New().String()
	cID1 := uuid.New().String()
	u := &User{
		ID:   uID,
		Name: "test",
		Contacts: map[string]*Contact{cID1: &Contact{
			ID:     cID1,
			Email:  "first@test.com",
			UserID: uID,
		}},
	}

	if err := s.SetUser(u); err != nil {
		t.Fatal(err)
	}

	// set contact
	cID2 := uuid.New().String()
	c := &Contact{
		ID:     cID2,
		Email:  "second@test.com",
		UserID: uID,
	}

	if err := s.SetContact(c); err != nil {
		t.Fatal(err)
	}

	// get user view
	uv, err := s.GetUserView(uID)
	if err != nil {
		t.Fatal(err)
	}

	if uv.ID != u.ID {
		t.Error("user view id not eqaul input user id")
	}

	t.Logf("get user view. data: %v", uv)

	// get user views
	uvs, err := s.GetUserViews()
	if err != nil {
		t.Fatal(err)
	}

	if len(uvs) != 1 {
		t.Error("user views length is not 1")
	}

	t.Logf("get user views. data: %v", uvs)
}
```

#終わりに
mapがどれくらいリソース食うか読めないので、RDBのクエリより早くなるかは不安でしかない・・・。

##参考にさせてもらいました##
- 【Redis】Go言語で高速呼び出しKVS【Redigo】https://qiita.com/chan-p/items/5c5e7cc1e966f8a90422