開始在 Go 資料庫/SQL 中使用 Spanner


目標

本教學課程將逐步引導您使用 Spanner 資料庫/SQL 驅動程式完成下列步驟:

  • 建立 Spanner 執行個體和資料庫。
  • 對資料庫中的資料進行寫入和讀取,以及執行 SQL 查詢。
  • 更新資料庫結構定義。
  • 使用讀取/寫入交易來更新資料。
  • 將次要索引新增至資料庫。
  • 使用索引對資料執行讀取作業和 SQL 查詢。
  • 使用唯讀交易擷取資料。

費用

本教學課程使用 Spanner,這是Google Cloud的計費元件。如要瞭解 Spanner 的使用費用,請參閱「定價」一文。

事前準備

完成「設定」一文中說明的步驟,包含建立與設定預設的 Google Cloud 專案、啟用計費功能、啟用 Cloud Spanner API 和設定 OAuth 2.0 以取得使用 Cloud Spanner API 的驗證憑證。

特別提醒您,請務必執行 gcloud auth application-default login 來設定本機開發環境的驗證憑證。

準備本機資料庫/SQL 環境

  1. 如果開發電腦尚未安裝 Go,請下載並安裝。

  2. 將範例存放區複製到本機電腦中:

    git clone https://github.com/googleapis/go-sql-spanner.git
    
  3. 變更為包含 Spanner 範例程式碼的目錄:

    cd go-sql-spanner/snippets
    

建立執行個體

首次使用 Spanner 時,您必須建立執行個體,這是 Spanner 資料庫會使用的資源分配單位。建立執行個體時,請選擇「執行個體設定」以決定資料儲存的位置,再選擇要使用的節點數量以決定執行個體的服務和儲存空間資源量。

執行下列指令,在 us-central1 地區使用 1 個節點建立 Spanner 執行個體:

gcloud spanner instances create test-instance --config=regional-us-central1 \
    --description="Test Instance" --nodes=1

請注意,如此將建立具備下列特性的執行個體:

  • 執行個體 ID test-instance
  • 顯示名稱 Test Instance
  • 執行個體設定 regional-us-central1 (地區設定會將資料儲存在一個地區,而多地區設定則會讓資料散佈在多個地區。詳情請參閱「關於執行個體」一文。
  • 節點數量 1 (node_count 與執行個體中的資料庫可用的服務和儲存空間資源數量相對應。詳情請參閱「節點和處理單元」一節)。

畫面上會顯示下列訊息:

Creating instance...done.

瀏覽範例檔案

範例存放區中有一項範例,說明如何搭配資料庫/SQL 使用 Spanner。

請查看 getting_started_guide.go 檔案,瞭解如何使用 Spanner。該檔案中的程式碼會顯示如何建立及使用新資料庫。這份資料會使用結構定義與資料模型頁面中顯示的範例結構定義。

建立資料庫

gcloud spanner databases create example-db --instance=test-instance

畫面上會顯示下列訊息:

Creating database...done.

製作表格

以下程式碼會在資料庫中建立兩個資料表。

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

	_ "github.com/googleapis/go-sql-spanner"
)

