本シリーズのリンク
- チーム開発参加の記録【2023-10~2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み(SQLite使用)
- チーム開発参加の記録【2023-10~2024-03】(2) Go言語からTursoを使ってみた
- チーム開発参加の記録【2023-10~2024-03】(3) slogを使ってリクエスト・レスポンス情報をログ出力してみた
- チーム開発参加の記録【2023-10~2024-03】(4) Go言語用ORM「Bun」をDBファーストで使う試み(PostgreSQL使用)
- チーム開発参加の記録【2023-10~2024-03】(5) Go言語用ORM「Bun」でトランザクション、UPSERT、JOINを使ってみた
- チーム開発参加の記録【2023-10~2024-03】(6) Go言語用ORM「Bun」で複数のクエリーをまとめてDBサーバーで実行
- チーム開発参加の記録【2023-10~2024-03】(7) slogでErrorレベルのログをSlackに飛ばしてみた
本記事で行うこと
前回の記事ではSQLiteでORM「Bun」を使ってみました。
本記事では、前回作ったGoプログラムをTursoで動かしてみます。
Tursoチュートリアルに沿ってDBを用意する
チュートリアルを見ながら、CLIをインストールするところから始めて、テーブル作成&テストデータINSERTまで行います。
CLIインストール
私はLinux環境ですので、以下のコマンドを実行しました。
curl -sSfL https://get.tur.so/install.sh | bash
バージョン確認
$ turso --version
turso version v0.87.6
サインアップ
以下を実行後、ブラウザでサインアップします。
turso auth signup
論理データベース作成
以下を実行してDBを作成しました。
DB名は何でも良いですが、本記事ではチュートリアルのまま「my-db」にしました。
turso db create my-db
DB作成後、以下を実行してDBの情報を取得します。
turso db show my-db
表示された情報:
この情報のうち、URLはGo言語のプログラムに埋め込んで使います。
DBのコマンドラインツールでテーブルを作成する
以下を実行してDBのコマンドラインツールを立ち上げます。
turso db shell my-db
前回の記事と同じテーブルを作成します。
CREATE TABLE foo (
col_text TEXT NOT NULL,
col_text_null TEXT,
col_int INTEGER NOT NULL,
col_int_null INTEGER,
col_real REAL NOT NULL,
col_real_null REAL,
col_timestamp REAL NOT NULL,
col_timestamp_null REAL,
col_json TEXT NOT NULL,
col_json_null TEXT,
col_array TEXT NOT NULL,
col_array_null TEXT
) STRICT;
スクリーンショット:
引き続き、前回の記事と同じデータをINSERTします。
INSERT INTO foo
(
col_text,
col_text_null,
col_int,
col_int_null,
col_real,
col_real_null,
col_timestamp,
col_timestamp_null,
col_json,
col_json_null,
col_array,
col_array_null
)
VALUES
(
'text_data',
NULL,
1234567890123456789,
NULL,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
NULL,
JULIANDAY('now'),
NULL,
'{"a":100,"b":"bbb"}',
NULL,
'[10,null]',
NULL
),
(
'text_data',
'text_data',
1234567890123456789,
1234567890123456789,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
JULIANDAY('now'),
JULIANDAY('now'),
'{"a":200,"b":null}',
'{"a":300,"z":"zzz"}',
'[30,null,50]',
'[]'
);
INSERTしたら、fooテーブルにSELECT文を発行してみます。
SELECT
col_text,
col_text_null,
col_int,
col_int_null,
col_real,
col_real_null,
STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp,
STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null,
col_json,
col_json_null,
col_array,
col_array_null
FROM
foo;
スクリーンショット:
結果が見づらいですが、データがINSERTされたことが確認できました。
DBのコマンドラインツールから抜けるには、Ctrl+dを押すか、以下のようにコマンドを入力します。
Go言語からTursoのDBを操作する
それではGo言語に行きます。
前回の記事とほぼ同じプログラムですが、Turso向けに変更を加えてあります。
app.go
package main
import (
"context"
"database/sql"
"net/http"
"os"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
_ "github.com/tursodatabase/libsql-client-go/libsql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/extra/bundebug"
)
type (
httpError struct {
Error string `json:"error"`
}
httpMessage struct {
Message string `json:"message"`
}
)
var (
db *bun.DB
)
func selectFoo(c echo.Context) error {
type (
jsonType struct {
A *int `json:"a"`
B *string `json:"b"`
}
resultSetType struct {
ColText string `json:"colText"`
ColTextNull *string `json:"colTextNull"`
ColInt int64 `json:"colInt"`
ColIntNull *int64 `json:"colIntNull"`
ColReal float64 `json:"colReal"`
ColRealNull *float64 `json:"colRealNull"`
ColTimestamp time.Time `json:"colTimestamp"`
ColTimestampNull bun.NullTime `json:"colTimestampNull"`
ColJson jsonType `json:"colJson"`
ColJsonNull *jsonType `json:"colJsonNull"`
ColArray []*int64 `json:"colArray"`
ColArrayNull *[]*int64 `json:"colArrayNull"`
}
)
ctx := context.TODO()
resultSetFoo := make([]resultSetType, 0)
if err := db.NewSelect().
Column("col_text").
Column("col_text_null").
Column("col_int").
Column("col_int_null").
Column("col_real").
Column("col_real_null").
ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp").
ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null").
Column("col_json").
Column("col_json_null").
Column("col_array").
Column("col_array_null").
Table("foo").
Scan(ctx, &resultSetFoo); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, resultSetFoo)
}
func updateFoo(c echo.Context) error {
ctx := context.TODO()
if _, err := db.NewUpdate().
Table("foo").
SetColumn("col_text_null", "?", "ABC").
Where("col_text_null IS NULL").
Exec(ctx); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}
func insertFoo(c echo.Context) error {
type dummyModel struct{}
ctx := context.TODO()
if _, err := db.NewInsert().
Model((*dummyModel)(nil)).
ModelTableExpr("foo").
Value("col_text", "?", "XYZ").
Value("col_text_null", "?", nil).
Value("col_int", "?", 123).
Value("col_int_null", "?", nil).
Value("col_real", "?", 3.14).
Value("col_real_null", "?", nil).
Value("col_timestamp", "JULIANDAY('now')").
Value("col_timestamp_null", "?", nil).
Value("col_json", "?", "{}").
Value("col_json_null", "?", nil).
Value("col_array", "?", "[]").
Value("col_array_null", "?", nil).
Exec(ctx); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}
func deleteFoo(c echo.Context) error {
ctx := context.TODO()
if _, err := db.NewDelete().
Table("foo").
Where("col_text = ?", "XYZ").
Exec(ctx); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}
func main() {
tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN")
dbUrl := "libsql://my-db-kanedaq.turso.io?authToken=" + tursoAuthToken
sqlite, err := sql.Open("libsql", dbUrl)
if err != nil {
panic(err)
}
sqlite.SetMaxOpenConns(1)
db = bun.NewDB(sqlite, sqlitedialect.New())
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"),
))
// EchoでAPIサーバー
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routing
api := e.Group("/api")
api.GET("/selectFoo", selectFoo)
api.PUT("/updateFoo", updateFoo)
api.POST("/insertFoo", insertFoo)
api.DELETE("/deleteFoo", deleteFoo)
e.Logger.Fatal(e.Start(":1323"))
}
前回の記事のapp.goとの差分は以下の通りです。
Tursoは、SQLiteをフォークしたlibSQLを使っていますので、そのGo SDKを使うように変更しました。
@@ -4,13 +4,14 @@
"context"
"database/sql"
"net/http"
+ "os"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
+ _ "github.com/tursodatabase/libsql-client-go/libsql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
- "github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bun/extra/bundebug"
)
@@ -134,7 +135,9 @@
}
func main() {
- sqlite, err := sql.Open(sqliteshim.ShimName, "/home/user/DataGripProjects/sqlite/exercise.sqlite3")
+ tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN")
+ dbUrl := "libsql://my-db-kanedaq.turso.io?authToken=" + tursoAuthToken
+ sqlite, err := sql.Open("libsql", dbUrl)
if err != nil {
panic(err)
}
go.mod
module exercise_turso
go 1.21
require (
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/labstack/echo/v4 v4.11.3 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/bun v1.1.16 // indirect
github.com/uptrace/bun/dialect/sqlitedialect v1.1.16 // indirect
github.com/uptrace/bun/extra/bundebug v1.1.16 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)
実行準備
上記プログラムからTursoのDBに接続するために、認証トークンが必要です。
以下を実行して認証トークンを作成します。
turso db tokens create my-db
app.go用に、このトークンを環境変数TURSO_AUTH_TOKENに設定すれば、実行準備完了です。
実行
4つのAPIを動かしたところ、前回の記事と同じ結果になりました。
以下、selectFoo APIの結果のみ載せておきます。
以上、Tursoで前回の記事と同じ処理ができることが確認できました。
libSQL serverをローカルで動かす
Tursoに接続しなくても、libSQL serverをローカルで動かすこともできます。
開発中はこちらを使うと良いでしょう。
libSQL serverのインストールと起動
このページから「sqld-installer.sh」をダウンロードして起動し、指示に従ってバイナリをインストール後、sqldコマンドでlibSQL serverを起動しました。
ホームディレクトリでsqldを起動したら、~/data.sqld/dbs/default というディレクトリが作成され、そこに以下のファイルが作られていました。
このうち、dataというファイルがデータベースファイルでした。
以下のように、sqlite3コマンドでこのdataファイルに接続して、DBを操作できました。
sqlite3 ~/data.sqld/dbs/default/data
Go言語からローカルのlibSQL DBを操作する
それではGo言語に行きます。
Turso向けとの違いは、
- URLがローカルホスト
- 認証トークンが不要
package main
import (
"context"
"database/sql"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
_ "github.com/tursodatabase/libsql-client-go/libsql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/extra/bundebug"
)
type (
httpError struct {
Error string `json:"error"`
}
httpMessage struct {
Message string `json:"message"`
}
)
var (
db *bun.DB
)
func selectFoo(c echo.Context) error {
type (
jsonType struct {
A *int `json:"a"`
B *string `json:"b"`
}
resultSetType struct {
ColText string `json:"colText"`
ColTextNull *string `json:"colTextNull"`
ColInt int64 `json:"colInt"`
ColIntNull *int64 `json:"colIntNull"`
ColReal float64 `json:"colReal"`
ColRealNull *float64 `json:"colRealNull"`
ColTimestamp time.Time `json:"colTimestamp"`
ColTimestampNull bun.NullTime `json:"colTimestampNull"`
ColJson jsonType `json:"colJson"`
ColJsonNull *jsonType `json:"colJsonNull"`
ColArray []*int64 `json:"colArray"`
ColArrayNull *[]*int64 `json:"colArrayNull"`
}
)
ctx := context.TODO()
resultSetFoo := make([]resultSetType, 0)
if err := db.NewSelect().
Column("col_text").
Column("col_text_null").
Column("col_int").
Column("col_int_null").
Column("col_real").
Column("col_real_null").
ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp").
ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null").
Column("col_json").
Column("col_json_null").
Column("col_array").
Column("col_array_null").
Table("foo").
Scan(ctx, &resultSetFoo); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, resultSetFoo)
}
func updateFoo(c echo.Context) error {
ctx := context.TODO()
if _, err := db.NewUpdate().
Table("foo").
SetColumn("col_text_null", "?", "ABC").
Where("col_text_null IS NULL").
Exec(ctx); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}
func insertFoo(c echo.Context) error {
type dummyModel struct{}
ctx := context.TODO()
if _, err := db.NewInsert().
Model((*dummyModel)(nil)).
ModelTableExpr("foo").
Value("col_text", "?", "XYZ").
Value("col_text_null", "?", nil).
Value("col_int", "?", 123).
Value("col_int_null", "?", nil).
Value("col_real", "?", 3.14).
Value("col_real_null", "?", nil).
Value("col_timestamp", "JULIANDAY('now')").
Value("col_timestamp_null", "?", nil).
Value("col_json", "?", "{}").
Value("col_json_null", "?", nil).
Value("col_array", "?", "[]").
Value("col_array_null", "?", nil).
Exec(ctx); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}
func deleteFoo(c echo.Context) error {
ctx := context.TODO()
if _, err := db.NewDelete().
Table("foo").
Where("col_text = ?", "XYZ").
Exec(ctx); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}
func main() {
dbUrl := "http://127.0.0.1:8080"
sqlite, err := sql.Open("libsql", dbUrl)
if err != nil {
panic(err)
}
sqlite.SetMaxOpenConns(1)
db = bun.NewDB(sqlite, sqlitedialect.New())
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"),
))
// EchoでAPIサーバー
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routing
api := e.Group("/api")
api.GET("/selectFoo", selectFoo)
api.PUT("/updateFoo", updateFoo)
api.POST("/insertFoo", insertFoo)
api.DELETE("/deleteFoo", deleteFoo)
e.Logger.Fatal(e.Start(":1323"))
}
Turso版app.goとの差分は以下の通りです。
@@ -4,7 +4,6 @@
"context"
"database/sql"
"net/http"
- "os"
"time"
"github.com/labstack/echo/v4"
@@ -135,8 +134,7 @@
}
func main() {
- tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN")
- dbUrl := "libsql://my-db-kanedaq.turso.io?authToken=" + tursoAuthToken
+ dbUrl := "http://127.0.0.1:8080"
sqlite, err := sql.Open("libsql", dbUrl)
if err != nil {
panic(err)
動作確認したところ、Turso版と同じ結果が得られました。
まとめ
初めてTursoを使いましたが、SQLite向けのプログラムを少し変更するだけで動いてくれました。