コンテンツにスキップ

11.9 Go

neo-clientの概要

neo-clientはMachbase Neo用のGoクライアントモジュールです。v2から標準のdatabase/sql ドライバーを中心に再構成され、旧バージョン(v1)のネイティブmachgoパッケージは提供されなくなりました。 v1のコードはv2と互換性がないため、machgo.Configmdb.Connect()を使用する既存コードは、 以下を参照してdatabase/sqlベースに移行してください。

neo-clientは次のパッケージを提供します。

  • client(モジュールルート、インポートパスgithub.com/machbase/neo-client/v2): 標準database/sqlドライバー、Appender、構造体スキャン・名前付きパラメーターヘルパー
  • api: Machbase専用のデータ型とオプション定義
  • machnet: clientが内部で使用する低レベルのプロトコル・転送実装。アプリケーションコードで 直接インポートする必要はほとんどありません。

前提条件

  • Machbaseサーバー: ネイティブポート(デフォルト5656)にアクセス可能な稼働中のDBMSまたはNeoサーバー
  • Go 1.22以降
  • アカウント情報: 有効なMachbaseユーザーアカウント(ローカル開発環境ではsys / managerなど)

はじめに

インストール

go get github.com/machbase/neo-client/v2

インポート

ドライバーパッケージはブランク識別子でインポートします。ドライバー名machbaseで自動登録されるため、 別途sql.Register()を呼び出す必要はありません。

import (
    "context"
    "database/sql"
    "fmt"

    _ "github.com/machbase/neo-client/v2"
)

接続

DSN形式