func CreateTables(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Create two tables in one batch on Spanner.
	conn, err := db.Conn(ctx)
	defer conn.Close()

	// Start a DDL batch on the connection.
	// This instructs the connection to buffer all DDL statements until the
	// command `run batch` is executed.
	if _, err := conn.ExecContext(ctx, "start batch ddl"); err != nil {
		return err
	}
	if _, err := conn.ExecContext(ctx,
		`CREATE TABLE Singers (
				SingerId   INT64 NOT NULL,
				FirstName  STRING(1024),
				LastName   STRING(1024),
				SingerInfo BYTES(MAX)
			) PRIMARY KEY (SingerId)`); err != nil {
		return err
	}
	if _, err := conn.ExecContext(ctx,
		`CREATE TABLE Albums (
				SingerId     INT64 NOT NULL,
				AlbumId      INT64 NOT NULL,
				AlbumTitle   STRING(MAX)
			) PRIMARY KEY (SingerId, AlbumId),
			INTERLEAVE IN PARENT Singers ON DELETE CASCADE`); err != nil {
		return err
	}
	// `run batch` sends the DDL statements to Spanner and blocks until
	// all statements have finished executing.
	if _, err := conn.ExecContext(ctx, "run batch"); err != nil {
		return err
	}

	fmt.Fprintf(w, "Created Singers & Albums tables in database: [%s]\n", databaseName)
	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go createtables projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

下一個步驟是將資料寫入資料庫。

建立連線

您必須先建立 sql.DB,才能執行讀取或寫入作業。sql.DB 包含可用於與 Spanner 互動的連線集區。資料庫名稱和其他連線屬性會在資料庫/SQL 資料來源名稱中指定。

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

	_ "github.com/googleapis/go-sql-spanner"
)

func CreateConnection(ctx context.Context, w io.Writer, databaseName string) error {
	// The dataSourceName should start with a fully qualified Spanner database name
	// in the format `projects/my-project/instances/my-instance/databases/my-database`.
	// Additional properties can be added after the database name by
	// adding one or more `;name=value` pairs.

	dsn := fmt.Sprintf("%s;numChannels=8", databaseName)
	db, err := sql.Open("spanner", dsn)
	if err != nil {
		return err
	}
	defer db.Close()

	row := db.QueryRowContext(ctx, "select 'Hello world!' as hello")
	var msg string
	if err := row.Scan(&msg); err != nil {
		return err
	}
	fmt.Fprintf(w, "Greeting from Spanner: %s\n", msg)
	return nil
}

使用 DML 寫入資料

您可以使用資料操縱語言 (DML) 在讀寫交易中插入資料。

您可以使用 ExecContext 函式執行 DML 陳述式。

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

	_ "github.com/googleapis/go-sql-spanner"
)

func WriteDataWithDml(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Add 4 rows in one statement.
	// The database/sql driver supports positional query parameters.
	res, err := db.ExecContext(ctx,
		"INSERT INTO Singers (SingerId, FirstName, LastName) "+
			"VALUES (?, ?, ?), (?, ?, ?), "+
			"       (?, ?, ?), (?, ?, ?)",
		12, "Melissa", "Garcia",
		13, "Russel", "Morales",
		14, "Jacqueline", "Long",
		15, "Dylan", "Shaw")
	if err != nil {
		return err
	}
	c, err := res.RowsAffected()
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%v records inserted\n", c)

	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go dmlwrite projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

4 records inserted.

使用變異寫入資料

您也可以使用變異來插入資料。

Mutation 是變異作業的容器。Mutation 代表一系列的插入、更新和刪除作業,Spanner 會以整體化的方式,將這些作業套用到 Spanner 資料庫中不同的資料列和資料表。

使用 Mutation.InsertOrUpdate() 建構 INSERT_OR_UPDATE 變異,這個方法會新增資料列,或更新已存在的資料列的資料欄值。或者,您也可以使用 Mutation.Insert() 方法建構 INSERT 變異,以便新增資料列。

使用 conn.Raw 函式取得底層 Spanner 連線的參照。SpannerConn.Apply 函式會以不可分割的形式將變異套用到資料庫。

以下程式碼顯示如何使用變異寫入資料:

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

	"cloud.google.com/go/spanner"
	spannerdriver "github.com/googleapis/go-sql-spanner"
)

