データのバージョン管理が可能な分散データベースNomsをさわってみる

# はじめに
データベースやデータのバージョン管理をしたいと思ったことはありませんか？
データのバージョン管理が簡単にできれば、好きなタイミングのデータで分析したり、移行データを必要な各断面で管理したり、データが更新されたトレースを取ることもできるかもしれません。

そんなことが簡単にできればいいのになーと思っていたのですが、先日Tech Crunchの記事でGitの考え方を取り入れたNomsというデータベースを知りました。Googleで検索してみても日本語の情報があまり出てこなかったため、（どんなものかいまいち分かっていないのですが）とりあえずインストールからさわってみるまでを書いておきます。

[Git - attic-labs/noms](https://github.com/attic-labs/noms)
[TC - コンテンツ・アドレッサブルで多バージョン分散データベースNomsのAttic Labsが$8.1Mを調達](http://jp.techcrunch.com/2016/08/03/20160802attic-labs-noms-noms/)

# Nomsとは
Gitの考え方を取り入れたデータベース…らしいです。
nomsではなくてNoms…らしいです。けど、nomsでもいい…らしいです。
個人的には下記の比較が分かりやすかったです！

| Noms | Git |
|:-----|----:|
| Database | Repository |
| Dataset | Branch |
| Commit | Commit |
| Bool, Number, String, Blob, List, Set, Map, Struct | Blob, File, Tree |

ただ、Nomsでもファイルを扱うことはできそうです。
https://github.com/attic-labs/noms/tree/master/samples/go/nomsfs


# 環境構築
## Ubuntu
今回はUbuntu環境で試してみます。
自分の場合は、以前にMesos/Marathon環境を作ったため、Mesos/Marathon環境にDockerのUbuntuイメージ（Ubuntu 14.04.4 LTS）をデプロイして試しました。設定値は次の通りです。

General

| 項目 | 設定値 |
|:-----|----:|
| ID | 適当 |
| CPUs | 適当 |
| Memory | 適当 |
| Disk Space | 適当 |
| Command | /usr/sbin/sshd -D |

Docker Container

| 項目 | 設定値 |
|:-----|----:|
| Image | ykshr/ubuntu-ssh |
| Network | Bridged |

Ports

| 項目 | 設定値 |
|:-----|----:|
| Container Port | 22 |
| Container Port | 8000 |

もし興味があれば、以前のブログにMesos/Marathon環境の作り方も記載しているので、作ってみて下さい。
[Qiita - Mesos/Marathon/DockerでプライベートIaaSを作る](http://qiita.com/ykshr/items/0fbbf36c1b6a8e867576)

## インストール
#### Goのインストール
NomsはGoベースなので、まずはGo（1.6+）をインストールします。
Goのインストーラを[https://golang.org/dl/](https://golang.org/dl/)からダウンロードします。

```shell-session
$ wget https://storage.googleapis.com/golang/go1.6.3.linux-amd64.tar.gz
$ tar xzvf go1.6.3.linux-amd64.tar.gz -C /usr/local
```

.bashrcを編集して、パスを通します。

```shell-session
$ vi ~/.bashrc
export GOROOT=/usr/local/go
export GOPATH=$HOME/work
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
```

#### Nomsのインストール
Gitからソースコードを落としてインストールします。

```shell-session
$ git clone https://github.com/attic-labs/noms $GOPATH/src/github.com/attic-labs/noms
$ go install github.com/attic-labs/noms/cmd/noms
```

Nomsがインストールできたかは、デモ用データベースのコミット履歴を表示することで確認します。
下記の通りコミット履歴が表示されればインストール成功です。

```shell-session
$ noms log http://demo.noms.io/cli-tour::sf-film-locations
commit m7iuf544vdtuemh2r80bspfs063bpvkf
Parent:    1a2aj8svslsu7g8hplsva6oq6iq3ib6c
Date:      "2016-07-31T07:35:01+0000"
InputPath: "http://localhost:8000/cli-tour::#c3feje9ftsvpgms1t0dj6ua8tsd59r1v.value"

commit 1a2aj8svslsu7g8hplsva6oq6iq3ib6c
Parent:    90fh1d2hadem3med2c7n2ois5o5ltmdc
Date:      "2016-06-20T00:00:00+0000"
InputPath: "http://demo.noms.io/cli-tour::#badri4p89a5gh2dgfq1712ecsck6pn4o.value"
[213] {
-   Locations: "Mission Delores Park (Mission District) via J-Church MUNI Train"
+   Locations: "Mission Dolores Park (Mission District) via J-Church MUNI Train"
  }
[221] {
-   FunFacts: "Mission Delores' official name is Mission San Francisco de Assis. It is the 

（以下、省略）
```

# さわってみる
さわってみないことにはどんなものか分かりません。
Gitに載っているGoのサンプルを順番に実行してみます。
[Git - A Short Tour of Noms for Go](https://github.com/attic-labs/noms/blob/master/doc/go-tour.md)

#### Nomsサーバーの起動（ローカルデータベースの作成）
noms serveでNomsサーバーが起動します。

```shell-session
$ noms serve ldb:/tmp/noms-go-tour
Listening on port 8000...
```

#### Databaseへの接続
別のターミナルを立ち上げて、GoのスクリプトからNomsサーバーにアクセスしてみます。
スクリプトファイル格納用のディレクトリを作成します。

```shell-session
$ mkdir noms-tour
$ cd noms-tour
```

接続できるか確認するためのスクリプトを作成します。

```shell-session
$ vi noms-go-tour1.go
package main

import (
  "fmt"
  "os"

  "github.com/attic-labs/noms/go/spec"
)

func main() {
  db, err := spec.GetDatabase("http://localhost:8000")
  if err != nil {
    fmt.Fprintf(os.Stderr, "Could not access database: %s\n", err)
    return
  }
  defer db.Close()
}
```

作成したスクリプトを実行してみます。
なにも出力されなければ接続成功です！

```shell-session
$ go run noms-go-tour1.go
```

#### Datasetの確認
Datasetが存在するか確認するためのスクリプトを作成します。

```shell-session
$ vi noms-go-tour2.go
package main

import (
  "fmt"
  "os"

  "github.com/attic-labs/noms/go/spec"
)

func main() {
  ds, err := spec.GetDataset("http://localhost:8000::people")
  if err != nil {
    fmt.Fprintf(os.Stderr, "Could not create dataset: %s\n", err)
    return
  }
  defer ds.Database().Close()

  if _, ok := ds.MaybeHeadValue(); !ok {
    fmt.Fprintf(os.Stdout, "head is empty\n")
  }
}
```

作成したスクリプトを実行してみます。
Datasetはまだ登録していないため、head is emptyが出力されます。

```shell-session
$ go run noms-go-tour2.go
head is empty
```

#### Datasetの新規登録
Datasetが存在しないことが確認できたので、新規データを登録するためのスクリプトを作成します。

```shell-session
$ vi noms-go-tour3.go
package main

import (
  "fmt"
  "os"

  "github.com/attic-labs/noms/go/spec"
  "github.com/attic-labs/noms/go/types"
)

func newPerson(givenName string, male bool) types.Struct {
  return types.NewStruct("Person", types.StructData{
    "given": types.String(givenName),
    "male":  types.Bool(male),
  })
}

func main() {
  ds, err := spec.GetDataset("http://localhost:8000::people")
  if err != nil {
    fmt.Fprintf(os.Stderr, "Could not create dataset: %s\n", err)
    return
  }
  defer ds.Database().Close()

  data := types.NewList(
    newPerson("Rickon", true),
    newPerson("Bran", true),
    newPerson("Arya", false),
    newPerson("Sansa", false),
  )

  fmt.Fprintf(os.Stdout, "data type: %v\n", data.Type().Describe())

  _, err = ds.CommitValue(data)
  if err != nil {
    fmt.Fprint(os.Stderr, "Error commiting: %s\n", err)
  }
}
```

作成したスクリプトを実行してみます。
ここでは、登録したDatasetのデータタイプが出力されます。

```shell-session
$ go run noms-go-tour3.go
data type: List<struct  {
  given: String
  male: Bool
}>
```

#### Datasetの読込み
先ほど登録したDatasetを読込むためのスクリプトを作成します。
スクリプトでは、下の流れでデータを表示しています。

1. Head Valueを取得
1. List型に変換
1. indexが0のデータをStruct（構造体）に変換
1. "given"を表示

```shell-session
$ vi noms-go-tour4.go
package main

import (
  "fmt"
  "os"

  "github.com/attic-labs/noms/go/spec"
  "github.com/attic-labs/noms/go/types"
)

func main() {
  ds, err := spec.GetDataset("http://localhost:8000::people")
  if err != nil {
    fmt.Fprintf(os.Stderr, "Could not create dataset: %s\n", err)
    return
  }
  defer ds.Database().Close()

  if headValue, ok := ds.MaybeHeadValue(); !ok {
    fmt.Fprintf(os.Stdout, "head is empty\n")
  } else {
    // type assertion to convert Head to List
    personList := headValue.(types.List)
    // type assertion to convert List Value to Struct
    personStruct := personList.Get(0).(types.Struct)
    // prints: Rickon
    fmt.Fprintf(os.Stdout, "given: %v\n", personStruct.Get("given"))
  }
}
```

作成したスクリプトを実行してみます。
index 0のデータが出力されるため、```given: Rickon```が出力されます。

```shell-session
$ go run noms-go-tour4.go
given: Rickon
```

#### Datasetの追加（データとデータタイプの追加）
Datasetをさらに追加します。
サンプルコードでは、追加するDatasetは、"given"、"male"に加えて"family"が定義されています。

```shell-session
$ vi noms-go-tour5.go
package main

import (
  "fmt"
  "os"

  "github.com/attic-labs/noms/go/spec"
  "github.com/attic-labs/noms/go/types"
)

func main() {
  ds, err := spec.GetDataset("http://localhost:8000::people")
  if err != nil {
    fmt.Fprintf(os.Stderr, "Could not create dataset: %s\n", err)
    return
  }
  defer ds.Database().Close()

  if headValue, ok := ds.MaybeHeadValue(); !ok {
    fmt.Fprintf(os.Stdout, "head is empty\n")
  } else {
    // type assertion to convert Head to List
    personList := headValue.(types.List)
    data := personList.Append(
      types.NewStruct("Person", types.StructData{
        "given":  types.String("Jon"),
        "family": types.String("Snow"),
        "male":   types.Bool(true),
      }),
    )

    fmt.Fprintf(os.Stdout, "data type: %v\n", data.Type().Describe())

    _, err = ds.CommitValue(data)
    if err != nil {
      fmt.Fprint(os.Stderr, "Error commiting: %s\n", err)
    }
  }
}
```

作成したスクリプトを実行してみます。
このスクリプトでもDatasetのデータタイプを出力するのですが、これまでのデータと追加したデータではデータタイプが異なるため、2つのデータタイプが出力されます。

```shell-session
$ go run noms-go-tour5.go
data type: List<struct Person {
  family: String,
  given: String,
  male: Bool,
} | struct Person {
  given: String,
  male: Bool,
}>
```

#### コミットログの表示
Nomsで最も大切な部分ですね。
これまでの操作によるコミットログを表示してみましょう。
コミットログはnomsコマンドで表示します。

```shell-session
$ noms log http://localhost:8000::people
commit b6953vj5tq7h0ckkv8gmfb86d8n089ab
Parent: hshltip9kss28uu910qadq04mhk9kuko
(root) {
+   Person {
+     family: "Snow",
+     given: "Jon",
+     male: true,
+   }
  }

commit hshltip9kss28uu910qadq04mhk9kuko
Parent: None
```

Gitのサンプルでは、3つのコミットがありますが、2つしか表示されませんでした。
ただ、今回は下の2回しかデータセットを更新していないため、2つで正しいのかなと思います。

* noms-go-tour3.go
* noms-go-tour5.go

# さわってみて
とりあえずデータベースを作って、データセットを登録し、コミットログを確認するところまでやってみました。印象としては、使い始めるまでのハードルも低く、おもしろいと思いました。開発も活発に行われているみたいなので、もうちょっと追ってみようかと思います。最後に感想です。

* 3rdパーティのDBとうまく連携できる仕組みがほしい  
なんだかんだ、DBの移行や、新たなDBを使い始めることは、ハードルが高いと思います。特にビジネスでは。最初は、システムとしてはPostgreSQLを使って、その裏でNomsがバージョンを管理してくれるみたいなことができればいいなと思いました。そんな使い方は本末転倒なのでしょうか…(´・ω・`)
* イケてるUIで操作したい  
Gitでもそうですが、分散管理・バージョン管理で頭を悩ませるのが、各コミットの状態がよく分からなくなってくることと、競合が発生してくることだと思います。Gitでは、SourceTreeなどのツールを用いることで、ある程度ビビることなくこの辺の問題に対処することができているのかなと思います。Nomsに求めることではないですが、そんなツールが出てくれれば適用範囲が広がるのかなと思いました。