🐶

Go で Cassandra/ScyllaDB を使うために gocqlx を使う

2024/10/17に公開

Go で ScyllaDB を使いたくなったので https://github.com/scylladb/gocqlx を使ってみました。
ScyllaDB は Cassandra と同じ CQL の世界なので Cassandra のライブラリをそのまま使うこともできるし、DynamoDB 互換 API も用意されているのでそちらから攻めることもできるのですが、公式で用意されているのでそれを使うのが良いんじゃないかと思いました。
All-In-One: CQL query builder, ORM and migration tool と書いてあるので機能も期待できそうです。名前からするに Cassandra でも使えると思います。

接続

接続自体は https://github.com/gocql/gocql を使います。作った接続を gocqlx でラップする感じです。

	cluster := gocql.NewCluster("127.0.0.1:9042")
	cluster.Keyspace = keyspace

	session, err := gocqlx.WrapSession(cluster.CreateSession())
	if err != nil {
		panic(fmt.Sprintf("CreateSession: %s", err))
	}
	defer session.Close()

	...

マイグレーション

マイグレーションの仕組みは用意されているものの、コマンドは用意されていないので作る必要があります。 https://github.com/golang-migrate/migrate でもある程度は対応されていますが、こちらだと1ファイルに複数行の DDL を書いたらエラーになったのと(詳しくは調査していません)、細かいところの対応がよくわからなくて使ってるうちに詰むと嫌なので、 gocqlx のものを使おうと思います。

マイグレーション実行スクリプトはざっくりこんな感じになります。

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/gocql/gocql"
	"github.com/scylladb/gocqlx/v3"
	"github.com/scylladb/gocqlx/v3/migrate"
)

func main() {
	const keyspace = "example"

	cluster := gocql.NewCluster("127.0.0.1:9042")
	cluster.Keyspace = keyspace

	session, err := gocqlx.WrapSession(cluster.CreateSession())
	if err != nil {
		panic(fmt.Sprintf("CreateSession: %s", err))
	}
	defer session.Close()

	dir := os.DirFS("cmd/migrate/cql")
	if err := migrate.FromFS(context.Background(), session, dir); err != nil {
		panic(fmt.Sprintf("Migrate: %s", err))
	}
}

マイグレーション結果は gocqlx_migrate テーブルに入ります。

cqlsh:example> select * from gocqlx_migrate;

 name     | checksum                         | done | end_time                        | start_time
----------+----------------------------------+------+---------------------------------+---------------------------------
 0002.cql | d8d252af421e1ca08eab181bfe712805 |    1 | 2024-10-16 11:17:13.870000+0000 | 2024-10-16 11:17:13.864000+0000
 0001.cql | 338aff7508207cab2cb093231793071c |    2 | 2024-10-16 11:17:13.864000+0000 | 2024-10-16 11:17:13.845000+0000

(2 rows)

ちなみにkeyspaceを作り直す場合はこんな感じで書くことができます。


func RecreateKeyspace(cluster *gocql.ClusterConfig, keyspace string) {
	session, err := cluster.CreateSession()
	if err != nil {
		panic(err)
	}
	defer session.Close()

	err = session.Query(fmt.Sprintf(`DROP KEYSPACE IF EXISTS %s`, keyspace)).Exec()
	if err != nil {
		panic(err)
	}
	err = session.Query(
		fmt.Sprintf(`CREATE KEYSPACE %s
WITH replication = {
	'class' : 'SimpleStrategy',
	'replication_factor' : %d
}`, keyspace, 1),
	).Exec()
	if err != nil {
		panic(err)
	}
}

マイグレーションはいろいろフックとかもあるのでお好みに合わせて調整してください。

スキーマ生成

現在は互換性の関係(https://github.com/scylladb/gocqlx/issues/287)で go install では入らないようです。以下の方法でインストールしましょう。

git clone git@github.com:scylladb/gocqlx.git
cd gocqlx/cmd/schemagen/
go install .

以下で実行します。 models/models.go が生成されます。

schemagen -cluster="127.0.0.1:9042" -keyspace="example" -output="models" -pkgname="models" -ignore-names="gocqlx_migrate"

Cassandra/ScyllaDB は JOIN もない(ないよね?)ので、単純な単一テーブルの定義さえ生成できれば困ることはなさそうです。

使ってみる

以下のようなテーブル定義に対して操作をしてみます。

CREATE TABLE item (
    id uuid,
    name text,
    tags set<text>,
    published_at timestamp,
    PRIMARY KEY(id)
);

以下のような感じで操作することができます。

package main

import (
	"fmt"
	"time"

	"github.com/gocql/gocql"
	"github.com/gofrs/uuid/v5"
	"〜/models"
	"github.com/scylladb/gocqlx/v3"
)

func main() {
	const keyspace = "example"

	cluster := gocql.NewCluster("127.0.0.1:9042")
	cluster.Keyspace = keyspace

	session, err := gocqlx.WrapSession(cluster.CreateSession())
	if err != nil {
		panic(err)
	}
	defer session.Close()

	itemID, err := uuid.NewV4()
	if err != nil {
		panic(err)
	}
	item := models.ItemStruct{
		Id:          [16]byte(itemID.Bytes()),
		Name:        "hoge",
		Tags:        []string{"foo", "bar"},
		PublishedAt: time.Now().UTC(),
	}
	q := session.Query(models.Item.Insert()).BindStruct(item)
	if err := q.ExecRelease(); err != nil {
		panic(err)
	}

	newItem := models.ItemStruct{
		Id: [16]byte(itemID.Bytes()),
	}
	q = session.Query(models.Item.Get()).BindStruct(newItem)
	if err := q.GetRelease(&newItem); err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", newItem)
}

細かいところは試していませんが、直接クエリも書けるようなので恐らく困ることはなさそうです。

sqlx インスパイアとのことですが、クエリビルダもあります。

Discussion