func WriteDataWithMutations(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Get a connection so that we can get access to the Spanner specific
	// connection interface SpannerConn.
	conn, err := db.Conn(ctx)
	if err != nil {
		return err
	}
	defer conn.Close()

	singerColumns := []string{"SingerId", "FirstName", "LastName"}
	albumColumns := []string{"SingerId", "AlbumId", "AlbumTitle"}
	mutations := []*spanner.Mutation{
		spanner.Insert("Singers", singerColumns, []interface{}{int64(1), "Marc", "Richards"}),
		spanner.Insert("Singers", singerColumns, []interface{}{int64(2), "Catalina", "Smith"}),
		spanner.Insert("Singers", singerColumns, []interface{}{int64(3), "Alice", "Trentor"}),
		spanner.Insert("Singers", singerColumns, []interface{}{int64(4), "Lea", "Martin"}),
		spanner.Insert("Singers", singerColumns, []interface{}{int64(5), "David", "Lomond"}),
		spanner.Insert("Albums", albumColumns, []interface{}{int64(1), int64(1), "Total Junk"}),
		spanner.Insert("Albums", albumColumns, []interface{}{int64(1), int64(2), "Go, Go, Go"}),
		spanner.Insert("Albums", albumColumns, []interface{}{int64(2), int64(1), "Green"}),
		spanner.Insert("Albums", albumColumns, []interface{}{int64(2), int64(2), "Forever Hold Your Peace"}),
		spanner.Insert("Albums", albumColumns, []interface{}{int64(2), int64(3), "Terrified"}),
	}
	// Mutations can be written outside an explicit transaction using SpannerConn#Apply.
	if err := conn.Raw(func(driverConn interface{}) error {
		spannerConn, ok := driverConn.(spannerdriver.SpannerConn)
		if !ok {
			return fmt.Errorf("unexpected driver connection %v, expected SpannerConn", driverConn)
		}
		_, err = spannerConn.Apply(ctx, mutations)
		return err
	}); err != nil {
		return err
	}
	fmt.Fprintf(w, "Inserted %v rows\n", len(mutations))

	return nil
}

使用 write 引數執行以下範例:

go run getting_started_guide.go write projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

使用 SQL 查詢資料

Spanner 支援可用於讀取資料的 SQL 介面。您可以透過 Google Cloud CLI 在指令列上存取這個介面,也可以透過程式輔助方式使用 Spanner 資料庫/SQL 驅動程式存取這個介面。

使用指令列

執行下列 SQL 陳述式,從 Albums 資料表讀取所有資料欄的值:

gcloud spanner databases execute-sql example-db --instance=test-instance \
    --sql='SELECT SingerId, AlbumId, AlbumTitle FROM Albums'

結果應為:

SingerId AlbumId AlbumTitle
1        1       Total Junk
1        2       Go, Go, Go
2        1       Green
2        2       Forever Hold Your Peace
2        3       Terrified

使用 Spanner 資料庫/SQL 驅動程式

除了在指令列上執行 SQL 陳述式外,您也可以使用 Spanner 資料庫/SQL 驅動程式,以程式輔助方式發出相同的 SQL 陳述式。

您可以使用下列函式和結構體執行 SQL 查詢:

  • DB 結構體中的 QueryContext 函式:使用此函式執行會傳回資料列的 SQL 陳述式,例如含有 THEN RETURN 子句的查詢或 DML 陳述式。
  • Rows 結構體:使用這個結構體存取 SQL 陳述式傳回的資料。

以下範例使用 QueryContext 函式:

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

	_ "github.com/googleapis/go-sql-spanner"
)

func QueryData(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	rows, err := db.QueryContext(ctx,
		`SELECT SingerId, AlbumId, AlbumTitle
		FROM Albums
		ORDER BY SingerId, AlbumId`)
	defer rows.Close()
	if err != nil {
		return err
	}
	for rows.Next() {
		var singerId, albumId int64
		var title string
		err = rows.Scan(&singerId, &albumId, &title)
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title)
	}
	if rows.Err() != nil {
		return rows.Err()
	}
	return rows.Close()
}

使用下列指令執行���例:

go run getting_started_guide.go query projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

使用 SQL 參數執行查詢

如果應用程式有經常執行的查詢,您可以透過參數化來提升效能。系統可快取並重新使用產生的參數查詢,減少編譯的成本。詳情請參閱「使用查詢參數,針對經常執行的查詢加快速度」。