neo-clientは次のDSN形式をサポートします。

  • サーバー値のみ: hostまたはhost:port
  • URL形式: tcp://user:password@host:port/database?as=proxy&fetch_rows=100
  • セミコロンで区切ったkey=valueリスト: key=value;key=value;... (例: user=sys;password=manager;server=127.0.0.1:5656

key=value形式は次の規則に従います。

  • 値は"..."または'...'で引用できます。
  • 引用された値内の;はリテラル文字として扱います。
  • 引用された値内ではバックスラッシュエスケープを使用できます。二重引用符の値では\"、 単一引用符の値では\'、および\\を使用できます。
  • 引用符が閉じていない、または対応しない場合は解析エラーになります。

例:

user="sys as demo";password="12;34";server=127.0.0.1:5656;
password="a\"b";server=127.0.0.1:5656;

サポートするDSNキーは次のとおりです。

キー説明
servertcp://user:password@127.0.0.1:5656形式のサーバーURL
host, portサーバーホストとポートを個別指定(デフォルトポート: 5656
user, uidログインユーザー
password, pwdログインパスワード
database, db初期データベース
auth_mode認証方式: passwordまたはchallenge
auth_key_file, auth_key_pemauth_mode=challengeの秘密鍵ファイルパスまたはインラインPEM
auth_sig_schemeチャレンジ認証の署名方式
fetch_rows, fetchrows1回のフェッチで取得する最大行数(デフォルト1000
statement_cache, statementcache文キャッシュモード: autoonoff(デフォルトauto
io_metrics, iometricsI/Oメトリクスの有効化: truefalse
alternative_servers127.0.0.2:5656,backup.example.com:5657のようなカンマ区切りの代替サーバー一覧

auth_key_fileまたはauth_key_pemを指定し、auth_modeを省略するとチャレンジ認証になります。 URLクエリパラメーターも同じオプション名を使用します。

tcp://sys:manager@127.0.0.1:5656/DATABASE_A?statement_cache=on&io_metrics=true

不明なキーはkey=valueのDSNではエラーですが、URLクエリ文字列では無視されます。 URLパスは初期データベースも指定します(tcp://sys:manager@127.0.0.1:5656/DATABASE_A)。 物理接続ごとに指定データベースが選択され、アプリケーションが直接USEを実行した場合は、 その接続がプールで再利用される前に指定データベースへ復元されます。

検索例

次の例は標準のdatabase/sqlパッケージでシステムテーブルM$SYS_TABLESを検索します。

package main

import (
	"context"
	"database/sql"
	"fmt"

	_ "github.com/machbase/neo-client/v2"
)

func main() {
	db, err := sql.Open("machbase", "server=tcp://sys:manager@127.0.0.1:5656")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	ctx := context.Background()
	rows, err := db.QueryContext(ctx, `SELECT NAME, ID, TYPE FROM M$SYS_TABLES ORDER BY NAME`)
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	for rows.Next() {
		var (
			name string
			id   int64
			typ  int
		)
		if err := rows.Scan(&name, &id, &typ); err != nil {
			panic(err)
		}
		fmt.Println(name, id, typ)
	}

	if err := rows.Err(); err != nil {
		panic(err)
	}
}

テーブルの作成と入力

次の例はdatabase/sqlでTagテーブルを作成し、ExecContextで行を入力します。

CREATE TAG TABLE IF NOT EXISTS example (
    name VARCHAR(100) PRIMARY KEY,
	time DATETIME BASE TIME,
    value DOUBLE
);
package main

import (
	"context"
	"database/sql"
	"fmt"
	"time"

	_ "github.com/machbase/neo-client/v2"
)

func main() {
	dsn := "server=tcp://sys:manager@127.0.0.1:5656"

	db, err := sql.Open("machbase", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()

	ctx := context.Background()

	_, err = db.ExecContext(ctx, `CREATE TAG TABLE IF NOT EXISTS EXAMPLE (
		NAME   VARCHAR(100)  PRIMARY KEY,
		TIME   DATETIME      BASE TIME,
		VALUE  DOUBLE
	)`)
	if err != nil {
		panic(err)
	}

	ts := time.Now()
	for i := 0; i < 10; i++ {
		rec := []any{
			"example-client",
			ts.Add(time.Duration(i) * time.Second),
			3.14 * float64(i),
		}
		result, err := db.ExecContext(ctx, `INSERT INTO EXAMPLE VALUES (?, ?, ?)`, rec...)
		if err != nil {
			panic(err)
		}
		affected, err := result.RowsAffected()
		if err != nil {
			panic(err)
		}
		fmt.Println("Rows affected:", affected)
	}
}

ROWID対応のStandard Editionで単一のINSERT ... VALUESが成功すると、Result.LastInsertId()で 入力行のROWIDを確認できます。戻り値の型はint64のため、ROWIDの64ビット値を保持するにはuint64に 変換します。バッチ、Appender、INSERT ... SELECT、UPSERTはROWIDを返しません。詳細な条件は ROWIDとINSERT結果IDを参照してください。

トランザクション

MachbaseはCREATE TABLEで作成した通常のテーブル(TRANSACTIONテーブル)で BEGIN/COMMIT/ROLLBACKをサポートします。TAG/LOGテーブルはトランザクションをサポートせず、 トランザクション内でTAG/LOGテーブルにDMLを実行するとMACHCLI-ERR-2362になります。

標準のdatabase/sqlトランザクションAPIをそのまま使用できます。

tx, err := db.BeginTx(ctx, nil)
if err != nil {
	panic(err)
}
if _, err := tx.ExecContext(ctx, `INSERT INTO EXAMPLE_TX VALUES (?, ?, ?)`, name, ts, value); err != nil {
	tx.Rollback()
	panic(err)
}
if err := tx.Commit(); err != nil {
	panic(err)
}

繰り返す準備コードを減らすにはclient.Tx/client.TxConnのクロージャーヘルパーを使用します。 関数がnilを返すとコミットし、エラーを返すとロールバックしてそのエラーをそのまま返します。 panicが発生するとロールバック後に再度panicします。

import client "github.com/machbase/neo-client/v2"

err := client.Tx(ctx, db, func(tx *sql.Tx) error {
	if _, err := tx.ExecContext(ctx, `INSERT INTO EXAMPLE_TX VALUES (?, ?, ?)`, name, ts, value); err != nil {
		return err // 自動ROLLBACK
	}
	return nil // 自動COMMIT
})

// TxConnはdb.Conn(ctx)で取得した特定の接続でトランザクションを実行します。
conn, _ := db.Conn(ctx)
defer conn.Close()
err = client.TxConn(ctx, conn, func(tx *sql.Tx) error {
	// ...
	return nil
})

クロージャーのエラーはそのまま返されるため、errors.Is/errors.Asを引き続き使用できます。 強制ロールバックにはセンチネルエラーを返す方法が一般的です。Machbaseはトランザクションオプション (分離レベル、読み取り専用)をサポートしないため、ドライバーが拒否します。

高性能な大量入力(Appender

大量の時系列入力には、行単位のINSERTの代わりにclient.Appenderを使用します。 Appenderはクライアントでレコードをバッファリングし、専用チャネルでサーバーにストリーミングするため、 個別のINSERT文より大幅に高速です。

import client "github.com/machbase/neo-client/v2"

appender := &client.Appender{}
// 一部の列のみ指定: 以下のAppend()はこの3値だけを送信し、残りの列はNULLになります。
if err := appender.Connect(ctx, dsn, "EXAMPLE", "NAME", "TIME", "VALUE"); err != nil {
	panic(err)
}
defer func() {
	successCount, failCount, err := appender.Close() // 残りのバッファをフラッシュ
	if err != nil {
		panic(err)
	}
	fmt.Println("Append finished. Success:", successCount, "Fail:", failCount)
}()

for _, rec := range records {
	// Connectに渡した列と同じ順序で値を1つずつ渡します。
	if err := appender.Append(rec.Name, rec.Time, rec.Value); err != nil {
		panic(err)
	}
}

主なポイント:

  • 列選択: Connect(またはWithInputColumns)に渡す列リストが、各Append呼び出しで 順番に提供する列を正確に決めます。リストにない列はNULLとして入力されます。
  • 列リストを省略すると(例: appender.Connect(ctx, dsn, "EXAMPLE"))、Appenderはテーブルの 全列を対象とし、各Appendnilを含めて全列の値を提供する必要があります。 そうしないと値の数のエラーになります。
  • Appendは行をバッファリングします。即時送信はFlush()を呼び出します。Close()はフラッシュとともに セッション単位の成功・失敗件数を返します。
  • バッファリングはWithBatchMaxRowsWithBatchMaxBytesWithBatchMaxDelayで調整できます。
    • WithBatchMaxRows(rows): デフォルト512、最小1
    • WithBatchMaxBytes(bytes): デフォルト512KB、最小4KB
    • WithBatchMaxDelay(duration): デフォルト5ms、最小1ms0は時間ベースの閾値を使用しません。
  • AppenderはTAG、LOG、TRANSACTIONテーブルで動作しますが、SQLを迂回するため、Appendはいずれの トランザクションにも含まれません。
appender := &client.Appender{}
if err := appender.Connect(ctx, dsn, "EXAMPLE", "NAME", "TIME", "VALUE"); err != nil {
	panic(err)
}
defer appender.Close()

appender.
	WithBatchMaxBytes(1024 * 1024).           // 1 MBの閾値
	WithBatchMaxRows(2000).                   // 行数の閾値
	WithBatchMaxDelay(500 * time.Millisecond) // 最大遅延の閾値
有効なAppenderを使用する接続で通常のクエリを併用しないでください。 Appendのワークロードには別接続を使用してください。

ARRAYと選択列Append

通常のappender.Connect(ctx, dsn, table)で列引数を省略し、ARRAY列の値にapi.NewSparseArray()で 作成したオブジェクトを渡せます。固定要素の選択とは異なります。

if err := appender.Connect(ctx, dsn, "ARRAY_APPEND_FULL_EXAMPLE"); err != nil {
    return err
}

ID LONG, A INT32[4]テーブルの入力順序、スパース値の構成、エラー時のClose、検索確認は 通常Connectの例を参照してください。

func appendSelected(ctx context.Context, dsn string) error {
    appender := &client.Appender{}
    if err := appender.Connect(
        ctx,
        dsn,
        "ARRAY_APPEND_EXAMPLE",
        "ID",
        "A[0]",
        "A[3]",
    ); err != nil {
        return err
    }
    if err := appender.Append(int64(1), int32(10), int32(40)); err != nil {
        _, _, _ = appender.Close()
        return err
    }
    success, failed, err := appender.Close()
    if err != nil {
        return err
    }
    if failed != 0 {
        return fmt.Errorf(
            "append result: success=%d failed=%d",
            success,
            failed,
        )
    }
    return nil
}

上記はcontextfmtclient "github.com/machbase/neo-client/v2"のインポートを前提とします。

行ごとに異なる位置を入力する場合はapi.NewSparseArray()を使用します。Array.Set()Get()Entries()、および要素位置を指定したAppend対象の位置は0始まりです。 APIとバージョン制限は Sparse ARRAYと選択列Append APIを参照してください。

結果を構造体にスキャン

列順に全対象を列挙する代わりに、dbタグで列を構造体フィールドにマッピングできます。 ヘルパーは取得済みの*sql.Rowsをそのまま受け取るため、標準database/sql APIと併用できます。

import client "github.com/machbase/neo-client/v2"

type TagRecord struct {
	Name  string    `db:"NAME"`
	Time  time.Time `db:"TIME"`
	Value float64   `db:"VALUE"`

	cached string // 非公開またはタグなしのフィールドは無視
}

records, err := client.Select[TagRecord](ctx, db,
	`SELECT NAME, TIME, VALUE FROM EXAMPLE WHERE NAME = ? ORDER BY TIME LIMIT 100`, "sensor-1")

提供するヘルパー:

関数用途
Select[T](ctx, q, query, args...)クエリを実行し、全行を[]Tにスキャン
Get[T](ctx, q, query, args...)クエリを実行し、最初の行をスキャン。結果がなければsql.ErrNoRows
ScanAll[T](rows) / ScanOne[T](rows)呼び出し側がすでに開いたrowsに同じ操作を実行
ScanEach[T](rows, fn)メモリ使用量を一定に保ちながら1行ずつストリーミング
NewCursor[T](rows)明示的なNext/Value/Errイテレーター
ScanStruct(rows, &dest)rows.Next()を呼ばずに現在の行をスキャン
ScanRow(rows, &dest) / ScanRows(rows, &slice)ジェネリクスを使用しない形式

Tは構造体、構造体ポインター、単一列クエリのスカラー、map[string]anyのいずれかです。

マッピング規則:

  • タグキーはdbで、既存DTOをそのまま使用できるようにjsonタグを代替として使用します。
  • 列名は大文字・小文字を区別せずに一致するため、db:"id"ID列と一致します。
  • db:"-"はフィールドを除外し、タグなしフィールドも除外されます。タグなしフィールドを名前で マッピングするにはWithNameMapper(client.NameMapperIdentity())を呼び出してください。
  • 埋め込み構造体は平坦化され、名前のあるネスト構造体はparent.childで指定します。
  • NULL列はnilになる*Tフィールド、またはsql.Null[T]で受け取れます。

デフォルトのマッピングは厳密です。一致するフィールドがない列と、一致する列がないフィールドは どちらもエラーとなり、変更されたSELECT *が値を黙って欠落させることを防ぎます。 呼び出しごとにWithLaxColumns()またはWithLaxFields()で緩和できます。

DATETIME列をstringint64time.Timeフィールドにスキャンする際は、machbase-neo HTTP APIの timeformat/tzクエリパラメーターと名前を合わせた追加のdbタグオプションを使用できます。

type Row struct {
	Time  string    `db:"TIME,timeformat=2006-01-02 15:04:05,tz=Local"` // カスタムレイアウト + 表示タイムゾーン
	Epoch int64     `db:"TIME,timeformat=ms"`                           // ミリ秒単位のエポック
	At    time.Time `db:"TIME,tz=UTC"`                                  // フィールド別にタイムゾーンを上書き
}
  • timeformat=<Go time layout>: string/*stringフィールドのGo時刻レイアウト (またはエポックを数値文字列で表すns/us/ms/s
  • timeformat=ns|us|ms|s: int64/*int64フィールドのエポック単位
  • tz=<IANA name>|Local|UTC: string/time.Timeフィールド(およびポインター形式)のタイムゾーン

このオプションはタグがなくても適用されます。DATETIME列に一致するstringint64time.Timeの フィールドは、デフォルトとしてWithDateTime(timeformat, tz)を使用します。WithDateTimeも未設定なら timeformat="2006-01-02 15:04:05.999"tz="Local"を使用します。 フィールド自体のタグは常にWithDateTimeより優先されます。

SelectScanAllScanRowsは結果全体をメモリに読み込むため、WithMaxRows(デフォルト1000)を 超えるとErrScanTooManyRowsで中断します。WithMaxRows(n)で上限を増やすか、WithMaxRows(0)で 制限を解除できます。制限のないScanEachNewCursorでストリーミングすることもできます。

rows, err := db.QueryContext(ctx, `SELECT NAME, TIME, VALUE FROM EXAMPLE`)
if err != nil {
	panic(err)
}
defer rows.Close() // ヘルパーは受け取ったrowsを閉じない

var total float64
err = client.ScanEach(rows, func(rec TagRecord) error {
	total += rec.Value
	return nil
})

名前付きパラメーター

NamedArgsは構造体またはmap[string]anyを、同じdbタグでsql.Named引数に変換します。 SQLテキストを検査・書き換えず、:nameプレースホルダーはサーバーが直接解釈します。

type condition struct {
	Name string    `db:"name"`
	From time.Time `db:"from"`
	To   time.Time `db:"to"`
}

args, err := client.NamedArgs(condition{Name: "sensor-1", From: begin, To: end})
if err != nil {
	panic(err)
}

records, err := client.Select[TagRecord](ctx, db, `
	SELECT NAME, TIME, VALUE FROM EXAMPLE
	 WHERE NAME = :name AND TIME BETWEEN :from AND :to`, args...)

名前付きパラメーターには、パラメーター名メタデータを報告するサーバー(Machbase v8.7.0以降)が 必要です。client.SupportsNamedParameters(ctx, db)で確認します。非サポートの場合、クエリは client.ErrNamedParamsUnsupportedで失敗するため、位置指定プレースホルダー?を使用する必要があります。 一般的なSQL機能とSDK別の違いは Named Bind Parameter syntaxを参照してください。

Machbase 8.7: DECIMALと名前付きパラメーター

Machbase 8.7は正確なDECIMAL値、NULL許容列情報、名前付きパラメーターを提供します。

import "database/sql"
import client "github.com/machbase/neo-client/v2"

amount, err := client.ParseDecimal("1234567890.125", 30, 3)
if err != nil {
	panic(err)
}
result, err := conn.ExecContext(ctx,
	"INSERT INTO payments(id, amount) VALUES (:id, :amount)",
	sql.Named("id", int32(1)),
	sql.Named("amount", amount),
)
if err != nil {
	panic(err)
}

database/sqlドライバーはsql.Namedを受け取り、DECIMALの検索値を正確な文字列で返します。 パラメーター名は大文字・小文字を区別せずに一致し、繰り返すプレースホルダーには1回渡した値を適用します。 名前付き引数と位置指定引数は混用できません。client.NamedArgsは構造体またはmapから sql.Namedリストを作成します。

Machbase 8.5.xに接続する場合は、そのサーバーバージョンがサポートするテーブル・データ型と、 位置指定パラメーター?を使用してください。名前付きパラメーターとMachbase 8.7のデータ型は使用できず、 NULL許容列情報が不明な場合(ColumnType.Nullable()ok=falseを返す場合)があります。

プリペアドステートメントと文キャッシュ

db.PrepareContextで作成した文は複数回実行できます。ドライバーの文キャッシュは接続単位で動作し、 DSNキーstatement_cache=auto|on|offで設定します。テーブルを削除して再作成した場合や結果列の型が 変わった場合は、キャッシュしたメタデータを更新するため文を再準備します。USEでセッションの データベースを変えた後も、既存の準備済み文やカーソルを別データベースの操作に再利用せず、 新しく準備またはオープンする必要があります。

付属サンプルの実行

実行可能なサンプルはneo-clientリポジトリの_example/に含まれています。

go run ./_example/query.go -s 127.0.0.1:5656 -u sys -p manager
go run ./_example/append.go -s 127.0.0.1:5656 -u sys -p manager
go run ./_example/insert.go -s 127.0.0.1:5656 -u sys -p manager
go run ./_example/scanbytag.go -s 127.0.0.1:5656 -u sys -p manager

補足と制限事項

  • 位置指定と名前付きプレースホルダーの両方を使用できますが、1つの文で混用できません。 名前付きAPIはsql.Named()を使用します。共通SQL機能とSDK別の違いは Named Bind Parameter syntaxを参照してください。
  • database/sqlの接続プールは通常のsql.DBの方式で動作します。DSNにdatabase/dbを指定すると、 物理接続ごとに指定データベースを選択します。アプリケーションが直接USEを実行したセッションは、 プールへの返却前に設定済みデータベースへ復元されます。
  • ROWID対応のStandard Editionでは、単一INSERT結果でResult.LastInsertId()を呼び出せます。 返されたint64uint64に変換してROWIDのビットパターンを保持します。詳細は ROWIDとINSERT結果IDを参照してください。
  • 使用後のRowsStmtsql.Connsql.DBは必ず閉じてください。 構造体スキャンヘルパーは受け取ったrowsを閉じません。
  • Appender.Close()はAppendセッションの成功・失敗件数を返します。
  • パラメーター型はドライバー実装に従います。一般的なSQL型、time.Time[]bytenet.IPapi.Decimalをサポートしますが、boolパラメーターはサポートしません。
最終更新日