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.

浏览示例文件

示例代码库包含一个示例,展示了如何将 Spanner 与数据库/SQL 搭配使用。

请浏览 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 交互。数据库名称和其他连接属性在 database/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 database/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) 键控的行,该代码会将 MarketingBudget 设置为 100000;而对于 Albums(2, 2) 键控的行,该代码会将其设置为 500000

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 以在 database/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 的延迟时间比标准查询 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

清理

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生额外费用,请删除数据库和您创建的实例。

删除数据库

如果您删除一个实例,则该实例中的所有数据库都会自动删除。 本步骤演示了如何在不删除实例的情况下删除数据库(您仍需为该实例付费)。

在命令行中

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

使用 Google Cloud 控制台

  1. 前往 Google Cloud 控制台中的 Spanner 实例页面。

    转到“实例”页面

  2. 点击实例。

  3. 点击您想删除的数据库。

  4. 数据库详细信息页面中,点击删除

  5. 确认您要删除数据库并点击删除

删除实例

删除实例会自动删除在该实例中创建的所有数据库。

在命令行中

gcloud spanner instances delete test-instance

使用 Google Cloud 控制台

  1. 前往 Google Cloud 控制台中的 Spanner 实例页面。

    转到“实例”页面

  2. 点击您的实例。

  3. 点击删除

  4. 确认您要删除实例并点击删除

后续步骤