以下範例說明如何在 WHERE 子句中使用參數,查詢包含 LastName 特定值的記錄。

Spanner 資料庫/SQL 驅動程式支援位置和命名的查詢參數。SQL 陳述式中的 ? 表示位置查詢參數。將查詢參數值做為額外引數傳遞至 QueryContext 函式。例如:

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

	_ "github.com/googleapis/go-sql-spanner"
)

func QueryDataWithParameter(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	rows, err := db.QueryContext(ctx,
		`SELECT SingerId, FirstName, LastName
		FROM Singers
		WHERE LastName = ?`, "Garcia")
	defer rows.Close()
	if err != nil {
		return err
	}
	for rows.Next() {
		var singerId int64
		var firstName, lastName string
		err = rows.Scan(&singerId, &firstName, &lastName)
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "%v %v %v\n", singerId, firstName, lastName)
	}
	if rows.Err() != nil {
		return rows.Err()
	}
	return rows.Close()
}

使用下列指令執行範例:

go run getting_started_guide.go querywithparameter projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

12 Melissa Garcia

更新資料庫結構定義

假設您需要新增名稱為 MarketingBudget 的新資料欄到 Albums 資料表,必須先更新資料庫結構定義,才能新增新資料欄到現有的資料表。Spanner 可在資料庫持續處理流量時,支援資料庫的結構定義更新作業。結構定義更新作業不需要讓資料庫離線,也不會鎖定整個資料表或資料欄;您可以在結構定義更新期間持續將資料���入資料庫。詳情請參閱進行結構定義更新一文中支援的結構定義更新和結構定義變更效能。

新增資料欄

您可以使用 Google Cloud CLI 在指令列上新增資料欄,或是使用程式輔助方式透過 Spanner 資料庫/SQL 驅動程式新增資料欄。

使用指令列

使用下列 ALTER TABLE 指令,在資料表中新增資料欄:

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget INT64'

畫面上會顯示下列訊息:

Schema updating...done.

使用 Spanner 資料庫/SQL 驅動程式

使用 ExecContext 函式修改結構定義:

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

	_ "github.com/googleapis/go-sql-spanner"
)

func AddColumn(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	_, err = db.ExecContext(ctx,
		`ALTER TABLE Albums
			ADD COLUMN MarketingBudget INT64`)
	if err != nil {
		return err
	}

	fmt.Fprint(w, "Added MarketingBudget column\n")
	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go addcolumn projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

Added MarketingBudget column.

執行 DDL 批次

建議您一次執行多項結構定義修改作業。使用 START BATCH DDLRUN BATCH 指令執行 DDL 批次。以下範例會在一個批次中建立兩個資料表:

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

	_ "github.com/googleapis/go-sql-spanner"
)

func DdlBatch(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Executing multiple DDL statements as one batch is
	// more efficient than executing each statement
	// individually.
	conn, err := db.Conn(ctx)
	defer conn.Close()

	if _, err := conn.ExecContext(ctx, "start batch ddl"); err != nil {
		return err
	}
	if _, err := conn.ExecContext(ctx,
		`CREATE TABLE Venues (
			VenueId     INT64 NOT NULL,
			Name        STRING(1024),
			Description JSON,
		) PRIMARY KEY (VenueId)`); err != nil {
		return err
	}
	if _, err := conn.ExecContext(ctx,
		`CREATE TABLE Concerts (
			ConcertId INT64 NOT NULL,
			VenueId   INT64 NOT NULL,
			SingerId  INT64 NOT NULL,
			StartTime TIMESTAMP,
			EndTime   TIMESTAMP,
			CONSTRAINT Fk_Concerts_Venues FOREIGN KEY
				(VenueId) REFERENCES Venues (VenueId),
			CONSTRAINT Fk_Concerts_Singers FOREIGN KEY
				(SingerId) REFERENCES Singers (SingerId),
		) PRIMARY KEY (ConcertId)`); err != nil {
		return err
	}
	// `run batch` sends the DDL statements to Spanner and blocks until
	// all statements have finished executing.
	if _, err := conn.ExecContext(ctx, "run batch"); err != nil {
		return err
	}

	fmt.Fprint(w, "Added Venues and Concerts tables\n")
	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go ddlbatch projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

Added Venues and Concerts tables.

寫入資料到新資料欄

以下程式碼會將資料寫入新資料欄,並在 Albums(1, 1)Albums(2, 2) 這兩個索引鍵表示的資料列中將 MarketingBudget 分別設為 100000500000

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

	"cloud.google.com/go/spanner"
	spannerdriver "github.com/googleapis/go-sql-spanner"
)

