LoginSignup
8
9

More than 5 years have passed since last update.

GolangとRedisで遊んでみた

Last updated at Posted at 2018-04-21

始めに

GolangでCQRSのQに使える高速なDBは無いもんかと考えてたりしたので、とりあえずRedisで遊んでみた。
クライアントはgithub.com/gomodule/redigoを使います。

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

構成

Golang: 1.9
Redis(Docker): 4.0
redigo: 2.0

成果物

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

モデル

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

Untitled (1).png

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

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

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

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
}
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・・・キーごと削除(テスト用)
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が死んだときのリカバーとかまでは考えません!。

テスト

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

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のクエリより早くなるかは不安でしかない・・・。

参考にさせてもらいました

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9