func UpdateDataWithMutations(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Get a connection so that we can get access to the Spanner specific
	// connection interface SpannerConn.
	conn, err := db.Conn(ctx)
	if err != nil {
		return err
	}
	defer conn.Close()

	cols := []string{"SingerId", "AlbumId", "MarketingBudget"}
	mutations := []*spanner.Mutation{
		spanner.Update("Albums", cols, []interface{}{1, 1, 100000}),
		spanner.Update("Albums", cols, []interface{}{2, 2, 500000}),
	}
	if err := conn.Raw(func(driverConn interface{}) error {
		spannerConn, ok := driverConn.(spannerdriver.SpannerConn)
		if !ok {
			return fmt.Errorf("unexpected driver connection %v, "+
				"expected SpannerConn", driverConn)
		}
		_, err = spannerConn.Apply(ctx, mutations)
		return err
	}); err != nil {
		return err
	}
	fmt.Fprintf(w, "Updated %v albums\n", len(mutations))

	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go update projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

Updated 2 albums

您也可以執行 SQL 查詢,藉此擷取剛寫入的值。

以下範例使用 QueryContext 函式執行查詢:

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

	_ "github.com/googleapis/go-sql-spanner"
)

func QueryNewColumn(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	rows, err := db.QueryContext(ctx,
		`SELECT SingerId, AlbumId, MarketingBudget
		FROM Albums
		ORDER BY SingerId, AlbumId`)
	defer rows.Close()
	if err != nil {
		return err
	}
	for rows.Next() {
		var singerId, albumId int64
		var marketingBudget sql.NullInt64
		err = rows.Scan(&singerId, &albumId, &marketingBudget)
		if err != nil {
			return err
		}
		budget := "NULL"
		if marketingBudget.Valid {
			budget = fmt.Sprintf("%v", marketingBudget.Int64)
		}
		fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, budget)
	}
	if rows.Err() != nil {
		return rows.Err()
	}
	return rows.Close()
}

如要執行這項查詢,請執行下列指令:

go run getting_started_guide.go querymarketingbudget projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

畫面上會顯示下列訊息:

1 1 100000
1 2 null
2 1 null
2 2 500000
2 3 null

更新資料

您可以在讀寫交易中使用 DML 來更新資料。

呼叫 DB.BeginTx 即可在資料庫/SQL 中執行讀寫交易。

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

	_ "github.com/googleapis/go-sql-spanner"
)

func WriteWithTransactionUsingDml(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Transfer marketing budget from one album to another. We do it in a
	// transaction to ensure that the transfer is atomic.
	tx, err := db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return err
	}
	// The Spanner database/sql driver supports both positional and named
	// query parameters. This query uses named query parameters.
	const selectSql = "SELECT MarketingBudget " +
		"FROM Albums " +
		"WHERE SingerId = @singerId and AlbumId = @albumId"
	// Get the marketing_budget of singer 2 / album 2.
	row := tx.QueryRowContext(ctx, selectSql,
		sql.Named("singerId", 2), sql.Named("albumId", 2))
	var budget2 int64
	if err := row.Scan(&budget2); err != nil {
		tx.Rollback()
		return err
	}
	const transfer = 20000
	// The transaction will only be committed if this condition still holds
	// at the time of commit. Otherwise, the transaction will be aborted.
	if budget2 >= transfer {
		// Get the marketing_budget of singer 1 / album 1.
		row := tx.QueryRowContext(ctx, selectSql,
			sql.Named("singerId", 1), sql.Named("albumId", 1))
		var budget1 int64
		if err := row.Scan(&budget1); err != nil {
			tx.Rollback()
			return err
		}
		// Transfer part of the marketing budget of Album 2 to Album 1.
		budget1 += transfer
		budget2 -= transfer
		const updateSql = "UPDATE Albums " +
			"SET MarketingBudget = @budget " +
			"WHERE SingerId = @singerId and AlbumId = @albumId"
		// Start a DML batch and execute it as part of the current transaction.
		if _, err := tx.ExecContext(ctx, "start batch dml"); err != nil {
			tx.Rollback()
			return err
		}
		if _, err := tx.ExecContext(ctx, updateSql,
			sql.Named("singerId", 1),
			sql.Named("albumId", 1),
			sql.Named("budget", budget1)); err != nil {
			_, _ = tx.ExecContext(ctx, "abort batch")
			tx.Rollback()
			return err
		}
		if _, err := tx.ExecContext(ctx, updateSql,
			sql.Named("singerId", 2),
			sql.Named("albumId", 2),
			sql.Named("budget", budget2)); err != nil {
			_, _ = tx.ExecContext(ctx, "abort batch")
			tx.Rollback()
			return err
		}
		// `run batch` sends the DML statements to Spanner.
		// The result contains the total affected rows across the entire batch.
		result, err := tx.ExecContext(ctx, "run batch")
		if err != nil {
			tx.Rollback()
			return err
		}
		if affected, err := result.RowsAffected(); err != nil {
			tx.Rollback()
			return err
		} else if affected != 2 {
			// The batch should update 2 rows.
			tx.Rollback()
			return fmt.Errorf("unexpected number of rows affected: %v", affected)
		}
	}
	// Commit the current transaction.
	if err := tx.Commit(); err != nil {
		return err
	}

	fmt.Fprintln(w, "Transferred marketing budget from Album 2 to Album 1")

	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go writewithtransactionusingdml projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

交易標記和要求標記

使用交易標記和要求標記排解 Spanner 中的交易和查詢問題。您可以將其他交易選項傳遞至 spannerdriver.BeginReadWriteTransaction 函式。

使用 spannerdriver.ExecOptions 傳遞 SQL 陳述式的其他查詢選項。例如:

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

	"cloud.google.com/go/spanner"
	spannerdriver "github.com/googleapis/go-sql-spanner"
)

func Tags(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Use the spannerdriver.BeginReadWriteTransaction function
	// to specify specific Spanner options, such as transaction tags.
	tx, err := spannerdriver.BeginReadWriteTransaction(ctx, db,
		spannerdriver.ReadWriteTransactionOptions{
			TransactionOptions: spanner.TransactionOptions{
				TransactionTag: "example-tx-tag",
			},
		})
	if err != nil {
		return err
	}

	// Pass in an argument of type spannerdriver.ExecOptions to supply
	// additional options for a statement.
	row := tx.QueryRowContext(ctx, "SELECT MarketingBudget "+
		"FROM Albums "+
		"WHERE SingerId=? and AlbumId=?",
		spannerdriver.ExecOptions{
			QueryOptions: spanner.QueryOptions{RequestTag: "query-marketing-budget"},
		}, 1, 1)
	var budget int64
	if err := row.Scan(&budget); err != nil {
		tx.Rollback()
		return err
	}

	// Reduce the marketing budget by 10% if it is more than 1,000.
	if budget > 1000 {
		budget = int64(float64(budget) - float64(budget)*0.1)
		if _, err := tx.ExecContext(ctx,
			`UPDATE Albums SET MarketingBudget=@budget 
               WHERE SingerId=@singerId AND AlbumId=@albumId`,
			spannerdriver.ExecOptions{
				QueryOptions: spanner.QueryOptions{RequestTag: "reduce-marketing-budget"},
			},
			sql.Named("budget", budget),
			sql.Named("singerId", 1),
			sql.Named("albumId", 1)); err != nil {
			tx.Rollback()
			return err
		}
	}
	// Commit the current transaction.
	if err := tx.Commit(); err != nil {
		return err
	}
	fmt.Fprintln(w, "Reduced marketing budget")

	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go tags projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

使用唯讀交易擷取資料

假設您想要在相同時間戳記執行一次以上的讀取作業。唯讀交易會觀察出交易修訂記錄中一致的前置字串,讓應用程式取得的資料始終保持一致。將 TxOptions.ReadOnly 欄位設為 true,即可執行唯讀交易。

以下顯示如何執行查詢,並在同一個唯讀交易中執行讀取作業:

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

	_ "github.com/googleapis/go-sql-spanner"
)

func ReadOnlyTransaction(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Start a read-only transaction by supplying additional transaction options.
	tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})

	albumsOrderedById, err := tx.QueryContext(ctx,
		`SELECT SingerId, AlbumId, AlbumTitle
		FROM Albums
		ORDER BY SingerId, AlbumId`)
	defer albumsOrderedById.Close()
	if err != nil {
		return err
	}
	for albumsOrderedById.Next() {
		var singerId, albumId int64
		var title string
		err = albumsOrderedById.Scan(&singerId, &albumId, &title)
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title)
	}

	albumsOrderedTitle, err := tx.QueryContext(ctx,
		`SELECT SingerId, AlbumId, AlbumTitle
		FROM Albums
		ORDER BY AlbumTitle`)
	defer albumsOrderedTitle.Close()
	if err != nil {
		return err
	}
	for albumsOrderedTitle.Next() {
		var singerId, albumId int64
		var title string
		err = albumsOrderedTitle.Scan(&singerId, &albumId, &title)
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title)
	}

	// End the read-only transaction by calling Commit.
	return tx.Commit()
}

使用下列指令執行範例:

go run getting_started_guide.go readonlytransaction projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

結果顯示:

    1 1 Total Junk
    1 2 Go, Go, Go
    2 1 Green
    2 2 Forever Hold Your Peace
    2 3 Terrified
    2 2 Forever Hold Your Peace
    1 2 Go, Go, Go
    2 1 Green
    2 3 Terrified
    1 1 Total Junk

分區查詢和 Data Boost

partitionQuery API 會將查詢分成較小的片段 (稱為「分區」),並使用多部機器平行擷取分區。每個分區都會由分區權杖識別。partitionQuery API 的延遲時間比標準 query API 還要長,因為它只適用於匯出或掃描整個資料庫等大量作業。

Data Boost 可讓您執行分析查詢和資料匯出作業,對已佈建 Spanner 執行個體的現有工作負載幾乎不會造成影響。Data Boost 僅支援分區查詢

以下範例說明如何使用資料庫/SQL 驅動程式搭配 Data Boost 執行分割查詢:

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

	"cloud.google.com/go/spanner"
	spannerdriver "github.com/googleapis/go-sql-spanner"
)

func DataBoost(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	// Run a partitioned query that uses Data Boost.
	rows, err := db.QueryContext(ctx,
		"SELECT SingerId, FirstName, LastName from Singers",
		spannerdriver.ExecOptions{
			PartitionedQueryOptions: spannerdriver.PartitionedQueryOptions{
				// AutoPartitionQuery instructs the Spanner database/sql driver to
				// automatically partition the query and execute each partition in parallel.
				// The rows are returned as one result set in undefined order.
				AutoPartitionQuery: true,
			},
			QueryOptions: spanner.QueryOptions{
				// Set DataBoostEnabled to true to enable DataBoost.
				// See https://cloud.google.com/spanner/docs/databoost/databoost-overview
				// for more information.
				DataBoostEnabled: true,
			},
		})
	defer rows.Close()
	if err != nil {
		return err
	}
	type Singer struct {
		SingerId  int64
		FirstName string
		LastName  string
	}
	var singers []Singer
	for rows.Next() {
		var singer Singer
		err = rows.Scan(&singer.SingerId, &singer.FirstName, &singer.LastName)
		if err != nil {
			return err
		}
		singers = append(singers, singer)
	}
	// Queries that use the AutoPartition option return rows in undefined order,
	// so we need to sort them in memory to guarantee the output order.
	slices.SortFunc(singers, func(a, b Singer) int {
		return int(a.SingerId - b.SingerId)
	})
	for _, s := range singers {
		fmt.Fprintf(w, "%v %v %v\n", s.SingerId, s.FirstName, s.LastName)
	}

	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go databoost projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

分區 DML

分區資料操縱語言 (DML) 是專為下列類型的大量更新和刪除作業而設計:

  • 定期清理和垃圾收集。
  • 對具有預設值的新資料欄進行補充作業。
import (
	"context"
	"database/sql"
	"fmt"
	"io"

	_ "github.com/googleapis/go-sql-spanner"
)

func PartitionedDml(ctx context.Context, w io.Writer, databaseName string) error {
	db, err := sql.Open("spanner", databaseName)
	if err != nil {
		return err
	}
	defer db.Close()

	conn, err := db.Conn(ctx)
	if err != nil {
		return err
	}
	// Enable Partitioned DML on this connection.
	if _, err := conn.ExecContext(ctx, "SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"); err != nil {
		return fmt.Errorf("failed to change DML mode to Partitioned_Non_Atomic: %v", err)
	}
	// Back-fill a default value for the MarketingBudget column.
	res, err := conn.ExecContext(ctx, "UPDATE Albums SET MarketingBudget=0 WHERE MarketingBudget IS NULL")
	if err != nil {
		return err
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get affected rows: %v", err)
	}

	// Partitioned DML returns the minimum number of records that were affected.
	fmt.Fprintf(w, "Updated at least %v albums\n", affected)

	// Closing the connection will return it to the connection pool. The DML mode will automatically be reset to the
	// default TRANSACTIONAL mode when the connection is returned to the pool, so we do not need to change it back
	// manually.
	_ = conn.Close()

	return nil
}

使用下列指令執行範例:

go run getting_started_guide.go pdml projects/$GCLOUD_PROJECT/instances/test-instance/databases/example-db

清除所用資源

如要避免系統向您的 Cloud Billing 帳戶收取您在本教學課程中所用資源的額外費用,請捨棄資料庫並刪除您建立的執行個體。

刪除資料庫

您刪除執行個體時,也會自動刪除其中所有資料庫。這個步驟將示範如何在保留執行個體的情況下刪除資料庫 (您仍須支付執行個體費用)。

使用指令列

gcloud spanner databases delete example-db --instance=test-instance

使用 Google Cloud 主控台

  1. 前往 Google Cloud 控制台的「Spanner 執行個體」頁面。

    前往「Instances」(執行個體) 頁面

  2. 點選執行個體。

  3. 點選您要刪除的資料庫。

  4. 在「Database details」(資料庫詳細資料) 頁面,按一下 [Delete] (刪除)

  5. 確認您要刪除資料庫,然後按一下 [Delete] (刪除)

刪除執行個體

您刪除執行個體時,也會自動捨棄您在其中建立的所有資料庫。

使用指令列

gcloud spanner instances delete test-instance

使用 Google Cloud 主控台

  1. 前往 Google Cloud 控制台的「Spanner 執行個體」頁面。

    前往「Instances」(執行個體) 頁面

  2. 點選執行個體。

  3. 按一下 [Delete] (刪除)

  4. 確認您要刪除執行個體,然後按一下 [Delete] (刪除)

後續步驟